pf2 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/README.md +20 -4
  4. data/Rakefile +1 -0
  5. data/doc/development.md +17 -0
  6. data/examples/mandelbrot.rb +69 -0
  7. data/examples/mandelbrot_ractor.rb +77 -0
  8. data/ext/pf2/backtrace_state.c +10 -0
  9. data/ext/pf2/backtrace_state.h +10 -0
  10. data/ext/pf2/configuration.c +90 -0
  11. data/ext/pf2/configuration.h +23 -0
  12. data/ext/pf2/debug.h +12 -0
  13. data/ext/pf2/extconf.rb +23 -6
  14. data/ext/pf2/pf2.c +17 -0
  15. data/ext/pf2/pf2.h +8 -0
  16. data/ext/pf2/ringbuffer.c +74 -0
  17. data/ext/pf2/ringbuffer.h +24 -0
  18. data/ext/pf2/sample.c +76 -0
  19. data/ext/pf2/sample.h +26 -0
  20. data/ext/pf2/serializer.c +377 -0
  21. data/ext/pf2/serializer.h +58 -0
  22. data/ext/pf2/session.c +408 -0
  23. data/ext/pf2/session.h +56 -0
  24. data/lib/pf2/cli.rb +25 -11
  25. data/lib/pf2/reporter/annotate.rb +101 -0
  26. data/lib/pf2/reporter/firefox_profiler_ser2.rb +17 -13
  27. data/lib/pf2/reporter/stack_weaver.rb +8 -0
  28. data/lib/pf2/reporter.rb +1 -1
  29. data/lib/pf2/version.rb +1 -1
  30. data/lib/pf2.rb +1 -1
  31. data/vendor/libbacktrace/.gitignore +5 -0
  32. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/README.md +1 -1
  33. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/configure +23 -0
  34. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/configure.ac +10 -0
  35. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/dwarf.c +199 -15
  36. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/elf.c +20 -14
  37. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/fileline.c +2 -2
  38. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/macho.c +2 -2
  39. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/pecoff.c +2 -2
  40. metadata +125 -111
  41. data/Cargo.lock +0 -630
  42. data/Cargo.toml +0 -3
  43. data/crates/backtrace-sys2/.gitignore +0 -1
  44. data/crates/backtrace-sys2/Cargo.toml +0 -9
  45. data/crates/backtrace-sys2/build.rs +0 -45
  46. data/crates/backtrace-sys2/src/lib.rs +0 -5
  47. data/crates/backtrace-sys2/src/libbacktrace/.gitignore +0 -15
  48. data/ext/pf2/Cargo.toml +0 -25
  49. data/ext/pf2/build.rs +0 -10
  50. data/ext/pf2/src/backtrace.rs +0 -127
  51. data/ext/pf2/src/lib.rs +0 -22
  52. data/ext/pf2/src/profile.rs +0 -69
  53. data/ext/pf2/src/profile_serializer.rs +0 -241
  54. data/ext/pf2/src/ringbuffer.rs +0 -150
  55. data/ext/pf2/src/ruby_c_api_helper.c +0 -6
  56. data/ext/pf2/src/ruby_init.rs +0 -40
  57. data/ext/pf2/src/ruby_internal_apis.rs +0 -77
  58. data/ext/pf2/src/sample.rs +0 -67
  59. data/ext/pf2/src/scheduler.rs +0 -10
  60. data/ext/pf2/src/serialization/profile.rs +0 -48
  61. data/ext/pf2/src/serialization/serializer.rs +0 -329
  62. data/ext/pf2/src/serialization.rs +0 -2
  63. data/ext/pf2/src/session/configuration.rs +0 -114
  64. data/ext/pf2/src/session/new_thread_watcher.rs +0 -80
  65. data/ext/pf2/src/session/ruby_object.rs +0 -90
  66. data/ext/pf2/src/session.rs +0 -248
  67. data/ext/pf2/src/siginfo_t.c +0 -5
  68. data/ext/pf2/src/signal_scheduler.rs +0 -201
  69. data/ext/pf2/src/signal_scheduler_unsupported_platform.rs +0 -39
  70. data/ext/pf2/src/timer_thread_scheduler.rs +0 -179
  71. data/ext/pf2/src/util.rs +0 -31
  72. data/lib/pf2/reporter/firefox_profiler.rb +0 -397
  73. data/rust-toolchain.toml +0 -2
  74. data/rustfmt.toml +0 -1
  75. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/Isaac.Newton-Opticks.txt +0 -0
  76. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/LICENSE +0 -0
  77. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/Makefile.am +0 -0
  78. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/Makefile.in +0 -0
  79. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/aclocal.m4 +0 -0
  80. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/alloc.c +0 -0
  81. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/allocfail.c +0 -0
  82. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/allocfail.sh +0 -0
  83. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/atomic.c +0 -0
  84. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/backtrace-supported.h.in +0 -0
  85. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/backtrace.c +0 -0
  86. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/backtrace.h +0 -0
  87. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/btest.c +0 -0
  88. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/compile +0 -0
  89. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/enable.m4 +0 -0
  90. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/lead-dot.m4 +0 -0
  91. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/libtool.m4 +0 -0
  92. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/ltoptions.m4 +0 -0
  93. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/ltsugar.m4 +0 -0
  94. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/ltversion.m4 +0 -0
  95. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/lt~obsolete.m4 +0 -0
  96. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/multi.m4 +0 -0
  97. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/override.m4 +0 -0
  98. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/unwind_ipinfo.m4 +0 -0
  99. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/warnings.m4 +0 -0
  100. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config.guess +0 -0
  101. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config.h.in +0 -0
  102. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config.sub +0 -0
  103. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/edtest.c +0 -0
  104. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/edtest2.c +0 -0
  105. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/filenames.h +0 -0
  106. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/filetype.awk +0 -0
  107. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/install-debuginfo-for-buildid.sh.in +0 -0
  108. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/install-sh +0 -0
  109. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/instrumented_alloc.c +0 -0
  110. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/internal.h +0 -0
  111. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/ltmain.sh +0 -0
  112. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/missing +0 -0
  113. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/mmap.c +0 -0
  114. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/mmapio.c +0 -0
  115. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/move-if-change +0 -0
  116. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/mtest.c +0 -0
  117. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/nounwind.c +0 -0
  118. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/posix.c +0 -0
  119. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/print.c +0 -0
  120. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/read.c +0 -0
  121. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/simple.c +0 -0
  122. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/sort.c +0 -0
  123. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/state.c +0 -0
  124. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/stest.c +0 -0
  125. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/test-driver +0 -0
  126. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/test_format.c +0 -0
  127. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/testlib.c +0 -0
  128. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/testlib.h +0 -0
  129. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/ttest.c +0 -0
  130. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/unittest.c +0 -0
  131. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/unknown.c +0 -0
  132. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/xcoff.c +0 -0
  133. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/xztest.c +0 -0
  134. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/zstdtest.c +0 -0
  135. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/ztest.c +0 -0
data/ext/pf2/session.c ADDED
@@ -0,0 +1,408 @@
1
+ #include <bits/time.h>
2
+ #include <pthread.h>
3
+ #include <signal.h>
4
+ #include <stdatomic.h>
5
+ #include <stdbool.h>
6
+ #include <stdio.h>
7
+ #include <stdlib.h>
8
+ #include <sys/time.h>
9
+ #include <time.h>
10
+
11
+ #include <ruby.h>
12
+ #include <ruby/debug.h>
13
+
14
+ #include <backtrace.h>
15
+
16
+ #include "backtrace_state.h"
17
+ #include "configuration.h"
18
+ #include "debug.h"
19
+ #include "sample.h"
20
+ #include "session.h"
21
+ #include "serializer.h"
22
+
23
+ #ifndef HAVE_TIMER_CREATE
24
+ // Global session pointer for setitimer fallback
25
+ static struct pf2_session *global_current_session = NULL;
26
+ #endif
27
+
28
+ static void *sample_collector_thread(void *arg);
29
+ static void sigprof_handler(int sig, siginfo_t *info, void *ucontext);
30
+ bool ensure_sample_capacity(struct pf2_session *session);
31
+ static void pf2_session_stop(struct pf2_session *session);
32
+
33
+ VALUE
34
+ rb_pf2_session_initialize(int argc, VALUE *argv, VALUE self)
35
+ {
36
+ struct pf2_session *session;
37
+ TypedData_Get_Struct(self, struct pf2_session, &pf2_session_type, session);
38
+
39
+ // Create configuration from options hash
40
+ VALUE kwargs = Qnil;
41
+ rb_scan_args(argc, argv, ":", &kwargs);
42
+ ID kwarg_labels[] = {
43
+ rb_intern("interval_ms"),
44
+ rb_intern("time_mode")
45
+ };
46
+ VALUE *kwarg_values = NULL;
47
+ rb_get_kwargs(kwargs, kwarg_labels, 0, 2, kwarg_values);
48
+
49
+ session->configuration = pf2_configuration_new_from_options_hash(kwargs);
50
+
51
+ return self;
52
+ }
53
+
54
+ VALUE
55
+ rb_pf2_session_start(VALUE self)
56
+ {
57
+ struct pf2_session *session;
58
+ TypedData_Get_Struct(self, struct pf2_session, &pf2_session_type, session);
59
+
60
+ session->is_running = true;
61
+
62
+ // Record start time
63
+ clock_gettime(CLOCK_REALTIME, &session->start_time_realtime);
64
+ clock_gettime(CLOCK_MONOTONIC, &session->start_time);
65
+
66
+ // Spawn a collector thread which periodically wakes up and collects samples
67
+ if (pthread_create(session->collector_thread, NULL, sample_collector_thread, session) != 0) {
68
+ rb_raise(rb_eRuntimeError, "Failed to spawn sample collector thread");
69
+ }
70
+
71
+ // Install signal handler for SIGPROF
72
+ struct sigaction sa;
73
+ sa.sa_sigaction = sigprof_handler;
74
+ sigemptyset(&sa.sa_mask);
75
+ sigaddset(&sa.sa_mask, SIGPROF); // Mask SIGPROFs when handler is running
76
+ sa.sa_flags = SA_SIGINFO | SA_RESTART;
77
+ if (sigaction(SIGPROF, &sa, NULL) == -1) {
78
+ rb_raise(rb_eRuntimeError, "Failed to install SIGPROF handler");
79
+ }
80
+
81
+ #ifndef HAVE_TIMER_CREATE
82
+ // Install signal handler for SIGALRM if using wall time mode with setitimer
83
+ if (session->configuration->time_mode != PF2_TIME_MODE_CPU_TIME) {
84
+ sigaddset(&sa.sa_mask, SIGALRM);
85
+ if (sigaction(SIGALRM, &sa, NULL) == -1) {
86
+ rb_raise(rb_eRuntimeError, "Failed to install SIGALRM handler");
87
+ }
88
+ }
89
+ #endif
90
+
91
+ #ifdef HAVE_TIMER_CREATE
92
+ // Configure a kernel timer to send SIGPROF periodically
93
+ struct sigevent sev;
94
+ sev.sigev_notify = SIGEV_SIGNAL;
95
+ sev.sigev_signo = SIGPROF;
96
+ sev.sigev_value.sival_ptr = session; // Passed as info->si_value.sival_ptr
97
+ if (timer_create(
98
+ session->configuration->time_mode == PF2_TIME_MODE_CPU_TIME
99
+ ? CLOCK_PROCESS_CPUTIME_ID
100
+ : CLOCK_MONOTONIC,
101
+ &sev,
102
+ &session->timer
103
+ ) == -1) {
104
+ rb_raise(rb_eRuntimeError, "Failed to create timer");
105
+ }
106
+ struct itimerspec its = {
107
+ .it_value = {
108
+ .tv_sec = 0,
109
+ .tv_nsec = session->configuration->interval_ms * 1000000,
110
+ },
111
+ .it_interval = {
112
+ .tv_sec = 0,
113
+ .tv_nsec = session->configuration->interval_ms * 1000000,
114
+ },
115
+ };
116
+ if (timer_settime(session->timer, 0, &its, NULL) == -1) {
117
+ rb_raise(rb_eRuntimeError, "Failed to start timer");
118
+ }
119
+ #else
120
+ // Use setitimer as fallback
121
+ // Some platforms (e.g. macOS) do not have timer_create(3).
122
+ // setitimer(3) can be used as a alternative, but has limited functionality.
123
+ global_current_session = session;
124
+
125
+ struct itimerval itv = {
126
+ .it_value = {
127
+ .tv_sec = 0,
128
+ .tv_usec = session->configuration->interval_ms * 1000,
129
+ },
130
+ .it_interval = {
131
+ .tv_sec = 0,
132
+ .tv_usec = session->configuration->interval_ms * 1000,
133
+ },
134
+ };
135
+ int which_timer = session->configuration->time_mode == PF2_TIME_MODE_CPU_TIME
136
+ ? ITIMER_PROF // CPU time (sends SIGPROF)
137
+ : ITIMER_REAL; // Wall time (sends SIGALRM)
138
+
139
+ if (setitimer(which_timer, &itv, NULL) == -1) {
140
+ rb_raise(rb_eRuntimeError, "Failed to start timer");
141
+ }
142
+ #endif
143
+
144
+ return Qtrue;
145
+ }
146
+
147
+ static void *
148
+ sample_collector_thread(void *arg)
149
+ {
150
+ struct pf2_session *session = arg;
151
+
152
+ while (session->is_running == true) {
153
+ // Take samples from the ring buffer
154
+ struct pf2_sample sample;
155
+ while (pf2_ringbuffer_pop(session->rbuf, &sample) == true) {
156
+ // Ensure we have capacity before adding a new sample
157
+ if (!ensure_sample_capacity(session)) {
158
+ // Failed to expand buffer
159
+ PF2_DEBUG_LOG("Failed to expand sample buffer. Dropping sample\n");
160
+ break;
161
+ }
162
+
163
+ session->samples[session->samples_index++] = sample;
164
+ }
165
+
166
+ // Sleep for 100 ms
167
+ // TODO: Replace with high watermark callback
168
+ struct timespec ts = { .tv_sec = 0, .tv_nsec = 10 * 1000000, }; // 10 ms
169
+ nanosleep(&ts, NULL);
170
+ }
171
+
172
+ return NULL;
173
+ }
174
+
175
+ // async-signal-safe
176
+ static void
177
+ sigprof_handler(int sig, siginfo_t *info, void *ucontext)
178
+ {
179
+ #ifdef PF2_DEBUG
180
+ struct timespec sig_start_time;
181
+ clock_gettime(CLOCK_MONOTONIC, &sig_start_time);
182
+ #endif
183
+
184
+ struct pf2_session *session;
185
+ #ifdef HAVE_TIMER_CREATE
186
+ session = info->si_value.sival_ptr;
187
+ #else
188
+ session = global_current_session;
189
+ #endif
190
+
191
+ // If garbage collection is in progress, don't collect samples.
192
+ if (atomic_load_explicit(&session->is_marking, memory_order_acquire)) {
193
+ PF2_DEBUG_LOG("Dropping sample: Garbage collection is in progress\n");
194
+ return;
195
+ }
196
+
197
+ struct pf2_sample sample;
198
+
199
+ if (pf2_sample_capture(&sample) == false) {
200
+ PF2_DEBUG_LOG("Dropping sample: Failed to capture sample\n");
201
+ return;
202
+ }
203
+
204
+ // Copy the sample to the ringbuffer
205
+ if (pf2_ringbuffer_push(session->rbuf, &sample) == false) {
206
+ // Copy failed. The sample buffer is full.
207
+ PF2_DEBUG_LOG("Dropping sample: Sample buffer is full\n");
208
+ return;
209
+ }
210
+
211
+ #ifdef PF2_DEBUG
212
+ struct timespec sig_end_time;
213
+ clock_gettime(CLOCK_MONOTONIC, &sig_end_time);
214
+
215
+ // Calculate elapsed time in nanoseconds
216
+ sample.consumed_time_ns =
217
+ (sig_end_time.tv_sec - sig_start_time.tv_sec) * 1000000000L +
218
+ (sig_end_time.tv_nsec - sig_start_time.tv_nsec);
219
+
220
+ PF2_DEBUG_LOG("sigprof_handler: consumed_time_ns: %lu\n", sample.consumed_time_ns);
221
+ #endif
222
+ }
223
+
224
+ // Ensures that the session's sample array has capacity for at least one more sample
225
+ // Returns true if successful, false if memory allocation failed
226
+ bool
227
+ ensure_sample_capacity(struct pf2_session *session)
228
+ {
229
+ // Check if we need to expand
230
+ if (session->samples_index < session->samples_capacity) {
231
+ return true;
232
+ }
233
+
234
+ // Calculate new size (double the current size)
235
+ size_t new_capacity = session->samples_capacity * 2;
236
+
237
+ // Reallocate the array
238
+ struct pf2_sample *new_samples = realloc(session->samples, new_capacity * sizeof(struct pf2_sample));
239
+ if (new_samples == NULL) {
240
+ return false;
241
+ }
242
+
243
+ session->samples = new_samples;
244
+ session->samples_capacity = new_capacity;
245
+
246
+ return true;
247
+ }
248
+
249
+ VALUE
250
+ rb_pf2_session_stop(VALUE self)
251
+ {
252
+ struct pf2_session *session;
253
+ TypedData_Get_Struct(self, struct pf2_session, &pf2_session_type, session);
254
+
255
+ pf2_session_stop(session);
256
+
257
+ // Create serializer and serialize
258
+ struct pf2_ser *serializer = pf2_ser_new();
259
+ pf2_ser_prepare(serializer, session);
260
+ VALUE result = pf2_ser_to_ruby_hash(serializer);
261
+ pf2_ser_free(serializer);
262
+
263
+ return result;
264
+ }
265
+
266
+ static void
267
+ pf2_session_stop(struct pf2_session *session)
268
+ {
269
+ // Calculate duration
270
+ struct timespec end_time;
271
+ clock_gettime(CLOCK_MONOTONIC, &end_time);
272
+ uint64_t start_ns = (uint64_t)session->start_time.tv_sec * 1000000000ULL + (uint64_t)session->start_time.tv_nsec;
273
+ uint64_t end_ns = (uint64_t)end_time.tv_sec * 1000000000ULL + (uint64_t)end_time.tv_nsec;
274
+ session->duration_ns = end_ns - start_ns;
275
+
276
+ // Disarm and delete the timer.
277
+ #ifdef HAVE_TIMER_CREATE
278
+ if (timer_delete(session->timer) == -1) {
279
+ rb_raise(rb_eRuntimeError, "Failed to delete timer");
280
+ }
281
+ #else
282
+ struct itimerval zero_timer = {{0, 0}, {0, 0}};
283
+ int which_timer = session->configuration->time_mode == PF2_TIME_MODE_CPU_TIME
284
+ ? ITIMER_PROF
285
+ : ITIMER_REAL;
286
+ if (setitimer(which_timer, &zero_timer, NULL) == -1) {
287
+ rb_raise(rb_eRuntimeError, "Failed to stop timer");
288
+ }
289
+ global_current_session = NULL;
290
+ #endif
291
+
292
+ // Terminate the collector thread
293
+ session->is_running = false;
294
+ pthread_join(*session->collector_thread, NULL);
295
+ }
296
+
297
+ VALUE
298
+ rb_pf2_session_configuration(VALUE self)
299
+ {
300
+ struct pf2_session *session;
301
+ TypedData_Get_Struct(self, struct pf2_session, &pf2_session_type, session);
302
+ return pf2_configuration_to_ruby_hash(session->configuration);
303
+ }
304
+
305
+ VALUE
306
+ pf2_session_alloc(VALUE self)
307
+ {
308
+ // Initialize state for libbacktrace
309
+ if (global_backtrace_state == NULL) {
310
+ global_backtrace_state = backtrace_create_state("pf2", 1, pf2_backtrace_print_error, NULL);
311
+ if (global_backtrace_state == NULL) {
312
+ rb_raise(rb_eRuntimeError, "Failed to initialize libbacktrace");
313
+ }
314
+ }
315
+
316
+ struct pf2_session *session = malloc(sizeof(struct pf2_session));
317
+ if (session == NULL) {
318
+ rb_raise(rb_eNoMemError, "Failed to allocate memory");
319
+ }
320
+
321
+ session->rbuf = pf2_ringbuffer_new(1000);
322
+ if (session->rbuf == NULL) {
323
+ rb_raise(rb_eNoMemError, "Failed to allocate memory");
324
+ }
325
+
326
+ atomic_store_explicit(&session->is_marking, false, memory_order_relaxed);
327
+ session->collector_thread = malloc(sizeof(pthread_t));
328
+ if (session->collector_thread == NULL) {
329
+ rb_raise(rb_eNoMemError, "Failed to allocate memory");
330
+ }
331
+
332
+ session->duration_ns = 0;
333
+
334
+ session->samples_index = 0;
335
+ session->samples_capacity = 500; // 10 seconds worth of samples at 50 Hz
336
+ session->samples = malloc(sizeof(struct pf2_sample) * session->samples_capacity);
337
+ if (session->samples == NULL) {
338
+ rb_raise(rb_eNoMemError, "Failed to allocate memory");
339
+ }
340
+
341
+ session->configuration = NULL;
342
+
343
+ return TypedData_Wrap_Struct(self, &pf2_session_type, session);
344
+ }
345
+
346
+ void
347
+ pf2_session_dmark(void *sess)
348
+ {
349
+ struct pf2_session *session = sess;
350
+
351
+ // Disallow sample collection during marking
352
+ atomic_store_explicit(&session->is_marking, true, memory_order_release);
353
+
354
+ // Iterate over all samples in the ringbuffer and mark them
355
+ struct pf2_ringbuffer *rbuf = session->rbuf;
356
+ struct pf2_sample *sample;
357
+ int head = atomic_load_explicit(&rbuf->head, memory_order_acquire);
358
+ int tail = atomic_load_explicit(&rbuf->tail, memory_order_acquire);
359
+ while (head != tail) {
360
+ sample = &rbuf->samples[head];
361
+ // TODO: Move this to mark function in pf2_sample
362
+ for (int i = 0; i < sample->depth; i++) {
363
+ rb_gc_mark(sample->cmes[i]);
364
+ }
365
+ head = (head + 1) % rbuf->size;
366
+ }
367
+
368
+ // Iterate over all samples in the samples array and mark them
369
+ for (size_t i = 0; i < session->samples_index; i++) {
370
+ sample = &session->samples[i];
371
+ for (int i = 0; i < sample->depth; i++) {
372
+ rb_gc_mark(sample->cmes[i]);
373
+ }
374
+ }
375
+
376
+ // Allow sample collection
377
+ atomic_store_explicit(&session->is_marking, false, memory_order_release);
378
+ }
379
+
380
+ void
381
+ pf2_session_dfree(void *sess)
382
+ {
383
+ struct pf2_session *session = sess;
384
+
385
+ assert(session->is_running == false || session->is_running == true);
386
+
387
+ // Stop the session if it's still running
388
+ if (session->is_running) {
389
+ pf2_session_stop(session);
390
+ }
391
+
392
+ pf2_configuration_free(session->configuration);
393
+ pf2_ringbuffer_free(session->rbuf);
394
+ free(session->samples);
395
+ free(session->collector_thread);
396
+ free(session);
397
+ }
398
+
399
+ size_t
400
+ pf2_session_dsize(const void *sess)
401
+ {
402
+ const struct pf2_session *session = sess;
403
+ return (
404
+ sizeof(struct pf2_session)
405
+ + sizeof(struct pf2_sample) * session->samples_capacity
406
+ + sizeof(struct pf2_sample) * session->rbuf->size
407
+ );
408
+ }
data/ext/pf2/session.h ADDED
@@ -0,0 +1,56 @@
1
+ #ifndef PF2_SESSION_H
2
+ #define PF2_SESSION_H
3
+
4
+ #include <pthread.h>
5
+ #include <stdatomic.h>
6
+ #include <sys/time.h>
7
+
8
+ #include <ruby.h>
9
+
10
+ #include "configuration.h"
11
+ #include "ringbuffer.h"
12
+ #include "sample.h"
13
+
14
+ struct pf2_session {
15
+ bool is_running;
16
+ #ifdef HAVE_TIMER_CREATE
17
+ timer_t timer;
18
+ #else
19
+ struct itimerval timer;
20
+ #endif
21
+ struct pf2_ringbuffer *rbuf;
22
+ atomic_bool is_marking; // Whether garbage collection is in progress
23
+ pthread_t *collector_thread;
24
+
25
+ struct pf2_sample *samples; // Dynamic array of samples
26
+ size_t samples_index;
27
+ size_t samples_capacity; // Current capacity of the samples array
28
+
29
+ struct timespec start_time_realtime;
30
+ struct timespec start_time; // When profiling started
31
+ uint64_t duration_ns; // Duration of profiling in nanoseconds
32
+
33
+ struct pf2_configuration *configuration;
34
+ };
35
+
36
+ VALUE rb_pf2_session_initialize(int argc, VALUE *argv, VALUE self);
37
+ VALUE rb_pf2_session_start(VALUE self);
38
+ VALUE rb_pf2_session_stop(VALUE self);
39
+ VALUE rb_pf2_session_configuration(VALUE self);
40
+ VALUE pf2_session_alloc(VALUE self);
41
+ void pf2_session_dmark(void *sess);
42
+ void pf2_session_dfree(void *sess);
43
+ size_t pf2_session_dsize(const void *sess);
44
+
45
+ static const rb_data_type_t pf2_session_type = {
46
+ .wrap_struct_name = "Pf2c::Session",
47
+ .function = {
48
+ .dmark = pf2_session_dmark,
49
+ .dfree = pf2_session_dfree,
50
+ .dsize = pf2_session_dsize,
51
+ },
52
+ .data = NULL,
53
+ .flags = 0,
54
+ };
55
+
56
+ #endif // PF2_SESSION_H
data/lib/pf2/cli.rb CHANGED
@@ -19,6 +19,8 @@ module Pf2
19
19
  case subcommand
20
20
  when 'report'
21
21
  subcommand_report(argv)
22
+ when 'annotate'
23
+ subcommand_annotate(argv)
22
24
  when 'serve'
23
25
  subcommand_serve(argv)
24
26
  when 'version'
@@ -53,20 +55,12 @@ module Pf2
53
55
  opts.on('-o', '--output FILE', 'Output file') do |path|
54
56
  options[:output_file] = path
55
57
  end
56
- opts.on('--experimental-serializer', 'Enable the experimental serializer mode') do
57
- options[:experimental_serializer] = true
58
- end
59
58
  end
60
59
  option_parser.parse!(argv)
61
60
 
62
- if options[:experimental_serializer]
63
- profile = Marshal.load(File.read(argv[0]))
64
- report = Pf2::Reporter::FirefoxProfilerSer2.new(profile).emit
65
- report = JSON.generate(report)
66
- else
67
- profile = JSON.parse(File.read(argv[0]), symbolize_names: true, max_nesting: false)
68
- report = JSON.generate(Pf2::Reporter::FirefoxProfiler.new(profile).emit)
69
- end
61
+ profile = Marshal.load(File.read(argv[0]))
62
+ report = Pf2::Reporter::FirefoxProfilerSer2.new(profile).emit
63
+ report = JSON.generate(report)
70
64
 
71
65
  if options[:output_file]
72
66
  File.write(options[:output_file], report)
@@ -77,6 +71,26 @@ module Pf2
77
71
  return 0
78
72
  end
79
73
 
74
+ def subcommand_annotate(argv)
75
+ options = {}
76
+ option_parser = OptionParser.new do |opts|
77
+ opts.banner = "Usage: pf2 report [options] COMMAND"
78
+ opts.on('-h', '--help', 'Prints this help') do
79
+ puts opts
80
+ return 0
81
+ end
82
+ opts.on('-d', '--source-directory DIR', 'Path to the source directory') do |dir|
83
+ options[:source_directory] = dir
84
+ end
85
+ end
86
+ option_parser.parse!(argv)
87
+
88
+ profile = Marshal.load(File.binread(argv[0]))
89
+ Pf2::Reporter::Annotate.new(profile, options[:source_directory] || '.').annotate
90
+
91
+ return 0
92
+ end
93
+
80
94
  def subcommand_serve(argv)
81
95
  options = {}
82
96
  option_parser = OptionParser.new do |opts|
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pf2
4
+ module Reporter
5
+ class Annotate
6
+ HitCount = Struct.new(:self, :total, keyword_init: false)
7
+ SourceCodeHits = Struct.new(:path, :line_count, keyword_init: false)
8
+
9
+ # @param profile [Hash]
10
+ # @param source_directory [String]
11
+ def initialize(profile, source_directory)
12
+ @profile = profile
13
+ @source_directory = source_directory
14
+ end
15
+
16
+ def annotate
17
+ tallied = tally_by_source_code_line(@profile)
18
+
19
+ # Print the source code with hit counts
20
+ tallied.each do |path, source_code_hits|
21
+ expanded_path = File.expand_path(path, @source_directory)
22
+ if !File.exist?(expanded_path)
23
+ if ignorable_path?(path)
24
+ puts "Ignoring file: #{path}"
25
+ else
26
+ puts "File not found: #{path}"
27
+ end
28
+ puts ""
29
+ puts ""
30
+ next
31
+ end
32
+ source_file = File.open(expanded_path, "r")
33
+
34
+ puts expanded_path
35
+ puts ""
36
+
37
+ # Print in tabular format
38
+
39
+ # Header row
40
+ puts " ttl self │"
41
+
42
+ source_file.each_line.with_index(1) do |line, lineno|
43
+ hits = source_code_hits.line_count[lineno]
44
+
45
+ if !hits.nil?
46
+ # If any samples are captured for this line
47
+ puts "%5d %5d │ %s" % [hits.total, hits.self, line.chomp]
48
+ else
49
+ puts "%5s %5s │ %s" % ["", "", line.chomp]
50
+ end
51
+ end
52
+
53
+ puts ""
54
+ puts ""
55
+ ensure
56
+ source_file.close if source_file
57
+ end
58
+ end
59
+
60
+ # @return [Array<SourceCodeHits>]
61
+ private def tally_by_source_code_line(profile)
62
+ # Iterate over all samples and tally self hits and total hits by location
63
+ hits_per_location = {}
64
+ @profile[:samples].each do |sample|
65
+ # Record a total hit for all locations in the stack
66
+ sample[:stack].each do |location_id|
67
+ hits_per_location[location_id] ||= HitCount.new(0, 0)
68
+ hits_per_location[location_id].total += 1
69
+ end
70
+
71
+ # Record a self hit for the topmost stack frame, which is the first element in the array
72
+ topmost_location_id = sample[:stack][0]
73
+ hits_per_location[topmost_location_id].self += 1
74
+ end
75
+
76
+ # Associate a filename and lineno for each location
77
+ hits_per_file = {}
78
+ hits_per_location.each do |location_id, hits|
79
+ location = @profile[:locations][location_id]
80
+ function = @profile[:functions][location[:function_index]]
81
+
82
+ filename = function[:filename]
83
+ # Some locations simply cannot be associated to a specific file.
84
+ # We just ignore them.
85
+ next if filename.nil?
86
+ lineno = location[:lineno]
87
+
88
+ hits_per_file[filename] ||= SourceCodeHits.new(filename, {})
89
+ hits_per_file[filename].line_count[lineno] = hits
90
+ end
91
+
92
+ hits_per_file
93
+ end
94
+
95
+ private def ignorable_path?(path)
96
+ return true if path.start_with?("<internal:")
97
+ false
98
+ end
99
+ end
100
+ end
101
+ end