pf2 0.7.1 → 0.9.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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/README.md +11 -0
  4. data/Rakefile +9 -2
  5. data/doc/development.md +11 -0
  6. data/examples/mandelbrot.rb +69 -0
  7. data/examples/mandelbrot_ractor.rb +77 -0
  8. data/ext/pf2/build.rs +7 -0
  9. data/ext/pf2/src/ruby_c_api_helper.c +6 -0
  10. data/ext/pf2/src/serialization/profile.rs +1 -0
  11. data/ext/pf2/src/serialization/serializer.rs +4 -0
  12. data/ext/pf2/src/signal_scheduler.rs +1 -1
  13. data/ext/pf2/src/util.rs +2 -1
  14. data/ext/pf2c/backtrace_state.c +10 -0
  15. data/ext/pf2c/backtrace_state.h +10 -0
  16. data/ext/pf2c/configuration.c +90 -0
  17. data/ext/pf2c/configuration.h +23 -0
  18. data/ext/pf2c/extconf.rb +21 -0
  19. data/ext/pf2c/pf2.c +17 -0
  20. data/ext/pf2c/pf2.h +8 -0
  21. data/ext/pf2c/ringbuffer.c +74 -0
  22. data/ext/pf2c/ringbuffer.h +24 -0
  23. data/ext/pf2c/sample.c +70 -0
  24. data/ext/pf2c/sample.h +22 -0
  25. data/ext/pf2c/serializer.c +377 -0
  26. data/ext/pf2c/serializer.h +58 -0
  27. data/ext/pf2c/session.c +344 -0
  28. data/ext/pf2c/session.h +51 -0
  29. data/lib/pf2/cli.rb +33 -2
  30. data/lib/pf2/reporter/annotate.rb +101 -0
  31. data/lib/pf2/reporter/firefox_profiler.rb +1 -1
  32. data/lib/pf2/reporter/firefox_profiler_ser2.rb +308 -0
  33. data/lib/pf2/reporter.rb +2 -0
  34. data/lib/pf2/version.rb +1 -1
  35. data/vendor/libbacktrace/.gitignore +5 -0
  36. data/vendor/libbacktrace/Isaac.Newton-Opticks.txt +9286 -0
  37. data/vendor/libbacktrace/LICENSE +29 -0
  38. data/vendor/libbacktrace/Makefile.am +708 -0
  39. data/vendor/libbacktrace/Makefile.in +2820 -0
  40. data/vendor/libbacktrace/README.md +46 -0
  41. data/vendor/libbacktrace/aclocal.m4 +864 -0
  42. data/vendor/libbacktrace/alloc.c +167 -0
  43. data/vendor/libbacktrace/allocfail.c +136 -0
  44. data/vendor/libbacktrace/allocfail.sh +104 -0
  45. data/vendor/libbacktrace/atomic.c +113 -0
  46. data/vendor/libbacktrace/backtrace-supported.h.in +66 -0
  47. data/vendor/libbacktrace/backtrace.c +129 -0
  48. data/vendor/libbacktrace/backtrace.h +189 -0
  49. data/vendor/libbacktrace/btest.c +517 -0
  50. data/vendor/libbacktrace/compile +348 -0
  51. data/vendor/libbacktrace/config/enable.m4 +38 -0
  52. data/vendor/libbacktrace/config/lead-dot.m4 +31 -0
  53. data/vendor/libbacktrace/config/libtool.m4 +7545 -0
  54. data/vendor/libbacktrace/config/ltoptions.m4 +369 -0
  55. data/vendor/libbacktrace/config/ltsugar.m4 +123 -0
  56. data/vendor/libbacktrace/config/ltversion.m4 +23 -0
  57. data/vendor/libbacktrace/config/lt~obsolete.m4 +98 -0
  58. data/vendor/libbacktrace/config/multi.m4 +68 -0
  59. data/vendor/libbacktrace/config/override.m4 +117 -0
  60. data/vendor/libbacktrace/config/unwind_ipinfo.m4 +37 -0
  61. data/vendor/libbacktrace/config/warnings.m4 +227 -0
  62. data/vendor/libbacktrace/config.guess +1700 -0
  63. data/vendor/libbacktrace/config.h.in +185 -0
  64. data/vendor/libbacktrace/config.sub +1885 -0
  65. data/vendor/libbacktrace/configure +15952 -0
  66. data/vendor/libbacktrace/configure.ac +642 -0
  67. data/vendor/libbacktrace/dwarf.c +4593 -0
  68. data/vendor/libbacktrace/edtest.c +120 -0
  69. data/vendor/libbacktrace/edtest2.c +43 -0
  70. data/vendor/libbacktrace/elf.c +7471 -0
  71. data/vendor/libbacktrace/fileline.c +407 -0
  72. data/vendor/libbacktrace/filenames.h +52 -0
  73. data/vendor/libbacktrace/filetype.awk +13 -0
  74. data/vendor/libbacktrace/install-debuginfo-for-buildid.sh.in +65 -0
  75. data/vendor/libbacktrace/install-sh +501 -0
  76. data/vendor/libbacktrace/instrumented_alloc.c +114 -0
  77. data/vendor/libbacktrace/internal.h +428 -0
  78. data/vendor/libbacktrace/ltmain.sh +8636 -0
  79. data/vendor/libbacktrace/macho.c +1361 -0
  80. data/vendor/libbacktrace/missing +215 -0
  81. data/vendor/libbacktrace/mmap.c +331 -0
  82. data/vendor/libbacktrace/mmapio.c +110 -0
  83. data/vendor/libbacktrace/move-if-change +83 -0
  84. data/vendor/libbacktrace/mtest.c +410 -0
  85. data/vendor/libbacktrace/nounwind.c +66 -0
  86. data/vendor/libbacktrace/pecoff.c +1123 -0
  87. data/vendor/libbacktrace/posix.c +104 -0
  88. data/vendor/libbacktrace/print.c +117 -0
  89. data/vendor/libbacktrace/read.c +110 -0
  90. data/vendor/libbacktrace/simple.c +108 -0
  91. data/vendor/libbacktrace/sort.c +108 -0
  92. data/vendor/libbacktrace/state.c +72 -0
  93. data/vendor/libbacktrace/stest.c +137 -0
  94. data/vendor/libbacktrace/test-driver +148 -0
  95. data/vendor/libbacktrace/test_format.c +55 -0
  96. data/vendor/libbacktrace/testlib.c +234 -0
  97. data/vendor/libbacktrace/testlib.h +110 -0
  98. data/vendor/libbacktrace/ttest.c +161 -0
  99. data/vendor/libbacktrace/unittest.c +92 -0
  100. data/vendor/libbacktrace/unknown.c +65 -0
  101. data/vendor/libbacktrace/xcoff.c +1617 -0
  102. data/vendor/libbacktrace/xztest.c +508 -0
  103. data/vendor/libbacktrace/zstdtest.c +523 -0
  104. data/vendor/libbacktrace/ztest.c +541 -0
  105. metadata +122 -3
@@ -0,0 +1,344 @@
1
+ #include <bits/time.h>
2
+ #include <stdatomic.h>
3
+ #include <stdbool.h>
4
+ #include <stdio.h>
5
+ #include <stdlib.h>
6
+ #include <signal.h>
7
+ #include <time.h>
8
+ #include <pthread.h>
9
+
10
+ #include <ruby.h>
11
+ #include <ruby/debug.h>
12
+
13
+ #include <backtrace.h>
14
+
15
+ #include "backtrace_state.h"
16
+ #include "configuration.h"
17
+ #include "sample.h"
18
+ #include "session.h"
19
+ #include "serializer.h"
20
+
21
+ static void *sample_collector_thread(void *arg);
22
+ static void sigprof_handler(int sig, siginfo_t *info, void *ucontext);
23
+ bool ensure_sample_capacity(struct pf2_session *session);
24
+
25
+ VALUE
26
+ rb_pf2_session_initialize(int argc, VALUE *argv, VALUE self)
27
+ {
28
+ struct pf2_session *session;
29
+ TypedData_Get_Struct(self, struct pf2_session, &pf2_session_type, session);
30
+
31
+ // Create configuration from options hash
32
+ VALUE kwargs = Qnil;
33
+ rb_scan_args(argc, argv, ":", &kwargs);
34
+ ID kwarg_labels[] = {
35
+ rb_intern("interval_ms"),
36
+ rb_intern("time_mode")
37
+ };
38
+ VALUE *kwarg_values = NULL;
39
+ rb_get_kwargs(kwargs, kwarg_labels, 0, 2, kwarg_values);
40
+
41
+ session->configuration = pf2_configuration_new_from_options_hash(kwargs);
42
+
43
+ return self;
44
+ }
45
+
46
+ VALUE
47
+ rb_pf2_session_start(VALUE self)
48
+ {
49
+ struct pf2_session *session;
50
+ TypedData_Get_Struct(self, struct pf2_session, &pf2_session_type, session);
51
+
52
+ session->is_running = true;
53
+
54
+ // Record start time
55
+ clock_gettime(CLOCK_REALTIME, &session->start_time_realtime);
56
+ clock_gettime(CLOCK_MONOTONIC, &session->start_time);
57
+
58
+ // Spawn a collector thread which periodically wakes up and collects samples
59
+ if (pthread_create(session->collector_thread, NULL, sample_collector_thread, session) != 0) {
60
+ rb_raise(rb_eRuntimeError, "Failed to spawn sample collector thread");
61
+ }
62
+
63
+ // Configure signal handler
64
+ struct sigaction sa;
65
+ sa.sa_sigaction = sigprof_handler;
66
+ sigemptyset(&sa.sa_mask);
67
+ sigaddset(&sa.sa_mask, SIGPROF); // Mask SIGPROFs when handler is running
68
+ sa.sa_flags = SA_SIGINFO | SA_RESTART;
69
+ if (sigaction(SIGPROF, &sa, NULL) == -1) {
70
+ rb_raise(rb_eRuntimeError, "Failed to install signal handler");
71
+ }
72
+
73
+ // Configure a timer to send SIGPROF every 10 ms of CPU time
74
+ struct sigevent sev;
75
+ sev.sigev_notify = SIGEV_SIGNAL;
76
+ sev.sigev_signo = SIGPROF;
77
+ sev.sigev_value.sival_ptr = session; // Passed as info->si_value.sival_ptr
78
+ if (timer_create(
79
+ session->configuration->time_mode == PF2_TIME_MODE_CPU_TIME
80
+ ? CLOCK_PROCESS_CPUTIME_ID
81
+ : CLOCK_MONOTONIC,
82
+ &sev,
83
+ &session->timer
84
+ ) == -1) {
85
+ rb_raise(rb_eRuntimeError, "Failed to create timer");
86
+ }
87
+ struct itimerspec its = {
88
+ .it_value = {
89
+ .tv_sec = 0,
90
+ .tv_nsec = session->configuration->interval_ms * 1000000,
91
+ },
92
+ .it_interval = {
93
+ .tv_sec = 0,
94
+ .tv_nsec = session->configuration->interval_ms * 1000000,
95
+ },
96
+ };
97
+ if (timer_settime(session->timer, 0, &its, NULL) == -1) {
98
+ rb_raise(rb_eRuntimeError, "Failed to start timer");
99
+ }
100
+
101
+ return Qtrue;
102
+ }
103
+
104
+ static void *
105
+ sample_collector_thread(void *arg)
106
+ {
107
+ struct pf2_session *session = arg;
108
+
109
+ while (session->is_running == true) {
110
+ // Take samples from the ring buffer
111
+ struct pf2_sample sample;
112
+ while (pf2_ringbuffer_pop(session->rbuf, &sample) == true) {
113
+ // Ensure we have capacity before adding a new sample
114
+ if (!ensure_sample_capacity(session)) {
115
+ // Failed to expand buffer
116
+ #ifdef PF2_DEBUG
117
+ printf("Failed to expand sample buffer. Dropping sample\n");
118
+ #endif
119
+ break;
120
+ }
121
+
122
+ session->samples[session->samples_index++] = sample;
123
+ }
124
+
125
+ // Sleep for 100 ms
126
+ // TODO: Replace with high watermark callback
127
+ struct timespec ts = { .tv_sec = 0, .tv_nsec = 10 * 1000000, }; // 10 ms
128
+ nanosleep(&ts, NULL);
129
+ }
130
+
131
+ return NULL;
132
+ }
133
+
134
+ // async-signal-safe
135
+ static void
136
+ sigprof_handler(int sig, siginfo_t *info, void *ucontext)
137
+ {
138
+ #ifdef PF2_DEBUG
139
+ struct timespec sig_start_time;
140
+ clock_gettime(CLOCK_MONOTONIC, &sig_start_time);
141
+ #endif
142
+
143
+ struct pf2_session *session = info->si_value.sival_ptr;
144
+
145
+ // If garbage collection is in progress, don't collect samples.
146
+ if (atomic_load_explicit(&session->is_marking, memory_order_acquire)) {
147
+ #ifdef PF2_DEBUG
148
+ printf("Dropping sample: Garbage collection is in progress\n");
149
+ #endif
150
+ return;
151
+ }
152
+
153
+ struct pf2_sample sample = { 0 };
154
+
155
+ if (pf2_sample_capture(&sample) == false) {
156
+ #ifdef PF2_DEBUG
157
+ printf("Dropping sample: Failed to capture sample\n");
158
+ #endif
159
+ return;
160
+ }
161
+
162
+ // Copy the sample to the ringbuffer
163
+ if (pf2_ringbuffer_push(session->rbuf, &sample) == false) {
164
+ // Copy failed. The sample buffer is full.
165
+ #ifdef PF2_DEBUG
166
+ printf("Dropping sample: Sample buffer is full\n");
167
+ #endif
168
+ return;
169
+ }
170
+
171
+ #ifdef PF2_DEBUG
172
+ struct timespec sig_end_time;
173
+ clock_gettime(CLOCK_MONOTONIC, &sig_end_time);
174
+
175
+ // Calculate elapsed time in nanoseconds
176
+ sample.consumed_time_ns =
177
+ (sig_end_time.tv_sec - sig_start_time.tv_sec) * 1000000000L +
178
+ (sig_end_time.tv_nsec - sig_start_time.tv_nsec);
179
+
180
+ printf("sigprof_handler: consumed_time_ns: %lu\n", sample.consumed_time_ns);
181
+ #endif
182
+ }
183
+
184
+ // Ensures that the session's sample array has capacity for at least one more sample
185
+ // Returns true if successful, false if memory allocation failed
186
+ bool
187
+ ensure_sample_capacity(struct pf2_session *session)
188
+ {
189
+ // Check if we need to expand
190
+ if (session->samples_index < session->samples_capacity) {
191
+ return true;
192
+ }
193
+
194
+ // Calculate new size (double the current size)
195
+ size_t new_capacity = session->samples_capacity * 2;
196
+
197
+ // Reallocate the array
198
+ struct pf2_sample *new_samples = realloc(session->samples, new_capacity * sizeof(struct pf2_sample));
199
+ if (new_samples == NULL) {
200
+ return false;
201
+ }
202
+
203
+ session->samples = new_samples;
204
+ session->samples_capacity = new_capacity;
205
+
206
+ return true;
207
+ }
208
+
209
+ VALUE
210
+ rb_pf2_session_stop(VALUE self)
211
+ {
212
+ struct pf2_session *session;
213
+ TypedData_Get_Struct(self, struct pf2_session, &pf2_session_type, session);
214
+
215
+ // Calculate duration
216
+ struct timespec end_time;
217
+ clock_gettime(CLOCK_MONOTONIC, &end_time);
218
+ uint64_t start_ns = (uint64_t)session->start_time.tv_sec * 1000000000ULL + (uint64_t)session->start_time.tv_nsec;
219
+ uint64_t end_ns = (uint64_t)end_time.tv_sec * 1000000000ULL + (uint64_t)end_time.tv_nsec;
220
+ session->duration_ns = end_ns - start_ns;
221
+
222
+ // Disarm and delete the timer.
223
+ if (timer_delete(session->timer) == -1) {
224
+ rb_raise(rb_eRuntimeError, "Failed to delete timer");
225
+ }
226
+
227
+ // Terminate the collector thread
228
+ session->is_running = false;
229
+ pthread_join(*session->collector_thread, NULL);
230
+
231
+ // Create serializer and serialize
232
+ struct pf2_ser *serializer = pf2_ser_new();
233
+ pf2_ser_prepare(serializer, session);
234
+ VALUE result = pf2_ser_to_ruby_hash(serializer);
235
+ pf2_ser_free(serializer);
236
+
237
+ return result;
238
+ }
239
+
240
+ VALUE
241
+ rb_pf2_session_configuration(VALUE self)
242
+ {
243
+ struct pf2_session *session;
244
+ TypedData_Get_Struct(self, struct pf2_session, &pf2_session_type, session);
245
+ return pf2_configuration_to_ruby_hash(session->configuration);
246
+ }
247
+
248
+ VALUE
249
+ pf2_session_alloc(VALUE self)
250
+ {
251
+ // Initialize state for libbacktrace
252
+ if (global_backtrace_state == NULL) {
253
+ global_backtrace_state = backtrace_create_state("pf2", 1, pf2_backtrace_print_error, NULL);
254
+ if (global_backtrace_state == NULL) {
255
+ rb_raise(rb_eRuntimeError, "Failed to initialize libbacktrace");
256
+ }
257
+ }
258
+
259
+ struct pf2_session *session = malloc(sizeof(struct pf2_session));
260
+ if (session == NULL) {
261
+ rb_raise(rb_eNoMemError, "Failed to allocate memory");
262
+ }
263
+
264
+ session->rbuf = pf2_ringbuffer_new(1000);
265
+ if (session->rbuf == NULL) {
266
+ rb_raise(rb_eNoMemError, "Failed to allocate memory");
267
+ }
268
+
269
+ atomic_store_explicit(&session->is_marking, false, memory_order_relaxed);
270
+ session->collector_thread = malloc(sizeof(pthread_t));
271
+ if (session->collector_thread == NULL) {
272
+ rb_raise(rb_eNoMemError, "Failed to allocate memory");
273
+ }
274
+
275
+ session->duration_ns = 0;
276
+
277
+ session->samples_index = 0;
278
+ session->samples_capacity = 500; // 10 seconds worth of samples at 50 Hz
279
+ session->samples = malloc(sizeof(struct pf2_sample) * session->samples_capacity);
280
+ if (session->samples == NULL) {
281
+ rb_raise(rb_eNoMemError, "Failed to allocate memory");
282
+ }
283
+
284
+ session->configuration = NULL;
285
+
286
+ return TypedData_Wrap_Struct(self, &pf2_session_type, session);
287
+ }
288
+
289
+ void
290
+ pf2_session_dmark(void *sess)
291
+ {
292
+ struct pf2_session *session = sess;
293
+
294
+ // Disallow sample collection during marking
295
+ atomic_store_explicit(&session->is_marking, true, memory_order_release);
296
+
297
+ // Iterate over all samples in the ringbuffer and mark them
298
+ struct pf2_ringbuffer *rbuf = session->rbuf;
299
+ struct pf2_sample *sample;
300
+ int head = atomic_load_explicit(&rbuf->head, memory_order_acquire);
301
+ int tail = atomic_load_explicit(&rbuf->tail, memory_order_acquire);
302
+ while (head != tail) {
303
+ sample = &rbuf->samples[head];
304
+ // TODO: Move this to mark function in pf2_sample
305
+ for (int i = 0; i < sample->depth; i++) {
306
+ rb_gc_mark(sample->cmes[i]);
307
+ }
308
+ head = (head + 1) % rbuf->size;
309
+ }
310
+
311
+ // Iterate over all samples in the samples array and mark them
312
+ for (size_t i = 0; i < session->samples_index; i++) {
313
+ sample = &session->samples[i];
314
+ for (int i = 0; i < sample->depth; i++) {
315
+ rb_gc_mark(sample->cmes[i]);
316
+ }
317
+ }
318
+
319
+ // Allow sample collection
320
+ atomic_store_explicit(&session->is_marking, false, memory_order_release);
321
+ }
322
+
323
+ void
324
+ pf2_session_dfree(void *sess)
325
+ {
326
+ // TODO: Ensure the uninstall process is complete before freeing the session
327
+ struct pf2_session *session = sess;
328
+ pf2_configuration_free(session->configuration);
329
+ pf2_ringbuffer_free(session->rbuf);
330
+ free(session->samples);
331
+ free(session->collector_thread);
332
+ free(session);
333
+ }
334
+
335
+ size_t
336
+ pf2_session_dsize(const void *sess)
337
+ {
338
+ const struct pf2_session *session = sess;
339
+ return (
340
+ sizeof(struct pf2_session)
341
+ + sizeof(struct pf2_sample) * session->samples_capacity
342
+ + sizeof(struct pf2_sample) * session->rbuf->size
343
+ );
344
+ }
@@ -0,0 +1,51 @@
1
+ #ifndef PF2_SESSION_H
2
+ #define PF2_SESSION_H
3
+
4
+ #include <pthread.h>
5
+ #include <stdatomic.h>
6
+
7
+ #include <ruby.h>
8
+
9
+ #include "configuration.h"
10
+ #include "ringbuffer.h"
11
+ #include "sample.h"
12
+
13
+ struct pf2_session {
14
+ bool is_running;
15
+ timer_t timer;
16
+ struct pf2_ringbuffer *rbuf;
17
+ atomic_bool is_marking; // Whether garbage collection is in progress
18
+ pthread_t *collector_thread;
19
+
20
+ struct pf2_sample *samples; // Dynamic array of samples
21
+ size_t samples_index;
22
+ size_t samples_capacity; // Current capacity of the samples array
23
+
24
+ struct timespec start_time_realtime;
25
+ struct timespec start_time; // When profiling started
26
+ uint64_t duration_ns; // Duration of profiling in nanoseconds
27
+
28
+ struct pf2_configuration *configuration;
29
+ };
30
+
31
+ VALUE rb_pf2_session_initialize(int argc, VALUE *argv, VALUE self);
32
+ VALUE rb_pf2_session_start(VALUE self);
33
+ VALUE rb_pf2_session_stop(VALUE self);
34
+ VALUE rb_pf2_session_configuration(VALUE self);
35
+ VALUE pf2_session_alloc(VALUE self);
36
+ void pf2_session_dmark(void *sess);
37
+ void pf2_session_dfree(void *sess);
38
+ size_t pf2_session_dsize(const void *sess);
39
+
40
+ static const rb_data_type_t pf2_session_type = {
41
+ .wrap_struct_name = "Pf2c::Session",
42
+ .function = {
43
+ .dmark = pf2_session_dmark,
44
+ .dfree = pf2_session_dfree,
45
+ .dsize = pf2_session_dsize,
46
+ },
47
+ .data = NULL,
48
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
49
+ };
50
+
51
+ #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,11 +55,20 @@ module Pf2
53
55
  opts.on('-o', '--output FILE', 'Output file') do |path|
54
56
  options[:output_file] = path
55
57
  end
58
+ opts.on('--experimental-serializer', 'Enable the experimental serializer mode') do
59
+ options[:experimental_serializer] = true
60
+ end
56
61
  end
57
62
  option_parser.parse!(argv)
58
63
 
59
- profile = JSON.parse(File.read(argv[0]), symbolize_names: true, max_nesting: false)
60
- report = JSON.generate(Pf2::Reporter::FirefoxProfiler.new(profile).emit)
64
+ if options[:experimental_serializer]
65
+ profile = Marshal.load(File.read(argv[0]))
66
+ report = Pf2::Reporter::FirefoxProfilerSer2.new(profile).emit
67
+ report = JSON.generate(report)
68
+ else
69
+ profile = JSON.parse(File.read(argv[0]), symbolize_names: true, max_nesting: false)
70
+ report = JSON.generate(Pf2::Reporter::FirefoxProfiler.new(profile).emit)
71
+ end
61
72
 
62
73
  if options[:output_file]
63
74
  File.write(options[:output_file], report)
@@ -68,6 +79,26 @@ module Pf2
68
79
  return 0
69
80
  end
70
81
 
82
+ def subcommand_annotate(argv)
83
+ options = {}
84
+ option_parser = OptionParser.new do |opts|
85
+ opts.banner = "Usage: pf2 report [options] COMMAND"
86
+ opts.on('-h', '--help', 'Prints this help') do
87
+ puts opts
88
+ return 0
89
+ end
90
+ opts.on('-d', '--source-directory DIR', 'Path to the source directory') do |dir|
91
+ options[:source_directory] = dir
92
+ end
93
+ end
94
+ option_parser.parse!(argv)
95
+
96
+ profile = Marshal.load(File.binread(argv[0]))
97
+ Pf2::Reporter::Annotate.new(profile, options[:source_directory] || '.').annotate
98
+
99
+ return 0
100
+ end
101
+
71
102
  def subcommand_serve(argv)
72
103
  options = {}
73
104
  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
@@ -12,7 +12,7 @@ module Pf2
12
12
  end
13
13
 
14
14
  def inspect
15
- "" # TODO: provide something better
15
+ "#<#{self.class.name}>" # TODO: add sample count etc.
16
16
  end
17
17
 
18
18
  def emit