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