Branch data Line data Source code
1 : : /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
2 : : // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
3 : : // SPDX-FileCopyrightText: 2008 litl, LLC
4 : : // SPDX-FileCopyrightText: 2012 Red Hat, Inc.
5 : : // SPDX-FileCopyrightText: 2019 Endless Mobile, Inc.
6 : : // SPDX-FileCopyrightText: 2021 Philip Chimento <philip.chimento@gmail.com>
7 : :
8 : : #include <config.h> // for GJS_VERSION
9 : :
10 : : #include <stdint.h>
11 : : #include <stdio.h>
12 : : #include <time.h> // for tzset
13 : :
14 : : #include <glib-object.h>
15 : : #include <glib.h>
16 : :
17 : : #include <js/CallArgs.h>
18 : : #include <js/Date.h> // for ResetTimeZone
19 : : #include <js/ErrorReport.h> // for ReportUncatchableException
20 : : #include <js/GCAPI.h> // for JS_GC
21 : : #include <js/JSON.h>
22 : : #include <js/PropertyAndElement.h>
23 : : #include <js/PropertyDescriptor.h> // for JSPROP_READONLY
24 : : #include <js/PropertySpec.h>
25 : : #include <js/RootingAPI.h>
26 : : #include <js/TypeDecls.h>
27 : : #include <js/Value.h> // for NullValue
28 : : #include <js/friend/DumpFunctions.h>
29 : : #include <jsapi.h> // for JS_GetFunctionObject, JS_NewPlainObject
30 : : #include <jsfriendapi.h> // for GetFunctionNativeReserved, NewFunctionByIdW...
31 : :
32 : : #include "gi/object.h"
33 : : #include "gjs/atoms.h"
34 : : #include "gjs/auto.h"
35 : : #include "gjs/context-private.h"
36 : : #include "gjs/jsapi-util-args.h"
37 : : #include "gjs/jsapi-util.h"
38 : : #include "gjs/macros.h"
39 : : #include "gjs/profiler-private.h"
40 : : #include "modules/system.h"
41 : : #include "util/log.h"
42 : : #include "util/misc.h" // for LogFile
43 : :
44 : : /* Note that this cannot be relied on to test whether two objects are the same!
45 : : * SpiderMonkey can move objects around in memory during garbage collection, and
46 : : * it can also deduplicate identical instances of objects in memory. */
47 : 10 : static bool gjs_address_of(JSContext* cx, unsigned argc, JS::Value* vp) {
48 : 10 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
49 : 10 : JS::RootedObject target_obj{cx};
50 : :
51 [ - + ]: 10 : if (!gjs_parse_call_args(cx, "addressOf", args, "o", "object", &target_obj))
52 : 0 : return false;
53 : :
54 : 10 : Gjs::AutoChar pointer_string{g_strdup_printf("%p", target_obj.get())};
55 : 10 : return gjs_string_from_utf8(cx, pointer_string, args.rval());
56 : 10 : }
57 : :
58 : : GJS_JSAPI_RETURN_CONVENTION
59 : 10 : static bool gjs_address_of_gobject(JSContext* cx, unsigned argc,
60 : : JS::Value* vp) {
61 : 10 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
62 : 10 : JS::RootedObject target_obj(cx);
63 : :
64 [ - + ]: 10 : if (!gjs_parse_call_args(cx, "addressOfGObject", args, "o", "object",
65 : : &target_obj))
66 : 0 : return false;
67 : :
68 : : GObject* obj;
69 [ + + ]: 10 : if (!ObjectBase::to_c_ptr(cx, target_obj, &obj)) {
70 : 1 : gjs_throw(cx, "Object %p is not a GObject", &target_obj);
71 : 1 : return false;
72 : : }
73 : :
74 : 9 : Gjs::AutoChar pointer_string{g_strdup_printf("%p", obj)};
75 : 9 : return gjs_string_from_utf8(cx, pointer_string, args.rval());
76 : 10 : }
77 : :
78 : 3 : static bool gjs_refcount(JSContext* cx, unsigned argc, JS::Value* vp) {
79 : 3 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
80 : 3 : JS::RootedObject target_obj{cx};
81 : :
82 [ - + ]: 3 : if (!gjs_parse_call_args(cx, "refcount", args, "o", "object", &target_obj))
83 : 0 : return false;
84 : :
85 : : GObject* obj;
86 [ - + ]: 3 : if (!ObjectBase::to_c_ptr(cx, target_obj, &obj))
87 : 0 : return false;
88 [ - + ]: 3 : if (!obj) {
89 : : // Object already disposed, treat as refcount 0
90 : 0 : args.rval().setInt32(0);
91 : 0 : return true;
92 : : }
93 : :
94 : 3 : args.rval().setInt32(obj->ref_count);
95 : 3 : return true;
96 : 3 : }
97 : :
98 : 0 : static bool gjs_breakpoint(JSContext* cx, unsigned argc, JS::Value* vp) {
99 : 0 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
100 [ # # ]: 0 : if (!gjs_parse_call_args(cx, "breakpoint", args, ""))
101 : 0 : return false;
102 : 0 : G_BREAKPOINT();
103 : 0 : args.rval().setUndefined();
104 : 0 : return true;
105 : : }
106 : :
107 : : // This can reduce performance, so should be used for debugging only.
108 : : // js::CollectNurseryBeforeDump() promotes any live objects in the nursery to
109 : : // the tenured heap. This is slow, but this way, we are certain to get an
110 : : // accurate picture of the heap.
111 : 1 : static bool gjs_dump_heap(JSContext* cx, unsigned argc, JS::Value* vp) {
112 : 1 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
113 : 1 : Gjs::AutoChar filename;
114 : :
115 [ - + ]: 1 : if (!gjs_parse_call_args(cx, "dumpHeap", args, "|F", "filename", &filename))
116 : 0 : return false;
117 : :
118 : 1 : LogFile file(filename);
119 [ + - ]: 1 : if (file.has_error()) {
120 : 1 : gjs_throw(cx, "Cannot dump heap to %s: %s", filename.get(),
121 : : file.errmsg());
122 : 1 : return false;
123 : : }
124 : 0 : js::DumpHeap(cx, file.fp(), js::CollectNurseryBeforeDump);
125 : :
126 [ # # ]: 0 : gjs_debug(GJS_DEBUG_CONTEXT, "Heap dumped to %s",
127 : 0 : filename ? filename.get() : "stdout");
128 : :
129 : 0 : args.rval().setUndefined();
130 : 0 : return true;
131 : 1 : }
132 : :
133 : 84 : static bool gjs_gc(JSContext* cx, unsigned argc, JS::Value* vp) {
134 : 84 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
135 [ - + ]: 84 : if (!gjs_parse_call_args(cx, "gc", args, ""))
136 : 0 : return false;
137 : 84 : JS_GC(cx);
138 : 84 : args.rval().setUndefined();
139 : 84 : return true;
140 : : }
141 : :
142 : 25 : static bool gjs_exit(JSContext* cx, unsigned argc, JS::Value* vp) {
143 : 25 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
144 : : int32_t ecode;
145 [ - + ]: 25 : if (!gjs_parse_call_args(cx, "exit", args, "i", "ecode", &ecode))
146 : 0 : return false;
147 : :
148 : 25 : GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx);
149 : 25 : gjs->exit(ecode);
150 : 25 : JS::ReportUncatchableException(cx);
151 : 25 : return false;
152 : : }
153 : :
154 : 0 : static bool gjs_clear_date_caches(JSContext*, unsigned argc, JS::Value* vp) {
155 : 0 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
156 : :
157 : : // Workaround for a bug in SpiderMonkey where tzset is not called before
158 : : // localtime_r, see https://bugzilla.mozilla.org/show_bug.cgi?id=1004706
159 : 0 : tzset();
160 : :
161 : 0 : JS::ResetTimeZone();
162 : :
163 : 0 : args.rval().setUndefined();
164 : 0 : return true;
165 : : }
166 : :
167 : 1 : static bool write_gc_info(const char16_t* buf, uint32_t len, void* data) {
168 : 1 : auto* fp = static_cast<FILE*>(data);
169 : :
170 : : long bytes_written;
171 : 1 : Gjs::AutoChar utf8{g_utf16_to_utf8(reinterpret_cast<const uint16_t*>(buf),
172 : : len, /* items_read = */ nullptr,
173 : 1 : &bytes_written, /* error = */ nullptr)};
174 [ - + ]: 1 : if (!utf8)
175 : 0 : utf8 = g_strdup("<invalid string>");
176 : :
177 : 1 : fwrite(utf8, 1, bytes_written, fp);
178 : 2 : return true;
179 : 1 : }
180 : :
181 : 2 : static bool gjs_dump_memory_info(JSContext* cx, unsigned argc, JS::Value* vp) {
182 : 2 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
183 : :
184 : 2 : Gjs::AutoChar filename;
185 [ - + ]: 2 : if (!gjs_parse_call_args(cx, "dumpMemoryInfo", args, "|F", "filename",
186 : : &filename))
187 : 0 : return false;
188 : :
189 : : int64_t gc_counters[Gjs::GCCounters::N_COUNTERS];
190 : :
191 : : // The object returned from NewMemoryInfoObject has gcBytes and mallocBytes
192 : : // properties which are the sum (over all zones) of bytes used. gcBytes is
193 : : // the number of bytes in garbage-collectable things (GC things).
194 : : // mallocBytes is the number of bytes allocated with malloc (reported with
195 : : // JS::AddAssociatedMemory).
196 : : //
197 : : // This info leaks internal state of the JS engine, which is why it is not
198 : : // returned to the caller, only dumped to a file and piped to Sysprof.
199 : : //
200 : : // The object also has a zone property with its own gcBytes and mallocBytes
201 : : // properties, representing the bytes used in the zone that the memory
202 : : // object belongs to. We only have one zone in GJS's context, so
203 : : // zone.gcBytes and zone.mallocBytes are a good measure for how much memory
204 : : // the actual user program is occupying. These are the values that we expose
205 : : // as counters in Sysprof. The difference between these values and the sum
206 : : // values is due to the self-hosting zone and atoms zone, that represent
207 : : // overhead of the JS engine.
208 : :
209 : 2 : JS::RootedObject gc_info(cx, js::gc::NewMemoryInfoObject(cx));
210 [ - + ]: 2 : if (!gc_info)
211 : 0 : return false;
212 : :
213 : 2 : const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
214 : : int32_t val;
215 : 2 : JS::RootedObject zone_info(cx);
216 : 2 : if (!gjs_object_require_property(cx, gc_info, "gc.zone", atoms.zone(),
217 [ + - ]: 6 : &zone_info) ||
218 [ - + - + ]: 4 : !gjs_object_require_property(cx, zone_info, "gc.zone.gcBytes",
219 : : atoms.gc_bytes(), &val))
220 : 0 : return false;
221 : 2 : gc_counters[Gjs::GCCounters::GC_HEAP_BYTES] = static_cast<int64_t>(val);
222 [ - + ]: 2 : if (!gjs_object_require_property(cx, zone_info, "gc.zone.mallocBytes",
223 : : atoms.malloc_bytes(), &val))
224 : 0 : return false;
225 : 2 : gc_counters[Gjs::GCCounters::MALLOC_HEAP_BYTES] = static_cast<int64_t>(val);
226 : :
227 : 2 : auto* gjs = GjsContextPrivate::from_cx(cx);
228 [ - + - + ]: 2 : if (gjs->profiler() &&
229 [ # # ]: 0 : !gjs_profiler_sample_gc_memory_info(gjs->profiler(), gc_counters)) {
230 : 0 : gjs_throw(cx, "Could not write GC counters to profiler");
231 : 0 : return false;
232 : : }
233 : :
234 : 2 : LogFile file(filename);
235 [ + + ]: 2 : if (file.has_error()) {
236 : 1 : gjs_throw(cx, "Cannot dump memory info to %s: %s", filename.get(),
237 : : file.errmsg());
238 : 1 : return false;
239 : : }
240 : :
241 : 1 : fprintf(file.fp(), "# GC Memory Info Object #\n\n```json\n");
242 : 1 : JS::RootedValue v_gc_info(cx, JS::ObjectValue(*gc_info));
243 : 1 : JS::RootedValue spacing(cx, JS::Int32Value(2));
244 [ - + ]: 1 : if (!JS_Stringify(cx, &v_gc_info, nullptr, spacing, write_gc_info,
245 : 1 : file.fp()))
246 : 0 : return false;
247 : 1 : fprintf(file.fp(), "\n```\n");
248 : :
249 : 1 : args.rval().setUndefined();
250 : 1 : return true;
251 : 2 : }
252 : :
253 : : static JSFunctionSpec module_funcs[] = {
254 : : JS_FN("addressOf", gjs_address_of, 1, GJS_MODULE_PROP_FLAGS),
255 : : JS_FN("addressOfGObject", gjs_address_of_gobject, 1, GJS_MODULE_PROP_FLAGS),
256 : : JS_FN("refcount", gjs_refcount, 1, GJS_MODULE_PROP_FLAGS),
257 : : JS_FN("breakpoint", gjs_breakpoint, 0, GJS_MODULE_PROP_FLAGS),
258 : : JS_FN("dumpHeap", gjs_dump_heap, 1, GJS_MODULE_PROP_FLAGS),
259 : : JS_FN("dumpMemoryInfo", gjs_dump_memory_info, 0, GJS_MODULE_PROP_FLAGS),
260 : : JS_FN("gc", gjs_gc, 0, GJS_MODULE_PROP_FLAGS),
261 : : JS_FN("exit", gjs_exit, 0, GJS_MODULE_PROP_FLAGS),
262 : : JS_FN("clearDateCaches", gjs_clear_date_caches, 0, GJS_MODULE_PROP_FLAGS),
263 : : JS_FS_END};
264 : :
265 : 69 : static bool get_program_args(JSContext* cx, unsigned argc, JS::Value* vp) {
266 : : static const size_t SLOT_ARGV = 0;
267 : :
268 : 69 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
269 : 69 : GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx);
270 : :
271 : 69 : JS::RootedValue v_argv(
272 : 69 : cx, js::GetFunctionNativeReserved(&args.callee(), SLOT_ARGV));
273 : :
274 [ + + ]: 69 : if (v_argv.isUndefined()) {
275 : : // First time this property is accessed, build the array
276 : 66 : JS::RootedObject argv(cx, priv->build_args_array());
277 [ - + ]: 66 : if (!argv)
278 : 0 : return false;
279 : 66 : js::SetFunctionNativeReserved(&args.callee(), SLOT_ARGV,
280 : 66 : JS::ObjectValue(*argv));
281 : 66 : args.rval().setObject(*argv);
282 [ + - ]: 66 : } else {
283 : 3 : args.rval().set(v_argv);
284 : : }
285 : :
286 : 69 : return true;
287 : 69 : }
288 : :
289 : 80 : bool gjs_js_define_system_stuff(JSContext* cx, JS::MutableHandleObject module) {
290 : 80 : module.set(JS_NewPlainObject(cx));
291 : :
292 [ - + ]: 80 : if (!JS_DefineFunctions(cx, module, &module_funcs[0]))
293 : 0 : return false;
294 : :
295 : 80 : GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx);
296 : 80 : const char* program_name = gjs->program_name();
297 : 80 : const char* program_path = gjs->program_path();
298 : :
299 : 80 : JS::RootedValue v_program_invocation_name{cx};
300 : 80 : JS::RootedValue v_program_path{cx, JS::NullValue()};
301 [ + + ]: 80 : if (program_path) {
302 [ - + ]: 9 : if (!gjs_string_from_utf8(cx, program_path, &v_program_path))
303 : 0 : return false;
304 : : }
305 : :
306 : 80 : JS::RootedObject program_args_getter{
307 : 80 : cx, JS_GetFunctionObject(js::NewFunctionByIdWithReserved(
308 : 160 : cx, get_program_args, 0, 0, gjs->atoms().program_args()))};
309 : :
310 [ + - ]: 160 : return program_args_getter &&
311 [ + - ]: 160 : gjs_string_from_utf8(cx, program_name, &v_program_invocation_name) &&
312 : : // The name is modeled after program_invocation_name, part of glibc
313 : 80 : JS_DefinePropertyById(cx, module,
314 : 80 : gjs->atoms().program_invocation_name(),
315 : 80 : v_program_invocation_name,
316 [ + - ]: 80 : GJS_MODULE_PROP_FLAGS | JSPROP_READONLY) &&
317 : 80 : JS_DefinePropertyById(cx, module, gjs->atoms().program_path(),
318 : 80 : v_program_path,
319 [ + - ]: 80 : GJS_MODULE_PROP_FLAGS | JSPROP_READONLY) &&
320 : 80 : JS_DefinePropertyById(cx, module, gjs->atoms().program_args(),
321 : 80 : program_args_getter, nullptr,
322 [ + - + - ]: 160 : GJS_MODULE_PROP_FLAGS) &&
323 : 80 : JS_DefinePropertyById(cx, module, gjs->atoms().version(),
324 : : GJS_VERSION,
325 : 80 : GJS_MODULE_PROP_FLAGS | JSPROP_READONLY);
326 : 80 : }
|