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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +20 -4
- data/Rakefile +1 -0
- data/doc/development.md +17 -0
- data/examples/mandelbrot.rb +69 -0
- data/examples/mandelbrot_ractor.rb +77 -0
- data/ext/pf2/backtrace_state.c +10 -0
- data/ext/pf2/backtrace_state.h +10 -0
- data/ext/pf2/configuration.c +90 -0
- data/ext/pf2/configuration.h +23 -0
- data/ext/pf2/debug.h +12 -0
- data/ext/pf2/extconf.rb +23 -6
- data/ext/pf2/pf2.c +17 -0
- data/ext/pf2/pf2.h +8 -0
- data/ext/pf2/ringbuffer.c +74 -0
- data/ext/pf2/ringbuffer.h +24 -0
- data/ext/pf2/sample.c +76 -0
- data/ext/pf2/sample.h +26 -0
- data/ext/pf2/serializer.c +377 -0
- data/ext/pf2/serializer.h +58 -0
- data/ext/pf2/session.c +394 -0
- data/ext/pf2/session.h +56 -0
- data/lib/pf2/cli.rb +25 -11
- data/lib/pf2/reporter/annotate.rb +101 -0
- data/lib/pf2/reporter/firefox_profiler_ser2.rb +17 -13
- data/lib/pf2/reporter/stack_weaver.rb +8 -0
- data/lib/pf2/reporter.rb +1 -1
- data/lib/pf2/version.rb +1 -1
- data/lib/pf2.rb +1 -1
- data/vendor/libbacktrace/.gitignore +5 -0
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/README.md +1 -1
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/configure +23 -0
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/configure.ac +10 -0
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/dwarf.c +199 -15
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/elf.c +20 -14
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/fileline.c +2 -2
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/macho.c +2 -2
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/pecoff.c +2 -2
- metadata +111 -111
- data/Cargo.lock +0 -630
- data/Cargo.toml +0 -3
- data/crates/backtrace-sys2/.gitignore +0 -1
- data/crates/backtrace-sys2/Cargo.toml +0 -9
- data/crates/backtrace-sys2/build.rs +0 -45
- data/crates/backtrace-sys2/src/lib.rs +0 -5
- data/crates/backtrace-sys2/src/libbacktrace/.gitignore +0 -15
- data/ext/pf2/Cargo.toml +0 -25
- data/ext/pf2/build.rs +0 -10
- data/ext/pf2/src/backtrace.rs +0 -127
- data/ext/pf2/src/lib.rs +0 -22
- data/ext/pf2/src/profile.rs +0 -69
- data/ext/pf2/src/profile_serializer.rs +0 -241
- data/ext/pf2/src/ringbuffer.rs +0 -150
- data/ext/pf2/src/ruby_c_api_helper.c +0 -6
- data/ext/pf2/src/ruby_init.rs +0 -40
- data/ext/pf2/src/ruby_internal_apis.rs +0 -77
- data/ext/pf2/src/sample.rs +0 -67
- data/ext/pf2/src/scheduler.rs +0 -10
- data/ext/pf2/src/serialization/profile.rs +0 -48
- data/ext/pf2/src/serialization/serializer.rs +0 -329
- data/ext/pf2/src/serialization.rs +0 -2
- data/ext/pf2/src/session/configuration.rs +0 -114
- data/ext/pf2/src/session/new_thread_watcher.rs +0 -80
- data/ext/pf2/src/session/ruby_object.rs +0 -90
- data/ext/pf2/src/session.rs +0 -248
- data/ext/pf2/src/siginfo_t.c +0 -5
- data/ext/pf2/src/signal_scheduler.rs +0 -201
- data/ext/pf2/src/signal_scheduler_unsupported_platform.rs +0 -39
- data/ext/pf2/src/timer_thread_scheduler.rs +0 -179
- data/ext/pf2/src/util.rs +0 -31
- data/lib/pf2/reporter/firefox_profiler.rb +0 -397
- data/rust-toolchain.toml +0 -2
- data/rustfmt.toml +0 -1
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/Isaac.Newton-Opticks.txt +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/LICENSE +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/Makefile.am +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/Makefile.in +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/aclocal.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/alloc.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/allocfail.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/allocfail.sh +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/atomic.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/backtrace-supported.h.in +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/backtrace.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/backtrace.h +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/btest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/compile +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/enable.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/lead-dot.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/libtool.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/ltoptions.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/ltsugar.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/ltversion.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/lt~obsolete.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/multi.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/override.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/unwind_ipinfo.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/warnings.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config.guess +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config.h.in +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config.sub +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/edtest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/edtest2.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/filenames.h +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/filetype.awk +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/install-debuginfo-for-buildid.sh.in +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/install-sh +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/instrumented_alloc.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/internal.h +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/ltmain.sh +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/missing +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/mmap.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/mmapio.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/move-if-change +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/mtest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/nounwind.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/posix.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/print.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/read.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/simple.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/sort.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/state.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/stest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/test-driver +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/test_format.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/testlib.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/testlib.h +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/ttest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/unittest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/unknown.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/xcoff.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/xztest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/zstdtest.c +0 -0
- /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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
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
|
140
|
-
|
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] <<
|
170
|
-
ret[:subcategory] <<
|
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
|
-
#
|
222
|
-
|
223
|
-
|
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
|
-
|
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] == :
|
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
|