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