Branch data Line data Source code
1 : : // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
2 : : // SPDX-FileCopyrightText: 2016 Christian Hergert <christian@hergert.me>
3 : :
4 : : #include <config.h> // for ENABLE_PROFILER, HAVE_SYS_SYSCALL_H, HAVE_UNISTD_H
5 : :
6 : : #ifdef HAVE_SIGNAL_H
7 : : # include <signal.h> // for siginfo_t, sigevent, sigaction, SIGPROF, ...
8 : : #endif
9 : :
10 : : #ifdef ENABLE_PROFILER
11 : : // IWYU has a weird loop where if this is present, it asks for it to be removed,
12 : : // and if absent, asks for it to be added
13 : : # include <alloca.h> // IWYU pragma: keep
14 : : # include <errno.h>
15 : : # include <stdint.h>
16 : : # include <stdio.h> // for sscanf
17 : : # include <string.h> // for memcpy, strlen
18 : : # include <sys/syscall.h> // for __NR_gettid
19 : : # include <sys/types.h> // for timer_t
20 : : # include <time.h> // for size_t, CLOCK_MONOTONIC, itimerspec, ...
21 : : # ifdef HAVE_UNISTD_H
22 : : # include <unistd.h> // for getpid, syscall
23 : : # endif
24 : : # include <array>
25 : : #endif
26 : :
27 : : #include <glib-object.h>
28 : : #include <glib.h>
29 : :
30 : : #ifdef ENABLE_PROFILER
31 : : # ifdef G_OS_UNIX
32 : : # include <glib-unix.h>
33 : : # endif
34 : : # include <sysprof-capture.h>
35 : : #endif
36 : :
37 : : #include <js/GCAPI.h> // for JSFinalizeStatus, JSGCStatus, GCReason
38 : : #include <js/ProfilingStack.h> // for EnableContextProfilingStack, ...
39 : : #include <js/TypeDecls.h>
40 : : #include <mozilla/Atomics.h> // for ProfilingStack operators
41 : :
42 : : #include "gjs/context.h"
43 : : #include "gjs/jsapi-util.h" // for gjs_explain_gc_reason
44 : : #include "gjs/mem-private.h"
45 : : #include "gjs/profiler-private.h"
46 : : #include "gjs/profiler.h"
47 : :
48 : : #define FLUSH_DELAY_SECONDS 3
49 : :
50 : : /*
51 : : * This is mostly non-exciting code wrapping the builtin Profiler in
52 : : * mozjs. In particular, the profiler consumer is required to "bring your
53 : : * own sampler". We do the very non-surprising thing of using POSIX
54 : : * timers to deliver SIGPROF to the thread containing the JSContext.
55 : : *
56 : : * However, we do use a Linux'ism that allows us to deliver the signal
57 : : * to only a single thread. Doing this in a generic fashion would
58 : : * require thread-registration so that we can mask SIGPROF from all
59 : : * threads except the JS thread. The gecko engine uses tgkill() to do
60 : : * this with a secondary thread instead of using POSIX timers. We could
61 : : * do this too, but it would still be Linux-only.
62 : : *
63 : : * Another option might be to use pthread_kill() and a secondary thread
64 : : * to perform the notification.
65 : : *
66 : : * From within the signal handler, we process the current stack as
67 : : * delivered to us from the JSContext. Any pointer data that comes from
68 : : * the runtime has to be copied, so we keep our own dedup'd string
69 : : * pointers for JavaScript file/line information. Non-JS instruction
70 : : * pointers are just fine, as they can be resolved by parsing the ELF for
71 : : * the file mapped on disk containing that address.
72 : : *
73 : : * As much of this code has to run from signal handlers, it is very
74 : : * important that we don't use anything that can malloc() or lock, or
75 : : * deadlocks are very likely. Most of GjsProfilerCapture is signal-safe.
76 : : */
77 : :
78 : : #define SAMPLES_PER_SEC G_GUINT64_CONSTANT(1000)
79 : : #define NSEC_PER_SEC G_GUINT64_CONSTANT(1000000000)
80 : :
81 [ # # # # : 0 : G_DEFINE_POINTER_TYPE(GjsProfiler, gjs_profiler)
# # ]
82 : :
83 : : struct _GjsProfiler {
84 : : #ifdef ENABLE_PROFILER
85 : : /* The stack for the JSContext profiler to use for current stack
86 : : * information while executing. We will look into this during our
87 : : * SIGPROF handler.
88 : : */
89 : : ProfilingStack stack;
90 : :
91 : : /* The context being profiled */
92 : : JSContext *cx;
93 : :
94 : : /* Buffers and writes our sampled stacks */
95 : : SysprofCaptureWriter* capture;
96 : : GSource* periodic_flush;
97 : :
98 : : SysprofCaptureWriter* target_capture;
99 : :
100 : : // Cache previous values of counters so that we don't overrun the output
101 : : // with counters that don't change very often
102 : : uint64_t last_counter_values[GJS_N_COUNTERS];
103 : : #endif /* ENABLE_PROFILER */
104 : :
105 : : /* The filename to write to */
106 : : char *filename;
107 : :
108 : : /* An FD to capture to */
109 : : int fd;
110 : :
111 : : #ifdef ENABLE_PROFILER
112 : : /* Our POSIX timer to wakeup SIGPROF */
113 : : timer_t timer;
114 : :
115 : : /* Cached copy of our pid */
116 : : GPid pid;
117 : :
118 : : /* Timing information */
119 : : int64_t gc_begin_time;
120 : : int64_t sweep_begin_time;
121 : : int64_t group_sweep_begin_time;
122 : : const char* gc_reason; // statically allocated
123 : :
124 : : /* GLib signal handler ID for SIGUSR2 */
125 : : unsigned sigusr2_id;
126 : : unsigned counter_base; // index of first GObject memory counter
127 : : unsigned gc_counter_base; // index of first GC stats counter
128 : : #endif /* ENABLE_PROFILER */
129 : :
130 : : /* If we are currently sampling */
131 : : unsigned running : 1;
132 : : };
133 : :
134 : : static GjsContext *profiling_context;
135 : :
136 : : #ifdef ENABLE_PROFILER
137 : : /*
138 : : * gjs_profiler_extract_maps:
139 : : *
140 : : * This function will write the mapped section information to the
141 : : * capture file so that the callgraph builder can generate symbols
142 : : * from the stack addresses provided.
143 : : *
144 : : * Returns: %TRUE if successful; otherwise %FALSE and the profile
145 : : * should abort.
146 : : */
147 : 5 : [[nodiscard]] static bool gjs_profiler_extract_maps(GjsProfiler* self) {
148 : 5 : int64_t now = g_get_monotonic_time() * 1000L;
149 : :
150 : 5 : g_assert(((void) "Profiler must be set up before extracting maps", self));
151 : :
152 : 5 : GjsAutoChar path = g_strdup_printf("/proc/%jd/maps", intmax_t(self->pid));
153 : :
154 : : char *content_tmp;
155 : : size_t len;
156 [ - + ]: 5 : if (!g_file_get_contents(path, &content_tmp, &len, nullptr))
157 : 0 : return false;
158 : 5 : GjsAutoChar content = content_tmp;
159 : :
160 : 5 : GjsAutoStrv lines = g_strsplit(content, "\n", 0);
161 : :
162 [ + + ]: 1421 : for (size_t ix = 0; lines[ix]; ix++) {
163 : : char file[256];
164 : : unsigned long start;
165 : : unsigned long end;
166 : : unsigned long offset;
167 : : unsigned long inode;
168 : :
169 : 1416 : file[sizeof file - 1] = '\0';
170 : :
171 : 1416 : int r = sscanf(lines[ix], "%lx-%lx %*15s %lx %*x:%*x %lu %255s",
172 : : &start, &end, &offset, &inode, file);
173 [ + + ]: 1416 : if (r != 5)
174 : 350 : continue;
175 : :
176 [ + + ]: 1066 : if (strcmp("[vdso]", file) == 0) {
177 : 5 : offset = 0;
178 : 5 : inode = 0;
179 : : }
180 : :
181 [ - + ]: 1066 : if (!sysprof_capture_writer_add_map(self->capture, now, -1, self->pid,
182 : : start, end, offset, inode, file))
183 : 0 : return false;
184 : : }
185 : :
186 : 5 : return true;
187 : 5 : }
188 : :
189 : 80 : static void setup_counter_helper(SysprofCaptureCounter* counter,
190 : : const char* counter_name,
191 : : unsigned counter_base, size_t ix) {
192 : 80 : g_snprintf(counter->category, sizeof counter->category, "GJS");
193 : 80 : g_snprintf(counter->name, sizeof counter->name, "%s", counter_name);
194 : 80 : g_snprintf(counter->description, sizeof counter->description, "%s",
195 : 80 : GJS_COUNTER_DESCRIPTIONS[ix]);
196 : 80 : counter->id = uint32_t(counter_base + ix);
197 : 80 : counter->type = SYSPROF_CAPTURE_COUNTER_INT64;
198 : 80 : counter->value.v64 = 0;
199 : 80 : }
200 : :
201 : 5 : [[nodiscard]] static bool gjs_profiler_define_counters(GjsProfiler* self) {
202 : 5 : int64_t now = g_get_monotonic_time() * 1000L;
203 : :
204 : 5 : g_assert(self && "Profiler must be set up before defining counters");
205 : :
206 : : std::array<SysprofCaptureCounter, GJS_N_COUNTERS> counters;
207 : 5 : self->counter_base =
208 : 5 : sysprof_capture_writer_request_counter(self->capture, GJS_N_COUNTERS);
209 : :
210 : : # define SETUP_COUNTER(counter_name, ix) \
211 : : setup_counter_helper(&counters[ix], #counter_name, self->counter_base, \
212 : : ix);
213 : 5 : GJS_FOR_EACH_COUNTER(SETUP_COUNTER);
214 : : # undef SETUP_COUNTER
215 : :
216 [ - + ]: 5 : if (!sysprof_capture_writer_define_counters(
217 : 5 : self->capture, now, -1, self->pid, counters.data(), GJS_N_COUNTERS))
218 : 0 : return false;
219 : :
220 : : std::array<SysprofCaptureCounter, Gjs::GCCounters::N_COUNTERS> gc_counters;
221 : 5 : self->gc_counter_base = sysprof_capture_writer_request_counter(
222 : : self->capture, Gjs::GCCounters::N_COUNTERS);
223 : :
224 : 5 : constexpr size_t category_size = sizeof gc_counters[0].category;
225 : 5 : constexpr size_t name_size = sizeof gc_counters[0].name;
226 : 5 : constexpr size_t description_size = sizeof gc_counters[0].description;
227 : :
228 [ + + ]: 15 : for (size_t ix = 0; ix < Gjs::GCCounters::N_COUNTERS; ix++) {
229 : 10 : g_snprintf(gc_counters[ix].category, category_size, "GJS");
230 : 10 : gc_counters[ix].id = uint32_t(self->gc_counter_base + ix);
231 : 10 : gc_counters[ix].type = SYSPROF_CAPTURE_COUNTER_INT64;
232 : 10 : gc_counters[ix].value.v64 = 0;
233 : : }
234 : 5 : g_snprintf(gc_counters[Gjs::GCCounters::GC_HEAP_BYTES].name, name_size,
235 : : "GC bytes");
236 : 5 : g_snprintf(gc_counters[Gjs::GCCounters::GC_HEAP_BYTES].description,
237 : : description_size, "Bytes used in GC heap");
238 : 5 : g_snprintf(gc_counters[Gjs::GCCounters::MALLOC_HEAP_BYTES].name, name_size,
239 : : "Malloc bytes");
240 : 5 : g_snprintf(gc_counters[Gjs::GCCounters::MALLOC_HEAP_BYTES].description,
241 : : description_size, "Malloc bytes owned by tenured GC things");
242 : :
243 : 5 : return sysprof_capture_writer_define_counters(self->capture, now, -1,
244 : 5 : self->pid, gc_counters.data(),
245 : 5 : Gjs::GCCounters::N_COUNTERS);
246 : : }
247 : :
248 : : #endif /* ENABLE_PROFILER */
249 : :
250 : : /*
251 : : * _gjs_profiler_new:
252 : : * @context: The #GjsContext to profile
253 : : *
254 : : * This creates a new profiler for the #JSContext. It is important that
255 : : * this instance is freed with _gjs_profiler_free() before the context is
256 : : * destroyed.
257 : : *
258 : : * Call gjs_profiler_start() to enable the profiler, and gjs_profiler_stop()
259 : : * when you have finished.
260 : : *
261 : : * The profiler works by enabling the JS profiler in spidermonkey so that
262 : : * sample information is available. A POSIX timer is used to signal SIGPROF
263 : : * to the process on a regular interval to collect the most recent profile
264 : : * sample and stash it away. It is a programming error to mask SIGPROF from
265 : : * the thread controlling the JS context.
266 : : *
267 : : * If another #GjsContext already has a profiler, or @context already has one,
268 : : * then returns %NULL instead.
269 : : *
270 : : * Returns: (transfer full) (nullable): A newly allocated #GjsProfiler
271 : : */
272 : : GjsProfiler *
273 : 5 : _gjs_profiler_new(GjsContext *context)
274 : : {
275 : 5 : g_return_val_if_fail(context, nullptr);
276 : :
277 [ - + ]: 5 : if (profiling_context == context) {
278 : 0 : g_critical("You can only create one profiler at a time.");
279 : 0 : return nullptr;
280 : : }
281 : :
282 [ - + ]: 5 : if (profiling_context) {
283 : 0 : g_message("Not going to profile GjsContext %p; you can only profile "
284 : : "one context at a time.", context);
285 : 0 : return nullptr;
286 : : }
287 : :
288 : 5 : GjsProfiler *self = g_new0(GjsProfiler, 1);
289 : :
290 : : #ifdef ENABLE_PROFILER
291 : 5 : self->cx = static_cast<JSContext *>(gjs_context_get_native_context(context));
292 : 5 : self->pid = getpid();
293 : : #endif
294 : 5 : self->fd = -1;
295 : :
296 : 5 : profiling_context = context;
297 : :
298 : 5 : return self;
299 : : }
300 : :
301 : : /*
302 : : * _gjs_profiler_free:
303 : : * @self: A #GjsProfiler
304 : : *
305 : : * Frees a profiler instance and cleans up any allocated data.
306 : : *
307 : : * If the profiler is running, it will be stopped. This may result in blocking
308 : : * to write the contents of the buffer to the underlying file-descriptor.
309 : : */
310 : : void
311 : 5 : _gjs_profiler_free(GjsProfiler *self)
312 : : {
313 [ - + ]: 5 : if (!self)
314 : 0 : return;
315 : :
316 [ - + ]: 5 : if (self->running)
317 : 0 : gjs_profiler_stop(self);
318 : :
319 : 5 : profiling_context = nullptr;
320 : :
321 [ + + ]: 5 : g_clear_pointer(&self->filename, g_free);
322 : : #ifdef ENABLE_PROFILER
323 [ - + ]: 5 : g_clear_pointer(&self->capture, sysprof_capture_writer_unref);
324 [ - + ]: 5 : g_clear_pointer(&self->periodic_flush, g_source_destroy);
325 [ - + ]: 5 : g_clear_pointer(&self->target_capture, sysprof_capture_writer_unref);
326 : :
327 [ - + ]: 5 : if (self->fd != -1)
328 : 0 : close(self->fd);
329 : :
330 : 5 : self->stack.~ProfilingStack();
331 : : #endif
332 : 5 : g_free(self);
333 : : }
334 : :
335 : : /*
336 : : * _gjs_profiler_is_running:
337 : : * @self: A #GjsProfiler
338 : : *
339 : : * Checks if the profiler is currently running. This means that the JS
340 : : * profiler is enabled and POSIX signal timers are registered.
341 : : *
342 : : * Returns: %TRUE if the profiler is active.
343 : : */
344 : : bool
345 : 104 : _gjs_profiler_is_running(GjsProfiler *self)
346 : : {
347 : 104 : g_return_val_if_fail(self, false);
348 : :
349 : 104 : return self->running;
350 : : }
351 : :
352 : : #ifdef ENABLE_PROFILER
353 : :
354 : 2 : static void gjs_profiler_sigprof(int signum [[maybe_unused]], siginfo_t* info,
355 : : void*) {
356 : 2 : GjsProfiler *self = gjs_context_get_profiler(profiling_context);
357 : :
358 : 2 : g_assert(((void) "SIGPROF handler called with invalid signal info", info));
359 : 2 : g_assert(((void) "SIGPROF handler called with other signal",
360 : : info->si_signo == SIGPROF));
361 : :
362 : : /*
363 : : * NOTE:
364 : : *
365 : : * This is the SIGPROF signal handler. Everything done in this thread
366 : : * needs to be things that are safe to do in a signal handler. One thing
367 : : * that is not okay to do, is *malloc*.
368 : : */
369 : :
370 [ + - - + ]: 2 : if (!self || info->si_code != SI_TIMER)
371 : 2 : return;
372 : :
373 : 2 : uint32_t depth = self->stack.stackSize();
374 [ + - ]: 2 : if (depth == 0)
375 : 2 : return;
376 : :
377 : 0 : int64_t now = g_get_monotonic_time() * 1000L;
378 : :
379 : : /* NOTE: cppcheck warns that alloca() is not recommended since it can
380 : : * easily overflow the stack; however, dynamic allocation is not an option
381 : : * here since we are in a signal handler.
382 : : */
383 : : SysprofCaptureAddress* addrs =
384 : : // cppcheck-suppress allocaCalled
385 : 0 : static_cast<SysprofCaptureAddress*>(alloca(sizeof *addrs * depth));
386 : :
387 [ # # ]: 0 : for (uint32_t ix = 0; ix < depth; ix++) {
388 : 0 : js::ProfilingStackFrame& entry = self->stack.frames[ix];
389 : 0 : const char *label = entry.label();
390 : 0 : const char *dynamic_string = entry.dynamicString();
391 : 0 : uint32_t flipped = depth - 1 - ix;
392 : 0 : size_t label_length = strlen(label);
393 : :
394 : : /*
395 : : * 512 is an arbitrarily large size, very likely to be enough to
396 : : * hold the final string.
397 : : */
398 : : char final_string[512];
399 : 0 : char *position = final_string;
400 : 0 : size_t available_length = sizeof (final_string) - 1;
401 : :
402 [ # # ]: 0 : if (label_length > 0) {
403 [ # # ]: 0 : label_length = MIN(label_length, available_length);
404 : :
405 : : /* Start copying the label to the final string */
406 : 0 : memcpy(position, label, label_length);
407 : 0 : available_length -= label_length;
408 : 0 : position += label_length;
409 : :
410 : : /*
411 : : * Add a space in between the label and the dynamic string,
412 : : * if there is one.
413 : : */
414 [ # # # # ]: 0 : if (dynamic_string && available_length > 0) {
415 : 0 : *position++ = ' ';
416 : 0 : available_length--;
417 : : }
418 : : }
419 : :
420 : : /* Now append the dynamic string at the end of the final string.
421 : : * The string is cut in case it doesn't fit the remaining space.
422 : : */
423 [ # # ]: 0 : if (dynamic_string) {
424 : 0 : size_t dynamic_string_length = strlen(dynamic_string);
425 : :
426 [ # # ]: 0 : if (dynamic_string_length > 0) {
427 [ # # ]: 0 : size_t remaining_length = MIN(available_length, dynamic_string_length);
428 : 0 : memcpy(position, dynamic_string, remaining_length);
429 : 0 : position += remaining_length;
430 : : }
431 : : }
432 : :
433 : 0 : *position = 0;
434 : :
435 : : /*
436 : : * GeckoProfiler will put "js::RunScript" on the stack, but it has
437 : : * a stack address of "this", which is not terribly useful since
438 : : * everything will show up as [stack] when building callgraphs.
439 : : */
440 [ # # ]: 0 : if (final_string[0] != '\0')
441 : 0 : addrs[flipped] =
442 : 0 : sysprof_capture_writer_add_jitmap(self->capture, final_string);
443 : : else
444 : 0 : addrs[flipped] = SysprofCaptureAddress(entry.stackAddress());
445 : : }
446 : :
447 [ # # ]: 0 : if (!sysprof_capture_writer_add_sample(self->capture, now, -1, self->pid,
448 : : -1, addrs, depth)) {
449 : 0 : gjs_profiler_stop(self);
450 : 0 : return;
451 : : }
452 : :
453 : : unsigned ids[GJS_N_COUNTERS];
454 : : SysprofCaptureCounterValue values[GJS_N_COUNTERS];
455 : 0 : size_t new_counts = 0;
456 : :
457 : : # define FETCH_COUNTERS(name, ix) \
458 : : { \
459 : : uint64_t count = GJS_GET_COUNTER(name); \
460 : : if (count != self->last_counter_values[ix]) { \
461 : : ids[new_counts] = self->counter_base + ix; \
462 : : values[new_counts].v64 = count; \
463 : : new_counts++; \
464 : : } \
465 : : self->last_counter_values[ix] = count; \
466 : : }
467 [ # # # # : 0 : GJS_FOR_EACH_COUNTER(FETCH_COUNTERS);
# # # # #
# # # # #
# # # # #
# # # # #
# # # # #
# # # ]
468 : : # undef FETCH_COUNTERS
469 : :
470 [ # # # # ]: 0 : if (new_counts > 0 &&
471 [ # # ]: 0 : !sysprof_capture_writer_set_counters(self->capture, now, -1, self->pid,
472 : : ids, values, new_counts))
473 : 0 : gjs_profiler_stop(self);
474 : : }
475 : :
476 : 0 : static gboolean profiler_auto_flush_cb(void* user_data) {
477 : 0 : auto* self = static_cast<GjsProfiler*>(user_data);
478 : :
479 [ # # ]: 0 : if (!self->running)
480 : 0 : return G_SOURCE_REMOVE;
481 : :
482 : 0 : sysprof_capture_writer_flush(self->capture);
483 : :
484 : 0 : return G_SOURCE_CONTINUE;
485 : : }
486 : :
487 : : #endif /* ENABLE_PROFILER */
488 : :
489 : : /**
490 : : * gjs_profiler_start:
491 : : * @self: A #GjsProfiler
492 : : *
493 : : * As expected, this starts the GjsProfiler.
494 : : *
495 : : * This will enable the underlying JS profiler and register a POSIX timer to
496 : : * deliver SIGPROF on the configured sampling frequency.
497 : : *
498 : : * To reduce sampling overhead, #GjsProfiler stashes information about the
499 : : * profile to be calculated once the profiler has been disabled. Calling
500 : : * gjs_profiler_stop() will result in that delayed work to be completed.
501 : : *
502 : : * You should call gjs_profiler_stop() when the profiler is no longer needed.
503 : : */
504 : : void
505 : 5 : gjs_profiler_start(GjsProfiler *self)
506 : : {
507 : 5 : g_return_if_fail(self);
508 [ - + ]: 5 : if (self->running)
509 : 0 : return;
510 : :
511 : : #ifdef ENABLE_PROFILER
512 : :
513 : 5 : g_return_if_fail(!self->capture);
514 : :
515 : 5 : struct sigaction sa = {{0}};
516 : 5 : struct sigevent sev = {{0}};
517 : 5 : struct itimerspec its = {{0}};
518 : : struct itimerspec old_its;
519 : :
520 [ - + ]: 5 : if (self->target_capture) {
521 : 0 : self->capture = sysprof_capture_writer_ref(self->target_capture);
522 [ - + ]: 5 : } else if (self->fd != -1) {
523 : 0 : self->capture = sysprof_capture_writer_new_from_fd(self->fd, 0);
524 : 0 : self->fd = -1;
525 : : } else {
526 : 10 : GjsAutoChar path = g_strdup(self->filename);
527 [ + + ]: 5 : if (!path)
528 : 3 : path = g_strdup_printf("gjs-%jd.syscap", intmax_t(self->pid));
529 : :
530 : 5 : self->capture = sysprof_capture_writer_new(path, 0);
531 : 5 : }
532 : :
533 [ - + ]: 5 : if (!self->capture) {
534 : 0 : g_warning("Failed to open profile capture");
535 : 0 : return;
536 : : }
537 : :
538 : : /* Automatically flush to be resilient against SIGINT, etc */
539 [ + - ]: 5 : if (!self->periodic_flush) {
540 : 5 : self->periodic_flush =
541 : 5 : g_timeout_source_new_seconds(FLUSH_DELAY_SECONDS);
542 : 5 : g_source_set_name(self->periodic_flush,
543 : : "[sysprof-capture-writer-flush]");
544 : 5 : g_source_set_priority(self->periodic_flush, G_PRIORITY_LOW + 100);
545 : 5 : g_source_set_callback(self->periodic_flush,
546 : : (GSourceFunc)profiler_auto_flush_cb, self,
547 : : nullptr);
548 : 5 : g_source_attach(self->periodic_flush,
549 : : g_main_context_get_thread_default());
550 : : }
551 : :
552 [ - + ]: 5 : if (!gjs_profiler_extract_maps(self)) {
553 : 0 : g_warning("Failed to extract proc maps");
554 [ # # ]: 0 : g_clear_pointer(&self->capture, sysprof_capture_writer_unref);
555 [ # # ]: 0 : g_clear_pointer(&self->periodic_flush, g_source_destroy);
556 : 0 : return;
557 : : }
558 : :
559 [ - + ]: 5 : if (!gjs_profiler_define_counters(self)) {
560 : 0 : g_warning("Failed to define sysprof counters");
561 [ # # ]: 0 : g_clear_pointer(&self->capture, sysprof_capture_writer_unref);
562 [ # # ]: 0 : g_clear_pointer(&self->periodic_flush, g_source_destroy);
563 : 0 : return;
564 : : }
565 : :
566 : : /* Setup our signal handler for SIGPROF delivery */
567 : 5 : sa.sa_flags = SA_RESTART | SA_SIGINFO;
568 : 5 : sa.sa_sigaction = gjs_profiler_sigprof;
569 : 5 : sigemptyset(&sa.sa_mask);
570 : :
571 [ - + ]: 5 : if (sigaction(SIGPROF, &sa, nullptr) == -1) {
572 : 0 : g_warning("Failed to register sigaction handler: %s", g_strerror(errno));
573 [ # # ]: 0 : g_clear_pointer(&self->capture, sysprof_capture_writer_unref);
574 [ # # ]: 0 : g_clear_pointer(&self->periodic_flush, g_source_destroy);
575 : 0 : return;
576 : : }
577 : :
578 : : /*
579 : : * Create our SIGPROF timer
580 : : *
581 : : * We want to receive a SIGPROF signal on the JS thread using our
582 : : * configured sampling frequency. Instead of allowing any thread to be
583 : : * notified, we set the _tid value to ensure that only our thread gets
584 : : * delivery of the signal. This feature is generally just for
585 : : * threading implementations, but it works for us as well and ensures
586 : : * that the thread is blocked while we capture the stack.
587 : : */
588 : 5 : sev.sigev_notify = SIGEV_THREAD_ID;
589 : 5 : sev.sigev_signo = SIGPROF;
590 : 5 : sev._sigev_un._tid = syscall(__NR_gettid);
591 : :
592 [ - + ]: 5 : if (timer_create(CLOCK_MONOTONIC, &sev, &self->timer) == -1) {
593 : 0 : g_warning("Failed to create profiler timer: %s", g_strerror(errno));
594 [ # # ]: 0 : g_clear_pointer(&self->capture, sysprof_capture_writer_unref);
595 [ # # ]: 0 : g_clear_pointer(&self->periodic_flush, g_source_destroy);
596 : 0 : return;
597 : : }
598 : :
599 : : /* Calculate sampling interval */
600 : 5 : its.it_interval.tv_sec = 0;
601 : 5 : its.it_interval.tv_nsec = NSEC_PER_SEC / SAMPLES_PER_SEC;
602 : 5 : its.it_value.tv_sec = 0;
603 : 5 : its.it_value.tv_nsec = NSEC_PER_SEC / SAMPLES_PER_SEC;
604 : :
605 : : /* Now start this timer */
606 [ - + ]: 5 : if (timer_settime(self->timer, 0, &its, &old_its) != 0) {
607 : 0 : g_warning("Failed to enable profiler timer: %s", g_strerror(errno));
608 : 0 : timer_delete(self->timer);
609 [ # # ]: 0 : g_clear_pointer(&self->capture, sysprof_capture_writer_unref);
610 [ # # ]: 0 : g_clear_pointer(&self->periodic_flush, g_source_destroy);
611 : 0 : return;
612 : : }
613 : :
614 : 5 : self->running = true;
615 : :
616 : : /* Notify the JS runtime of where to put stack info */
617 : 5 : js::SetContextProfilingStack(self->cx, &self->stack);
618 : :
619 : : /* Start recording stack info */
620 : 5 : js::EnableContextProfilingStack(self->cx, true);
621 : :
622 : 5 : g_message("Profiler started");
623 : :
624 : : #else /* !ENABLE_PROFILER */
625 : :
626 : : self->running = true;
627 : : g_message("Profiler is disabled. Recompile with it enabled to use.");
628 : :
629 : : #endif /* ENABLE_PROFILER */
630 : : }
631 : :
632 : : /**
633 : : * gjs_profiler_stop:
634 : : * @self: A #GjsProfiler
635 : : *
636 : : * Stops a currently running #GjsProfiler. If the profiler is not running,
637 : : * this function will do nothing.
638 : : *
639 : : * Some work may be delayed until the end of the capture. Such delayed work
640 : : * includes flushing the resulting samples and file location information to
641 : : * disk.
642 : : *
643 : : * This may block while writing to disk. Generally, the writes are delivered
644 : : * to a tmpfs device, and are therefore negligible.
645 : : */
646 : : void
647 : 5 : gjs_profiler_stop(GjsProfiler *self)
648 : : {
649 : : /* Note: can be called from a signal handler */
650 : :
651 : 5 : g_assert(self);
652 : :
653 [ - + ]: 5 : if (!self->running)
654 : 0 : return;
655 : :
656 : : #ifdef ENABLE_PROFILER
657 : :
658 : 5 : struct itimerspec its = {{0}};
659 : 5 : timer_settime(self->timer, 0, &its, nullptr);
660 : 5 : timer_delete(self->timer);
661 : :
662 : 5 : js::EnableContextProfilingStack(self->cx, false);
663 : 5 : js::SetContextProfilingStack(self->cx, nullptr);
664 : :
665 : 5 : sysprof_capture_writer_flush(self->capture);
666 : :
667 [ + - ]: 5 : g_clear_pointer(&self->capture, sysprof_capture_writer_unref);
668 [ + - ]: 5 : g_clear_pointer(&self->periodic_flush, g_source_destroy);
669 : :
670 : 5 : g_message("Profiler stopped");
671 : :
672 : : #endif /* ENABLE_PROFILER */
673 : :
674 : 5 : self->running = false;
675 : : }
676 : :
677 : : #ifdef ENABLE_PROFILER
678 : :
679 : : static gboolean
680 : 0 : gjs_profiler_sigusr2(void *data)
681 : : {
682 : 0 : GjsContext* context = GJS_CONTEXT(data);
683 : 0 : GjsProfiler *current_profiler = gjs_context_get_profiler(context);
684 : :
685 [ # # ]: 0 : if (current_profiler) {
686 [ # # ]: 0 : if (_gjs_profiler_is_running(current_profiler))
687 : 0 : gjs_profiler_stop(current_profiler);
688 : : else
689 : 0 : gjs_profiler_start(current_profiler);
690 : : }
691 : :
692 : 0 : return G_SOURCE_CONTINUE;
693 : : }
694 : :
695 : : #endif /* ENABLE_PROFILER */
696 : :
697 : : /*
698 : : * _gjs_profiler_setup_signals:
699 : : * @context: a #GjsContext with a profiler attached
700 : : *
701 : : * If you want to simply allow profiling of your process with minimal
702 : : * fuss, simply call gjs_profiler_setup_signals(). This will allow
703 : : * enabling and disabling the profiler with SIGUSR2. You must call
704 : : * this from main() immediately when your program starts and must not
705 : : * block SIGUSR2 from your signal mask.
706 : : *
707 : : * If this is not sufficient, use gjs_profiler_chain_signal() from your
708 : : * own signal handler to pass the signal to a GjsProfiler.
709 : : */
710 : : void
711 : 0 : _gjs_profiler_setup_signals(GjsProfiler *self,
712 : : GjsContext *context)
713 : : {
714 : 0 : g_return_if_fail(context == profiling_context);
715 : :
716 : : #ifdef ENABLE_PROFILER
717 : :
718 [ # # ]: 0 : if (self->sigusr2_id != 0)
719 : 0 : return;
720 : :
721 : 0 : self->sigusr2_id = g_unix_signal_add(SIGUSR2, gjs_profiler_sigusr2, context);
722 : :
723 : : #else /* !ENABLE_PROFILER */
724 : :
725 : : g_message("Profiler is disabled. Not setting up signals.");
726 : : (void)self;
727 : :
728 : : #endif /* ENABLE_PROFILER */
729 : : }
730 : :
731 : : /**
732 : : * gjs_profiler_chain_signal:
733 : : * @context: a #GjsContext with a profiler attached
734 : : * @info: #siginfo_t passed in to signal handler
735 : : *
736 : : * Use this to pass a signal info caught by another signal handler to a
737 : : * GjsProfiler. This might be needed if you have your own complex signal
738 : : * handling system for which GjsProfiler cannot simply add a SIGUSR2 handler.
739 : : *
740 : : * This function should only be called from the JS thread.
741 : : *
742 : : * Returns: %TRUE if the signal was handled.
743 : : */
744 : : bool
745 : 0 : gjs_profiler_chain_signal(GjsContext *context,
746 : : siginfo_t *info)
747 : : {
748 : : #ifdef ENABLE_PROFILER
749 : :
750 [ # # ]: 0 : if (info) {
751 [ # # ]: 0 : if (info->si_signo == SIGPROF) {
752 : 0 : gjs_profiler_sigprof(SIGPROF, info, nullptr);
753 : 0 : return true;
754 : : }
755 : :
756 [ # # ]: 0 : if (info->si_signo == SIGUSR2) {
757 : 0 : gjs_profiler_sigusr2(context);
758 : 0 : return true;
759 : : }
760 : : }
761 : :
762 : : #else // !ENABLE_PROFILER
763 : :
764 : : (void)context;
765 : : (void)info;
766 : :
767 : : #endif /* ENABLE_PROFILER */
768 : :
769 : 0 : return false;
770 : : }
771 : :
772 : : /**
773 : : * gjs_profiler_set_capture_writer:
774 : : * @self: A #GjsProfiler
775 : : * @capture: (nullable): A #SysprofCaptureWriter
776 : : *
777 : : * Set the capture writer to which profiling data is written when the @self
778 : : * is stopped.
779 : : */
780 : 0 : void gjs_profiler_set_capture_writer(GjsProfiler* self, gpointer capture) {
781 : 0 : g_return_if_fail(self);
782 : 0 : g_return_if_fail(!self->running);
783 : :
784 : : #ifdef ENABLE_PROFILER
785 [ # # ]: 0 : g_clear_pointer(&self->target_capture, sysprof_capture_writer_unref);
786 : 0 : self->target_capture =
787 [ # # ]: 0 : capture ? sysprof_capture_writer_ref(
788 : : reinterpret_cast<SysprofCaptureWriter*>(capture))
789 : : : NULL;
790 : : #else
791 : : // Unused in the no-profiler case
792 : : (void)capture;
793 : : #endif
794 : : }
795 : :
796 : : /**
797 : : * gjs_profiler_set_filename:
798 : : * @self: A #GjsProfiler
799 : : * @filename: string containing a filename
800 : : *
801 : : * Set the file to which profiling data is written when the @self is stopped.
802 : : * By default, this is `gjs-$PID.syscap` in the current directory.
803 : : */
804 : : void
805 : 2 : gjs_profiler_set_filename(GjsProfiler *self,
806 : : const char *filename)
807 : : {
808 : 2 : g_return_if_fail(self);
809 : 2 : g_return_if_fail(!self->running);
810 : :
811 : 2 : g_free(self->filename);
812 : 2 : self->filename = g_strdup(filename);
813 : : }
814 : :
815 : 0 : void _gjs_profiler_add_mark(GjsProfiler* self, int64_t time_nsec,
816 : : int64_t duration_nsec, const char* group,
817 : : const char* name, const char* message) {
818 : 0 : g_return_if_fail(self);
819 : 0 : g_return_if_fail(group);
820 : 0 : g_return_if_fail(name);
821 : :
822 : : #ifdef ENABLE_PROFILER
823 [ # # # # ]: 0 : if (self->running && self->capture != nullptr) {
824 : 0 : sysprof_capture_writer_add_mark(self->capture, time_nsec, -1, self->pid,
825 : : duration_nsec, group, name, message);
826 : : }
827 : : #else
828 : : // Unused in the no-profiler case
829 : : (void)time_nsec;
830 : : (void)duration_nsec;
831 : : (void)message;
832 : : #endif
833 : : }
834 : :
835 : 0 : bool _gjs_profiler_sample_gc_memory_info(
836 : : GjsProfiler* self, int64_t gc_counters[Gjs::GCCounters::N_COUNTERS]) {
837 : 0 : g_return_val_if_fail(self, false);
838 : :
839 : : #ifdef ENABLE_PROFILER
840 [ # # # # ]: 0 : if (self->running && self->capture) {
841 : : unsigned ids[Gjs::GCCounters::N_COUNTERS];
842 : : SysprofCaptureCounterValue values[Gjs::GCCounters::N_COUNTERS];
843 : :
844 [ # # ]: 0 : for (size_t ix = 0; ix < Gjs::GCCounters::N_COUNTERS; ix++) {
845 : 0 : ids[ix] = self->gc_counter_base + ix;
846 : 0 : values[ix].v64 = gc_counters[ix];
847 : : }
848 : :
849 : 0 : int64_t now = g_get_monotonic_time() * 1000L;
850 [ # # ]: 0 : if (!sysprof_capture_writer_set_counters(self->capture, now, -1,
851 : : self->pid, ids, values,
852 : : Gjs::GCCounters::N_COUNTERS))
853 : 0 : return false;
854 : : }
855 : : #else
856 : : // Unused in the no-profiler case
857 : : (void)gc_counters;
858 : : #endif
859 : 0 : return true;
860 : : }
861 : :
862 : 0 : void gjs_profiler_set_fd(GjsProfiler* self, int fd) {
863 : 0 : g_return_if_fail(self);
864 : 0 : g_return_if_fail(!self->filename);
865 : 0 : g_return_if_fail(!self->running);
866 : :
867 : : #ifdef ENABLE_PROFILER
868 [ # # ]: 0 : if (self->fd != fd) {
869 [ # # ]: 0 : if (self->fd != -1)
870 : 0 : close(self->fd);
871 : 0 : self->fd = fd;
872 : : }
873 : : #else
874 : : (void)fd; // Unused in the no-profiler case
875 : : #endif
876 : : }
877 : :
878 : 0 : void _gjs_profiler_set_finalize_status(GjsProfiler* self,
879 : : JSFinalizeStatus status) {
880 : : #ifdef ENABLE_PROFILER
881 : : // Implementation note for mozjs-128:
882 : : //
883 : : // Sweeping happens in three phases:
884 : : // 1st phase (JSFINALIZE_GROUP_PREPARE): the collector prepares to sweep a
885 : : // group of zones. 2nd phase (JSFINALIZE_GROUP_START): weak references to
886 : : // unmarked things have been removed, but no GC thing has been swept. 3rd
887 : : // Phase (JSFINALIZE_GROUP_END): all dead GC things for a group of zones
888 : : // have been swept. The above repeats for each sweep group.
889 : : // JSFINALIZE_COLLECTION_END occurs at the end of all GC. (see jsgc.cpp,
890 : : // BeginSweepPhase/BeginSweepingZoneGroup and SweepPhase, all called from
891 : : // IncrementalCollectSlice).
892 : : //
893 : : // Incremental GC muddies the waters, because BeginSweepPhase is always run
894 : : // to entirety, but SweepPhase can be run incrementally and mixed with JS
895 : : // code runs or even native code, when MaybeGC/IncrementalGC return.
896 : : // After GROUP_START, the collector may yield to the mutator meaning JS code
897 : : // can run between the callback for GROUP_START and GROUP_END.
898 : :
899 : 0 : int64_t now = g_get_monotonic_time() * 1000L;
900 : :
901 [ # # # # : 0 : switch (status) {
# ]
902 : 0 : case JSFINALIZE_GROUP_PREPARE:
903 : 0 : self->sweep_begin_time = now;
904 : 0 : break;
905 : 0 : case JSFINALIZE_GROUP_START:
906 : 0 : self->group_sweep_begin_time = now;
907 : 0 : break;
908 : 0 : case JSFINALIZE_GROUP_END:
909 [ # # ]: 0 : if (self->group_sweep_begin_time != 0) {
910 : 0 : _gjs_profiler_add_mark(self, self->group_sweep_begin_time,
911 : 0 : now - self->group_sweep_begin_time,
912 : : "GJS", "Group sweep", nullptr);
913 : : }
914 : 0 : self->group_sweep_begin_time = 0;
915 : 0 : break;
916 : 0 : case JSFINALIZE_COLLECTION_END:
917 [ # # ]: 0 : if (self->sweep_begin_time != 0) {
918 : 0 : _gjs_profiler_add_mark(self, self->sweep_begin_time,
919 : 0 : now - self->sweep_begin_time, "GJS",
920 : : "Sweep", nullptr);
921 : : }
922 : 0 : self->sweep_begin_time = 0;
923 : 0 : break;
924 : 0 : default:
925 : : g_assert_not_reached();
926 : : }
927 : : #else
928 : : (void)self;
929 : : (void)status;
930 : : #endif
931 : 0 : }
932 : :
933 : 0 : void _gjs_profiler_set_gc_status(GjsProfiler* self, JSGCStatus status,
934 : : JS::GCReason reason) {
935 : : #ifdef ENABLE_PROFILER
936 : 0 : int64_t now = g_get_monotonic_time() * 1000L;
937 : :
938 [ # # # ]: 0 : switch (status) {
939 : 0 : case JSGC_BEGIN:
940 : 0 : self->gc_begin_time = now;
941 : 0 : self->gc_reason = gjs_explain_gc_reason(reason);
942 : 0 : break;
943 : 0 : case JSGC_END:
944 [ # # ]: 0 : if (self->gc_begin_time != 0) {
945 : 0 : _gjs_profiler_add_mark(self, self->gc_begin_time,
946 : 0 : now - self->gc_begin_time, "GJS",
947 : : "Garbage collection", self->gc_reason);
948 : : }
949 : 0 : self->gc_begin_time = 0;
950 : 0 : self->gc_reason = nullptr;
951 : 0 : break;
952 : 0 : default:
953 : : g_assert_not_reached();
954 : : }
955 : : #else
956 : : (void)self;
957 : : (void)status;
958 : : (void)reason;
959 : : #endif
960 : 0 : }
|