pf2 0.8.0 → 1.0.0.alpha1

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 +11 -0
  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 +394 -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 +111 -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,394 @@
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
+
32
+ VALUE
33
+ rb_pf2_session_initialize(int argc, VALUE *argv, VALUE self)
34
+ {
35
+ struct pf2_session *session;
36
+ TypedData_Get_Struct(self, struct pf2_session, &pf2_session_type, session);
37
+
38
+ // Create configuration from options hash
39
+ VALUE kwargs = Qnil;
40
+ rb_scan_args(argc, argv, ":", &kwargs);
41
+ ID kwarg_labels[] = {
42
+ rb_intern("interval_ms"),
43
+ rb_intern("time_mode")
44
+ };
45
+ VALUE *kwarg_values = NULL;
46
+ rb_get_kwargs(kwargs, kwarg_labels, 0, 2, kwarg_values);
47
+
48
+ session->configuration = pf2_configuration_new_from_options_hash(kwargs);
49
+
50
+ return self;
51
+ }
52
+
53
+ VALUE
54
+ rb_pf2_session_start(VALUE self)
55
+ {
56
+ struct pf2_session *session;
57
+ TypedData_Get_Struct(self, struct pf2_session, &pf2_session_type, session);
58
+
59
+ session->is_running = true;
60
+
61
+ // Record start time
62
+ clock_gettime(CLOCK_REALTIME, &session->start_time_realtime);
63
+ clock_gettime(CLOCK_MONOTONIC, &session->start_time);
64
+
65
+ // Spawn a collector thread which periodically wakes up and collects samples
66
+ if (pthread_create(session->collector_thread, NULL, sample_collector_thread, session) != 0) {
67
+ rb_raise(rb_eRuntimeError, "Failed to spawn sample collector thread");
68
+ }
69
+
70
+ // Install signal handler for SIGPROF
71
+ struct sigaction sa;
72
+ sa.sa_sigaction = sigprof_handler;
73
+ sigemptyset(&sa.sa_mask);
74
+ sigaddset(&sa.sa_mask, SIGPROF); // Mask SIGPROFs when handler is running
75
+ sa.sa_flags = SA_SIGINFO | SA_RESTART;
76
+ if (sigaction(SIGPROF, &sa, NULL) == -1) {
77
+ rb_raise(rb_eRuntimeError, "Failed to install SIGPROF handler");
78
+ }
79
+
80
+ #ifndef HAVE_TIMER_CREATE
81
+ // Install signal handler for SIGALRM if using wall time mode with setitimer
82
+ if (session->configuration->time_mode != PF2_TIME_MODE_CPU_TIME) {
83
+ sigaddset(&sa.sa_mask, SIGALRM);
84
+ if (sigaction(SIGALRM, &sa, NULL) == -1) {
85
+ rb_raise(rb_eRuntimeError, "Failed to install SIGALRM handler");
86
+ }
87
+ }
88
+ #endif
89
+
90
+ #ifdef HAVE_TIMER_CREATE
91
+ // Configure a kernel timer to send SIGPROF periodically
92
+ struct sigevent sev;
93
+ sev.sigev_notify = SIGEV_SIGNAL;
94
+ sev.sigev_signo = SIGPROF;
95
+ sev.sigev_value.sival_ptr = session; // Passed as info->si_value.sival_ptr
96
+ if (timer_create(
97
+ session->configuration->time_mode == PF2_TIME_MODE_CPU_TIME
98
+ ? CLOCK_PROCESS_CPUTIME_ID
99
+ : CLOCK_MONOTONIC,
100
+ &sev,
101
+ &session->timer
102
+ ) == -1) {
103
+ rb_raise(rb_eRuntimeError, "Failed to create timer");
104
+ }
105
+ struct itimerspec its = {
106
+ .it_value = {
107
+ .tv_sec = 0,
108
+ .tv_nsec = session->configuration->interval_ms * 1000000,
109
+ },
110
+ .it_interval = {
111
+ .tv_sec = 0,
112
+ .tv_nsec = session->configuration->interval_ms * 1000000,
113
+ },
114
+ };
115
+ if (timer_settime(session->timer, 0, &its, NULL) == -1) {
116
+ rb_raise(rb_eRuntimeError, "Failed to start timer");
117
+ }
118
+ #else
119
+ // Use setitimer as fallback
120
+ // Some platforms (e.g. macOS) do not have timer_create(3).
121
+ // setitimer(3) can be used as a alternative, but has limited functionality.
122
+ global_current_session = session;
123
+
124
+ struct itimerval itv = {
125
+ .it_value = {
126
+ .tv_sec = 0,
127
+ .tv_usec = session->configuration->interval_ms * 1000,
128
+ },
129
+ .it_interval = {
130
+ .tv_sec = 0,
131
+ .tv_usec = session->configuration->interval_ms * 1000,
132
+ },
133
+ };
134
+ int which_timer = session->configuration->time_mode == PF2_TIME_MODE_CPU_TIME
135
+ ? ITIMER_PROF // CPU time (sends SIGPROF)
136
+ : ITIMER_REAL; // Wall time (sends SIGALRM)
137
+
138
+ if (setitimer(which_timer, &itv, NULL) == -1) {
139
+ rb_raise(rb_eRuntimeError, "Failed to start timer");
140
+ }
141
+ #endif
142
+
143
+ return Qtrue;
144
+ }
145
+
146
+ static void *
147
+ sample_collector_thread(void *arg)
148
+ {
149
+ struct pf2_session *session = arg;
150
+
151
+ while (session->is_running == true) {
152
+ // Take samples from the ring buffer
153
+ struct pf2_sample sample;
154
+ while (pf2_ringbuffer_pop(session->rbuf, &sample) == true) {
155
+ // Ensure we have capacity before adding a new sample
156
+ if (!ensure_sample_capacity(session)) {
157
+ // Failed to expand buffer
158
+ PF2_DEBUG_LOG("Failed to expand sample buffer. Dropping sample\n");
159
+ break;
160
+ }
161
+
162
+ session->samples[session->samples_index++] = sample;
163
+ }
164
+
165
+ // Sleep for 100 ms
166
+ // TODO: Replace with high watermark callback
167
+ struct timespec ts = { .tv_sec = 0, .tv_nsec = 10 * 1000000, }; // 10 ms
168
+ nanosleep(&ts, NULL);
169
+ }
170
+
171
+ return NULL;
172
+ }
173
+
174
+ // async-signal-safe
175
+ static void
176
+ sigprof_handler(int sig, siginfo_t *info, void *ucontext)
177
+ {
178
+ #ifdef PF2_DEBUG
179
+ struct timespec sig_start_time;
180
+ clock_gettime(CLOCK_MONOTONIC, &sig_start_time);
181
+ #endif
182
+
183
+ struct pf2_session *session;
184
+ #ifdef HAVE_TIMER_CREATE
185
+ session = info->si_value.sival_ptr;
186
+ #else
187
+ session = global_current_session;
188
+ #endif
189
+
190
+ // If garbage collection is in progress, don't collect samples.
191
+ if (atomic_load_explicit(&session->is_marking, memory_order_acquire)) {
192
+ PF2_DEBUG_LOG("Dropping sample: Garbage collection is in progress\n");
193
+ return;
194
+ }
195
+
196
+ struct pf2_sample sample;
197
+
198
+ if (pf2_sample_capture(&sample) == false) {
199
+ PF2_DEBUG_LOG("Dropping sample: Failed to capture sample\n");
200
+ return;
201
+ }
202
+
203
+ // Copy the sample to the ringbuffer
204
+ if (pf2_ringbuffer_push(session->rbuf, &sample) == false) {
205
+ // Copy failed. The sample buffer is full.
206
+ PF2_DEBUG_LOG("Dropping sample: Sample buffer is full\n");
207
+ return;
208
+ }
209
+
210
+ #ifdef PF2_DEBUG
211
+ struct timespec sig_end_time;
212
+ clock_gettime(CLOCK_MONOTONIC, &sig_end_time);
213
+
214
+ // Calculate elapsed time in nanoseconds
215
+ sample.consumed_time_ns =
216
+ (sig_end_time.tv_sec - sig_start_time.tv_sec) * 1000000000L +
217
+ (sig_end_time.tv_nsec - sig_start_time.tv_nsec);
218
+
219
+ PF2_DEBUG_LOG("sigprof_handler: consumed_time_ns: %lu\n", sample.consumed_time_ns);
220
+ #endif
221
+ }
222
+
223
+ // Ensures that the session's sample array has capacity for at least one more sample
224
+ // Returns true if successful, false if memory allocation failed
225
+ bool
226
+ ensure_sample_capacity(struct pf2_session *session)
227
+ {
228
+ // Check if we need to expand
229
+ if (session->samples_index < session->samples_capacity) {
230
+ return true;
231
+ }
232
+
233
+ // Calculate new size (double the current size)
234
+ size_t new_capacity = session->samples_capacity * 2;
235
+
236
+ // Reallocate the array
237
+ struct pf2_sample *new_samples = realloc(session->samples, new_capacity * sizeof(struct pf2_sample));
238
+ if (new_samples == NULL) {
239
+ return false;
240
+ }
241
+
242
+ session->samples = new_samples;
243
+ session->samples_capacity = new_capacity;
244
+
245
+ return true;
246
+ }
247
+
248
+ VALUE
249
+ rb_pf2_session_stop(VALUE self)
250
+ {
251
+ struct pf2_session *session;
252
+ TypedData_Get_Struct(self, struct pf2_session, &pf2_session_type, session);
253
+
254
+ // Calculate duration
255
+ struct timespec end_time;
256
+ clock_gettime(CLOCK_MONOTONIC, &end_time);
257
+ uint64_t start_ns = (uint64_t)session->start_time.tv_sec * 1000000000ULL + (uint64_t)session->start_time.tv_nsec;
258
+ uint64_t end_ns = (uint64_t)end_time.tv_sec * 1000000000ULL + (uint64_t)end_time.tv_nsec;
259
+ session->duration_ns = end_ns - start_ns;
260
+
261
+ // Disarm and delete the timer.
262
+ #ifdef HAVE_TIMER_CREATE
263
+ if (timer_delete(session->timer) == -1) {
264
+ rb_raise(rb_eRuntimeError, "Failed to delete timer");
265
+ }
266
+ #else
267
+ struct itimerval zero_timer = {{0, 0}, {0, 0}};
268
+ int which_timer = session->configuration->time_mode == PF2_TIME_MODE_CPU_TIME
269
+ ? ITIMER_PROF
270
+ : ITIMER_REAL;
271
+ if (setitimer(which_timer, &zero_timer, NULL) == -1) {
272
+ rb_raise(rb_eRuntimeError, "Failed to stop timer");
273
+ }
274
+ global_current_session = NULL;
275
+ #endif
276
+
277
+ // Terminate the collector thread
278
+ session->is_running = false;
279
+ pthread_join(*session->collector_thread, NULL);
280
+
281
+ // Create serializer and serialize
282
+ struct pf2_ser *serializer = pf2_ser_new();
283
+ pf2_ser_prepare(serializer, session);
284
+ VALUE result = pf2_ser_to_ruby_hash(serializer);
285
+ pf2_ser_free(serializer);
286
+
287
+ return result;
288
+ }
289
+
290
+ VALUE
291
+ rb_pf2_session_configuration(VALUE self)
292
+ {
293
+ struct pf2_session *session;
294
+ TypedData_Get_Struct(self, struct pf2_session, &pf2_session_type, session);
295
+ return pf2_configuration_to_ruby_hash(session->configuration);
296
+ }
297
+
298
+ VALUE
299
+ pf2_session_alloc(VALUE self)
300
+ {
301
+ // Initialize state for libbacktrace
302
+ if (global_backtrace_state == NULL) {
303
+ global_backtrace_state = backtrace_create_state("pf2", 1, pf2_backtrace_print_error, NULL);
304
+ if (global_backtrace_state == NULL) {
305
+ rb_raise(rb_eRuntimeError, "Failed to initialize libbacktrace");
306
+ }
307
+ }
308
+
309
+ struct pf2_session *session = malloc(sizeof(struct pf2_session));
310
+ if (session == NULL) {
311
+ rb_raise(rb_eNoMemError, "Failed to allocate memory");
312
+ }
313
+
314
+ session->rbuf = pf2_ringbuffer_new(1000);
315
+ if (session->rbuf == NULL) {
316
+ rb_raise(rb_eNoMemError, "Failed to allocate memory");
317
+ }
318
+
319
+ atomic_store_explicit(&session->is_marking, false, memory_order_relaxed);
320
+ session->collector_thread = malloc(sizeof(pthread_t));
321
+ if (session->collector_thread == NULL) {
322
+ rb_raise(rb_eNoMemError, "Failed to allocate memory");
323
+ }
324
+
325
+ session->duration_ns = 0;
326
+
327
+ session->samples_index = 0;
328
+ session->samples_capacity = 500; // 10 seconds worth of samples at 50 Hz
329
+ session->samples = malloc(sizeof(struct pf2_sample) * session->samples_capacity);
330
+ if (session->samples == NULL) {
331
+ rb_raise(rb_eNoMemError, "Failed to allocate memory");
332
+ }
333
+
334
+ session->configuration = NULL;
335
+
336
+ return TypedData_Wrap_Struct(self, &pf2_session_type, session);
337
+ }
338
+
339
+ void
340
+ pf2_session_dmark(void *sess)
341
+ {
342
+ struct pf2_session *session = sess;
343
+
344
+ // Disallow sample collection during marking
345
+ atomic_store_explicit(&session->is_marking, true, memory_order_release);
346
+
347
+ // Iterate over all samples in the ringbuffer and mark them
348
+ struct pf2_ringbuffer *rbuf = session->rbuf;
349
+ struct pf2_sample *sample;
350
+ int head = atomic_load_explicit(&rbuf->head, memory_order_acquire);
351
+ int tail = atomic_load_explicit(&rbuf->tail, memory_order_acquire);
352
+ while (head != tail) {
353
+ sample = &rbuf->samples[head];
354
+ // TODO: Move this to mark function in pf2_sample
355
+ for (int i = 0; i < sample->depth; i++) {
356
+ rb_gc_mark(sample->cmes[i]);
357
+ }
358
+ head = (head + 1) % rbuf->size;
359
+ }
360
+
361
+ // Iterate over all samples in the samples array and mark them
362
+ for (size_t i = 0; i < session->samples_index; i++) {
363
+ sample = &session->samples[i];
364
+ for (int i = 0; i < sample->depth; i++) {
365
+ rb_gc_mark(sample->cmes[i]);
366
+ }
367
+ }
368
+
369
+ // Allow sample collection
370
+ atomic_store_explicit(&session->is_marking, false, memory_order_release);
371
+ }
372
+
373
+ void
374
+ pf2_session_dfree(void *sess)
375
+ {
376
+ // TODO: Ensure the uninstall process is complete before freeing the session
377
+ struct pf2_session *session = sess;
378
+ pf2_configuration_free(session->configuration);
379
+ pf2_ringbuffer_free(session->rbuf);
380
+ free(session->samples);
381
+ free(session->collector_thread);
382
+ free(session);
383
+ }
384
+
385
+ size_t
386
+ pf2_session_dsize(const void *sess)
387
+ {
388
+ const struct pf2_session *session = sess;
389
+ return (
390
+ sizeof(struct pf2_session)
391
+ + sizeof(struct pf2_sample) * session->samples_capacity
392
+ + sizeof(struct pf2_sample) * session->rbuf->size
393
+ );
394
+ }
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 = RUBY_TYPED_FREE_IMMEDIATELY,
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
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'json'
4
4
 
5
+ require_relative 'stack_weaver'
6
+
5
7
  module Pf2
6
8
  module Reporter
7
9
  # Generates Firefox Profiler's "processed profile format"
@@ -59,7 +61,7 @@ module Pf2
59
61
  counters: [],
60
62
  threads: thread_reports,
61
63
  }
62
- FirefoxProfiler.deep_camelize_keys(report)
64
+ FirefoxProfilerSer2.deep_camelize_keys(report)
63
65
  end
64
66
 
65
67
  class ThreadReport
@@ -72,6 +74,7 @@ module Pf2
72
74
  @stack_tree = { :stack_id => nil }
73
75
  @reverse_stack_tree = []
74
76
  @string_table = { nil => 0 }
77
+ @stack_weaver = StackWeaver.new(@profile)
75
78
  end
76
79
 
77
80
  def inspect
@@ -79,9 +82,6 @@ module Pf2
79
82
  end
80
83
 
81
84
  def emit
82
- # TODO: weave?
83
- # @thread[:stack_tree] = x
84
-
85
85
  # Build func table from profile[:functions]
86
86
  func_table = build_func_table
87
87
  # Build frame table from profile[:locations]
@@ -136,8 +136,10 @@ module Pf2
136
136
  }
137
137
 
138
138
  @samples.each do |sample|
139
- stack = sample[:stack].reverse
140
- stack_id = @stack_tree.dig(*stack, :stack_id)
139
+ # Weave Ruby stack and native stack
140
+ woven_stack = @stack_weaver.weave(sample[:stack], sample[:native_stack] || [])
141
+ woven_stack.reverse! # StackWeaver returns in root->leaf order, we need leaf->root
142
+ stack_id = @stack_tree.dig(*woven_stack, :stack_id)
141
143
 
142
144
  ret[:stack] << stack_id
143
145
  ret[:time] << sample[:elapsed_ns] / 1_000_000 # ns -> ms
@@ -165,9 +167,11 @@ module Pf2
165
167
  }
166
168
 
167
169
  @profile[:locations].each.with_index do |location, i|
170
+ function = @profile[:functions][location[:function_index]]
171
+
168
172
  ret[:address] << location[:address]
169
- ret[:category] << 1
170
- ret[:subcategory] << 1
173
+ ret[:category] << (function[:implementation] == :native ? 3 : 2)
174
+ ret[:subcategory] << nil
171
175
  ret[:func] << location[:function_index]
172
176
  ret[:inner_window_id] << nil
173
177
  ret[:implementation] << nil
@@ -218,15 +222,15 @@ module Pf2
218
222
  }
219
223
 
220
224
  @profile[:samples].each do |sample|
221
- # Stack (Array of location indices) recorded in sample, reversed
222
- # example: [1, 2, 9] (1 is the root)
223
- stack = sample[:stack].reverse
225
+ # Weave Ruby stack and native stack
226
+ woven_stack = @stack_weaver.weave(sample[:stack], sample[:native_stack] || [])
227
+ woven_stack.reverse! # StackWeaver returns in root->leaf order, we need leaf->root
224
228
 
225
229
  # Build the stack_table Array which Firefox Profiler requires.
226
230
  # At the same time, build the stack tree for efficient traversal.
227
231
 
228
232
  current_node = @stack_tree # the stack tree root
229
- stack.each do |location_index|
233
+ woven_stack.each do |location_index|
230
234
  if current_node[location_index].nil?
231
235
  # The tree node is unknown. Create it.
232
236
  new_stack_id = ret[:frame].length # The position of the new stack in the stack_table array
@@ -238,7 +242,7 @@ module Pf2
238
242
 
239
243
  # Register the stack in the stack_table Array
240
244
  ret[:frame] << location_index
241
- ret[:category] << (function[:implementation] == :ruby ? 2 : 1)
245
+ ret[:category] << (function[:implementation] == :native ? 3 : 2)
242
246
  ret[:subcategory] << nil
243
247
  ret[:prefix] << current_node[:stack_id] # the parent's position in the stack_table array
244
248
  end