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