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