pf2 0.9.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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -1
  3. data/README.md +9 -4
  4. data/Rakefile +3 -9
  5. data/doc/development.md +6 -0
  6. data/ext/pf2/debug.h +12 -0
  7. data/ext/pf2/extconf.rb +23 -6
  8. data/ext/{pf2c → pf2}/sample.c +6 -0
  9. data/ext/{pf2c → pf2}/sample.h +4 -0
  10. data/ext/{pf2c → pf2}/serializer.c +1 -1
  11. data/ext/{pf2c → pf2}/session.c +93 -29
  12. data/ext/{pf2c → pf2}/session.h +6 -1
  13. data/lib/pf2/cli.rb +3 -11
  14. data/lib/pf2/reporter/firefox_profiler_ser2.rb +17 -13
  15. data/lib/pf2/reporter/stack_weaver.rb +8 -0
  16. data/lib/pf2/reporter.rb +0 -1
  17. data/lib/pf2/version.rb +1 -1
  18. data/lib/pf2.rb +1 -1
  19. metadata +25 -128
  20. data/Cargo.lock +0 -630
  21. data/Cargo.toml +0 -3
  22. data/crates/backtrace-sys2/.gitignore +0 -1
  23. data/crates/backtrace-sys2/Cargo.toml +0 -9
  24. data/crates/backtrace-sys2/build.rs +0 -45
  25. data/crates/backtrace-sys2/src/lib.rs +0 -5
  26. data/crates/backtrace-sys2/src/libbacktrace/.gitignore +0 -15
  27. data/crates/backtrace-sys2/src/libbacktrace/Isaac.Newton-Opticks.txt +0 -9286
  28. data/crates/backtrace-sys2/src/libbacktrace/LICENSE +0 -29
  29. data/crates/backtrace-sys2/src/libbacktrace/Makefile.am +0 -708
  30. data/crates/backtrace-sys2/src/libbacktrace/Makefile.in +0 -2820
  31. data/crates/backtrace-sys2/src/libbacktrace/README.md +0 -46
  32. data/crates/backtrace-sys2/src/libbacktrace/aclocal.m4 +0 -864
  33. data/crates/backtrace-sys2/src/libbacktrace/alloc.c +0 -167
  34. data/crates/backtrace-sys2/src/libbacktrace/allocfail.c +0 -136
  35. data/crates/backtrace-sys2/src/libbacktrace/allocfail.sh +0 -104
  36. data/crates/backtrace-sys2/src/libbacktrace/atomic.c +0 -113
  37. data/crates/backtrace-sys2/src/libbacktrace/backtrace-supported.h.in +0 -66
  38. data/crates/backtrace-sys2/src/libbacktrace/backtrace.c +0 -129
  39. data/crates/backtrace-sys2/src/libbacktrace/backtrace.h +0 -189
  40. data/crates/backtrace-sys2/src/libbacktrace/btest.c +0 -517
  41. data/crates/backtrace-sys2/src/libbacktrace/compile +0 -348
  42. data/crates/backtrace-sys2/src/libbacktrace/config/enable.m4 +0 -38
  43. data/crates/backtrace-sys2/src/libbacktrace/config/lead-dot.m4 +0 -31
  44. data/crates/backtrace-sys2/src/libbacktrace/config/libtool.m4 +0 -7545
  45. data/crates/backtrace-sys2/src/libbacktrace/config/ltoptions.m4 +0 -369
  46. data/crates/backtrace-sys2/src/libbacktrace/config/ltsugar.m4 +0 -123
  47. data/crates/backtrace-sys2/src/libbacktrace/config/ltversion.m4 +0 -23
  48. data/crates/backtrace-sys2/src/libbacktrace/config/lt~obsolete.m4 +0 -98
  49. data/crates/backtrace-sys2/src/libbacktrace/config/multi.m4 +0 -68
  50. data/crates/backtrace-sys2/src/libbacktrace/config/override.m4 +0 -117
  51. data/crates/backtrace-sys2/src/libbacktrace/config/unwind_ipinfo.m4 +0 -37
  52. data/crates/backtrace-sys2/src/libbacktrace/config/warnings.m4 +0 -227
  53. data/crates/backtrace-sys2/src/libbacktrace/config.guess +0 -1700
  54. data/crates/backtrace-sys2/src/libbacktrace/config.h.in +0 -185
  55. data/crates/backtrace-sys2/src/libbacktrace/config.sub +0 -1885
  56. data/crates/backtrace-sys2/src/libbacktrace/configure +0 -15929
  57. data/crates/backtrace-sys2/src/libbacktrace/configure.ac +0 -632
  58. data/crates/backtrace-sys2/src/libbacktrace/dwarf.c +0 -4409
  59. data/crates/backtrace-sys2/src/libbacktrace/edtest.c +0 -120
  60. data/crates/backtrace-sys2/src/libbacktrace/edtest2.c +0 -43
  61. data/crates/backtrace-sys2/src/libbacktrace/elf.c +0 -7465
  62. data/crates/backtrace-sys2/src/libbacktrace/fileline.c +0 -407
  63. data/crates/backtrace-sys2/src/libbacktrace/filenames.h +0 -52
  64. data/crates/backtrace-sys2/src/libbacktrace/filetype.awk +0 -13
  65. data/crates/backtrace-sys2/src/libbacktrace/install-debuginfo-for-buildid.sh.in +0 -65
  66. data/crates/backtrace-sys2/src/libbacktrace/install-sh +0 -501
  67. data/crates/backtrace-sys2/src/libbacktrace/instrumented_alloc.c +0 -114
  68. data/crates/backtrace-sys2/src/libbacktrace/internal.h +0 -428
  69. data/crates/backtrace-sys2/src/libbacktrace/ltmain.sh +0 -8636
  70. data/crates/backtrace-sys2/src/libbacktrace/macho.c +0 -1361
  71. data/crates/backtrace-sys2/src/libbacktrace/missing +0 -215
  72. data/crates/backtrace-sys2/src/libbacktrace/mmap.c +0 -331
  73. data/crates/backtrace-sys2/src/libbacktrace/mmapio.c +0 -110
  74. data/crates/backtrace-sys2/src/libbacktrace/move-if-change +0 -83
  75. data/crates/backtrace-sys2/src/libbacktrace/mtest.c +0 -410
  76. data/crates/backtrace-sys2/src/libbacktrace/nounwind.c +0 -66
  77. data/crates/backtrace-sys2/src/libbacktrace/pecoff.c +0 -1123
  78. data/crates/backtrace-sys2/src/libbacktrace/posix.c +0 -104
  79. data/crates/backtrace-sys2/src/libbacktrace/print.c +0 -117
  80. data/crates/backtrace-sys2/src/libbacktrace/read.c +0 -110
  81. data/crates/backtrace-sys2/src/libbacktrace/simple.c +0 -108
  82. data/crates/backtrace-sys2/src/libbacktrace/sort.c +0 -108
  83. data/crates/backtrace-sys2/src/libbacktrace/state.c +0 -72
  84. data/crates/backtrace-sys2/src/libbacktrace/stest.c +0 -137
  85. data/crates/backtrace-sys2/src/libbacktrace/test-driver +0 -148
  86. data/crates/backtrace-sys2/src/libbacktrace/test_format.c +0 -55
  87. data/crates/backtrace-sys2/src/libbacktrace/testlib.c +0 -234
  88. data/crates/backtrace-sys2/src/libbacktrace/testlib.h +0 -110
  89. data/crates/backtrace-sys2/src/libbacktrace/ttest.c +0 -161
  90. data/crates/backtrace-sys2/src/libbacktrace/unittest.c +0 -92
  91. data/crates/backtrace-sys2/src/libbacktrace/unknown.c +0 -65
  92. data/crates/backtrace-sys2/src/libbacktrace/xcoff.c +0 -1617
  93. data/crates/backtrace-sys2/src/libbacktrace/xztest.c +0 -508
  94. data/crates/backtrace-sys2/src/libbacktrace/zstdtest.c +0 -523
  95. data/crates/backtrace-sys2/src/libbacktrace/ztest.c +0 -541
  96. data/ext/pf2/Cargo.toml +0 -25
  97. data/ext/pf2/build.rs +0 -10
  98. data/ext/pf2/src/backtrace.rs +0 -127
  99. data/ext/pf2/src/lib.rs +0 -22
  100. data/ext/pf2/src/profile.rs +0 -69
  101. data/ext/pf2/src/profile_serializer.rs +0 -241
  102. data/ext/pf2/src/ringbuffer.rs +0 -150
  103. data/ext/pf2/src/ruby_c_api_helper.c +0 -6
  104. data/ext/pf2/src/ruby_init.rs +0 -40
  105. data/ext/pf2/src/ruby_internal_apis.rs +0 -77
  106. data/ext/pf2/src/sample.rs +0 -67
  107. data/ext/pf2/src/scheduler.rs +0 -10
  108. data/ext/pf2/src/serialization/profile.rs +0 -48
  109. data/ext/pf2/src/serialization/serializer.rs +0 -329
  110. data/ext/pf2/src/serialization.rs +0 -2
  111. data/ext/pf2/src/session/configuration.rs +0 -114
  112. data/ext/pf2/src/session/new_thread_watcher.rs +0 -80
  113. data/ext/pf2/src/session/ruby_object.rs +0 -90
  114. data/ext/pf2/src/session.rs +0 -248
  115. data/ext/pf2/src/siginfo_t.c +0 -5
  116. data/ext/pf2/src/signal_scheduler.rs +0 -201
  117. data/ext/pf2/src/signal_scheduler_unsupported_platform.rs +0 -39
  118. data/ext/pf2/src/timer_thread_scheduler.rs +0 -179
  119. data/ext/pf2/src/util.rs +0 -31
  120. data/ext/pf2c/extconf.rb +0 -21
  121. data/lib/pf2/reporter/firefox_profiler.rb +0 -397
  122. data/rust-toolchain.toml +0 -2
  123. data/rustfmt.toml +0 -1
  124. /data/ext/{pf2c → pf2}/backtrace_state.c +0 -0
  125. /data/ext/{pf2c → pf2}/backtrace_state.h +0 -0
  126. /data/ext/{pf2c → pf2}/configuration.c +0 -0
  127. /data/ext/{pf2c → pf2}/configuration.h +0 -0
  128. /data/ext/{pf2c → pf2}/pf2.c +0 -0
  129. /data/ext/{pf2c → pf2}/pf2.h +0 -0
  130. /data/ext/{pf2c → pf2}/ringbuffer.c +0 -0
  131. /data/ext/{pf2c → pf2}/ringbuffer.h +0 -0
  132. /data/ext/{pf2c → pf2}/serializer.h +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a0dc2d338a482c56472ec6279cc4fd111c4263a540ab00872d57439a14244af
4
- data.tar.gz: 56bdc8e81a1d4bccee07d7785eec713329c8ec4b9e659f2b494e12e8e016e60a
3
+ metadata.gz: 22269113784c160efa2ef33412444646afdb33d58215f0a7f0ec2e4e05ad2708
4
+ data.tar.gz: cc1b783cab40fc4469b5bc09fc3a3a5dec4a76357d14accb7f6a4458ea7cc7bd
5
5
  SHA512:
6
- metadata.gz: 43449c0433cfdc390aecf5335f2a8631d11a0a41ac3afc6fc469909e5fd28c45bde16de8fbcff5229c16551d26a2b47210ff2342dea5b190a47ca128e9656c80
7
- data.tar.gz: e6f5f2481932100cd82504dcb3b24e71b123b1263a2588b584cf5e72e53817f2004c7ffe6e96ff12f9815975ddfeac679219468a1af9223f6b5a367b3ec0b5e6
6
+ metadata.gz: 551b033c17201f6f21d741e53bf5b5c43cce3adb3f5daa0e1f17679176c23b87d727cbd162b67f855a39b9fd344ed499ec7d283598f95de30c25d2b3ac7d179e
7
+ data.tar.gz: ffa342dcfd1c467737485c0aba8497ff98c144a53ae5c0e50566ef1ba58e6fc9e0cfbe0ad870e64788b2a93f31eb8011c0fa1194e674f092dfb9b662d7b9db71
data/CHANGELOG.md CHANGED
@@ -1,4 +1,18 @@
1
- ## [Unreleased]
1
+ ## [0.10.0] - 2025-12-26
2
+
3
+ ## Added
4
+
5
+ **This version contains a complete rewrite of the profiler!**
6
+
7
+ - The default sample collection backend has been switched to the new C-based backend.
8
+ - The previous Rust-based backed has been removed. Use v0.9.0 if you need it.
9
+ - macOS / non-Linux platform support!
10
+ - On platforms which lack `timer_create(3)` such as macOS, Pf2 now fall backs to `setitimer(3)` based sampling. This mode does not support per-thread CPU time sampling.
11
+
12
+ ### Changed
13
+
14
+ - `logger` is now declared as a dependency (Ruby 4.0 compat).
15
+
2
16
 
3
17
  ## [0.9.0] - 2025-03-22
4
18
 
@@ -11,6 +25,7 @@
11
25
 
12
26
  - Set SA_RESTART flag to reduce EINTRs in profiled code
13
27
 
28
+
14
29
  ## [0.8.0] - 2025-01-27
15
30
 
16
31
  ## Added
@@ -19,12 +34,14 @@
19
34
  - This serializer is more efficient and has a smaller memory footprint than the default serializer.
20
35
  - Ser2 still lacks some features, such as weaving of native stacks.
21
36
 
37
+
22
38
  ## [0.7.1] - 2025-01-02
23
39
 
24
40
  ### Fixed
25
41
 
26
42
  - Reverted Cargo.lock version to 3 to support older versions of Rust (<1.78).
27
43
 
44
+
28
45
  ## [0.7.0] - 2025-01-03
29
46
 
30
47
  ### Changed
data/README.md CHANGED
@@ -116,7 +116,7 @@ Schedulers determine when to execute sample collection, based on configuration (
116
116
 
117
117
  The first is the `SignalScheduler`, based on POSIX timers. Pf2 will use this scheduler when possible. SignalScheduler creates a POSIX timer for each Ruby Thread (the underlying pthread to be more accurate) using `timer_create(2)`. This leaves the actual time-keeping to the OS, which is capable of tracking accurate per-thread CPU time usage.
118
118
 
119
- When the specified interval has arrived (the timer has _expired_), the OS delivers us a SIGALRM (note: Unlike `setitimer(2)`, `timer_create(2)` allows us to choose which signal to be delivered, and Pf2 uses SIGALRM regardless of time mode). This is why the scheduler is named SignalScheduler.
119
+ When the specified interval has arrived (the timer has _expired_), the OS delivers us a SIGPROF signal. This is why the scheduler is named SignalScheduler.
120
120
 
121
121
  Signals are directed to Ruby Threads' underlying pthread, effectively "pausing" the Thread's activity. This routing is done using `SIGEV_THREAD_ID`, which is a Linux-only feature. Sample collection is done in the signal handler, which is expected to be more _accurate_, capturing the paused Thread's activity.
122
122
 
@@ -128,11 +128,16 @@ Another scheduler is the `TimerThreadScheduler`, which maintains a time-keeping
128
128
 
129
129
  This scheduler is wall-time only, and does not support CPU-time based profiling.
130
130
 
131
- Future Plans
131
+ #### macOS Support
132
+
133
+ On platforms where `timer_create()` is not supported (namely macOS), Pf2 falls back to `setitimer()`.
134
+
135
+
136
+ Wishlist
132
137
  --------
133
138
 
134
- - Remove known limitations, if possible
135
- - Implement a "tracing" scheduler, using the C TracePoint API
139
+ - [Flame Scopes](https://www.brendangregg.com/flamescope.html)
140
+ - More unit/e2e tests
136
141
  - more
137
142
 
138
143
  Development
data/Rakefile CHANGED
@@ -4,15 +4,9 @@ require 'minitest/test_task'
4
4
 
5
5
  task default: %i[]
6
6
 
7
- if ENV['PF2_PF2C'] == '1'
8
- Rake::ExtensionTask.new 'pf2c' do |ext|
9
- ext.name = 'pf2'
10
- ext.lib_dir = 'lib/pf2'
11
- end
12
- else
13
- Rake::ExtensionTask.new 'pf2' do |ext|
14
- ext.lib_dir = 'lib/pf2'
15
- end
7
+ Rake::ExtensionTask.new 'pf2' do |ext|
8
+ ext.name = 'pf2'
9
+ ext.lib_dir = 'lib/pf2'
16
10
  end
17
11
 
18
12
  Minitest::TestTask.create(:test) do |t|
data/doc/development.md CHANGED
@@ -1,6 +1,12 @@
1
1
  Pf2 Development
2
2
  ===========
3
3
 
4
+ Setup
5
+ --------
6
+
7
+ - `git submodule update --init`
8
+
9
+
4
10
  Releasing
5
11
  --------
6
12
 
data/ext/pf2/debug.h ADDED
@@ -0,0 +1,12 @@
1
+ #ifndef PF2_DEBUG_H
2
+ #define PF2_DEBUG_H
3
+
4
+ #include <stdio.h>
5
+
6
+ #ifdef PF2_DEBUG
7
+ #define PF2_DEBUG_LOG(format, ...) printf(format, ##__VA_ARGS__)
8
+ #else
9
+ #define PF2_DEBUG_LOG(format, ...) ((void)0)
10
+ #endif
11
+
12
+ #endif // PF2_DEBUG_H
data/ext/pf2/extconf.rb CHANGED
@@ -1,10 +1,27 @@
1
1
  require 'mkmf'
2
- require 'rb_sys/mkmf'
2
+ require 'mini_portile2'
3
3
 
4
- abort 'missing rb_profile_thread_frames()' unless have_func 'rb_profile_thread_frames'
4
+ libbacktrace = MiniPortile.new('libbacktrace', '1.0.0')
5
+ libbacktrace.source_directory = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'vendor', 'libbacktrace'))
6
+ libbacktrace.configure_options << 'CFLAGS=-fPIC'
7
+ libbacktrace.cook
8
+ libbacktrace.mkmf_config
5
9
 
6
- create_rust_makefile 'pf2/pf2' do |r|
7
- if ENV['PF2_FEATURES']
8
- r.features = ENV['PF2_FEATURES'].split(",")
9
- end
10
+ if !have_func('backtrace_full', 'backtrace.h')
11
+ raise 'libbacktrace has not been properly configured'
12
+ end
13
+
14
+ append_ldflags('-lrt') # for timer_create
15
+ append_cflags('-fvisibility=hidden')
16
+ append_cflags('-DPF2_DEBUG') if ENV['PF2_DEBUG'] == '1'
17
+
18
+ # Check for timer functions
19
+ have_timer_create = have_func('timer_create')
20
+ have_setitimer = have_func('setitimer')
21
+
22
+ if have_timer_create || have_setitimer
23
+ $srcs = Dir.glob("#{File.join(File.dirname(__FILE__), '*.c')}")
24
+ create_makefile 'pf2/pf2'
25
+ else
26
+ raise 'Neither timer_create nor setitimer is available'
10
27
  end
@@ -1,5 +1,6 @@
1
1
  #include <stdbool.h>
2
2
  #include <time.h>
3
+ #include <pthread.h>
3
4
 
4
5
  #include <backtrace.h>
5
6
  #include <ruby.h>
@@ -17,11 +18,16 @@ static int backtrace_on_ok(void *data, uintptr_t pc);
17
18
  bool
18
19
  pf2_sample_capture(struct pf2_sample *sample)
19
20
  {
21
+ // Initialize sample
22
+ memset(sample, 0, sizeof(struct pf2_sample));
23
+
20
24
  // Record the current time
21
25
  struct timespec now;
22
26
  clock_gettime(CLOCK_MONOTONIC, &now);
23
27
  sample->timestamp_ns = (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec;
24
28
 
29
+ sample->context_pthread = pthread_self();
30
+
25
31
  // Obtain the current stack from Ruby
26
32
  sample->depth = rb_profile_frames(0, 200, sample->cmes, sample->linenos);
27
33
 
@@ -1,11 +1,15 @@
1
1
  #ifndef PF2_SAMPLE_H
2
2
  #define PF2_SAMPLE_H
3
3
 
4
+ #include <pthread.h>
5
+
4
6
  #include <ruby.h>
5
7
 
6
8
  extern const int PF2_SAMPLE_MAX_NATIVE_DEPTH;
7
9
 
8
10
  struct pf2_sample {
11
+ pthread_t context_pthread;
12
+
9
13
  int depth;
10
14
  VALUE cmes[200];
11
15
  int linenos[200];
@@ -83,7 +83,7 @@ pf2_ser_prepare(struct pf2_ser *serializer, struct pf2_session *session) {
83
83
  ensure_samples_capacity(serializer);
84
84
 
85
85
  struct pf2_ser_sample *ser_sample = &serializer->samples[serializer->samples_count++];
86
- ser_sample->ruby_thread_id = 0; // TODO: Add thread ID support
86
+ ser_sample->ruby_thread_id = sample->context_pthread;
87
87
  ser_sample->elapsed_ns = sample->timestamp_ns - serializer->start_timestamp_ns;
88
88
 
89
89
  // Copy and process Ruby stack frames
@@ -1,11 +1,12 @@
1
1
  #include <bits/time.h>
2
+ #include <pthread.h>
3
+ #include <signal.h>
2
4
  #include <stdatomic.h>
3
5
  #include <stdbool.h>
4
6
  #include <stdio.h>
5
7
  #include <stdlib.h>
6
- #include <signal.h>
8
+ #include <sys/time.h>
7
9
  #include <time.h>
8
- #include <pthread.h>
9
10
 
10
11
  #include <ruby.h>
11
12
  #include <ruby/debug.h>
@@ -14,13 +15,20 @@
14
15
 
15
16
  #include "backtrace_state.h"
16
17
  #include "configuration.h"
18
+ #include "debug.h"
17
19
  #include "sample.h"
18
20
  #include "session.h"
19
21
  #include "serializer.h"
20
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
+
21
28
  static void *sample_collector_thread(void *arg);
22
29
  static void sigprof_handler(int sig, siginfo_t *info, void *ucontext);
23
30
  bool ensure_sample_capacity(struct pf2_session *session);
31
+ static void pf2_session_stop(struct pf2_session *session);
24
32
 
25
33
  VALUE
26
34
  rb_pf2_session_initialize(int argc, VALUE *argv, VALUE self)
@@ -60,17 +68,28 @@ rb_pf2_session_start(VALUE self)
60
68
  rb_raise(rb_eRuntimeError, "Failed to spawn sample collector thread");
61
69
  }
62
70
 
63
- // Configure signal handler
71
+ // Install signal handler for SIGPROF
64
72
  struct sigaction sa;
65
73
  sa.sa_sigaction = sigprof_handler;
66
74
  sigemptyset(&sa.sa_mask);
67
75
  sigaddset(&sa.sa_mask, SIGPROF); // Mask SIGPROFs when handler is running
68
76
  sa.sa_flags = SA_SIGINFO | SA_RESTART;
69
77
  if (sigaction(SIGPROF, &sa, NULL) == -1) {
70
- rb_raise(rb_eRuntimeError, "Failed to install signal handler");
78
+ rb_raise(rb_eRuntimeError, "Failed to install SIGPROF handler");
71
79
  }
72
80
 
73
- // Configure a timer to send SIGPROF every 10 ms of CPU time
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
74
93
  struct sigevent sev;
75
94
  sev.sigev_notify = SIGEV_SIGNAL;
76
95
  sev.sigev_signo = SIGPROF;
@@ -97,6 +116,30 @@ rb_pf2_session_start(VALUE self)
97
116
  if (timer_settime(session->timer, 0, &its, NULL) == -1) {
98
117
  rb_raise(rb_eRuntimeError, "Failed to start timer");
99
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
100
143
 
101
144
  return Qtrue;
102
145
  }
@@ -113,9 +156,7 @@ sample_collector_thread(void *arg)
113
156
  // Ensure we have capacity before adding a new sample
114
157
  if (!ensure_sample_capacity(session)) {
115
158
  // Failed to expand buffer
116
- #ifdef PF2_DEBUG
117
- printf("Failed to expand sample buffer. Dropping sample\n");
118
- #endif
159
+ PF2_DEBUG_LOG("Failed to expand sample buffer. Dropping sample\n");
119
160
  break;
120
161
  }
121
162
 
@@ -140,31 +181,30 @@ sigprof_handler(int sig, siginfo_t *info, void *ucontext)
140
181
  clock_gettime(CLOCK_MONOTONIC, &sig_start_time);
141
182
  #endif
142
183
 
143
- struct pf2_session *session = info->si_value.sival_ptr;
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
144
190
 
145
191
  // If garbage collection is in progress, don't collect samples.
146
192
  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
193
+ PF2_DEBUG_LOG("Dropping sample: Garbage collection is in progress\n");
150
194
  return;
151
195
  }
152
196
 
153
- struct pf2_sample sample = { 0 };
197
+ struct pf2_sample sample;
154
198
 
155
199
  if (pf2_sample_capture(&sample) == false) {
156
- #ifdef PF2_DEBUG
157
- printf("Dropping sample: Failed to capture sample\n");
158
- #endif
200
+ PF2_DEBUG_LOG("Dropping sample: Failed to capture sample\n");
159
201
  return;
160
202
  }
161
203
 
162
204
  // Copy the sample to the ringbuffer
163
205
  if (pf2_ringbuffer_push(session->rbuf, &sample) == false) {
164
206
  // Copy failed. The sample buffer is full.
165
- #ifdef PF2_DEBUG
166
- printf("Dropping sample: Sample buffer is full\n");
167
- #endif
207
+ PF2_DEBUG_LOG("Dropping sample: Sample buffer is full\n");
168
208
  return;
169
209
  }
170
210
 
@@ -177,7 +217,7 @@ sigprof_handler(int sig, siginfo_t *info, void *ucontext)
177
217
  (sig_end_time.tv_sec - sig_start_time.tv_sec) * 1000000000L +
178
218
  (sig_end_time.tv_nsec - sig_start_time.tv_nsec);
179
219
 
180
- printf("sigprof_handler: consumed_time_ns: %lu\n", sample.consumed_time_ns);
220
+ PF2_DEBUG_LOG("sigprof_handler: consumed_time_ns: %lu\n", sample.consumed_time_ns);
181
221
  #endif
182
222
  }
183
223
 
@@ -212,6 +252,20 @@ rb_pf2_session_stop(VALUE self)
212
252
  struct pf2_session *session;
213
253
  TypedData_Get_Struct(self, struct pf2_session, &pf2_session_type, session);
214
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
+ {
215
269
  // Calculate duration
216
270
  struct timespec end_time;
217
271
  clock_gettime(CLOCK_MONOTONIC, &end_time);
@@ -220,21 +274,24 @@ rb_pf2_session_stop(VALUE self)
220
274
  session->duration_ns = end_ns - start_ns;
221
275
 
222
276
  // Disarm and delete the timer.
277
+ #ifdef HAVE_TIMER_CREATE
223
278
  if (timer_delete(session->timer) == -1) {
224
279
  rb_raise(rb_eRuntimeError, "Failed to delete timer");
225
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
226
291
 
227
292
  // Terminate the collector thread
228
293
  session->is_running = false;
229
294
  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
295
  }
239
296
 
240
297
  VALUE
@@ -323,8 +380,15 @@ pf2_session_dmark(void *sess)
323
380
  void
324
381
  pf2_session_dfree(void *sess)
325
382
  {
326
- // TODO: Ensure the uninstall process is complete before freeing the session
327
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
+
328
392
  pf2_configuration_free(session->configuration);
329
393
  pf2_ringbuffer_free(session->rbuf);
330
394
  free(session->samples);
@@ -3,6 +3,7 @@
3
3
 
4
4
  #include <pthread.h>
5
5
  #include <stdatomic.h>
6
+ #include <sys/time.h>
6
7
 
7
8
  #include <ruby.h>
8
9
 
@@ -12,7 +13,11 @@
12
13
 
13
14
  struct pf2_session {
14
15
  bool is_running;
16
+ #ifdef HAVE_TIMER_CREATE
15
17
  timer_t timer;
18
+ #else
19
+ struct itimerval timer;
20
+ #endif
16
21
  struct pf2_ringbuffer *rbuf;
17
22
  atomic_bool is_marking; // Whether garbage collection is in progress
18
23
  pthread_t *collector_thread;
@@ -45,7 +50,7 @@ static const rb_data_type_t pf2_session_type = {
45
50
  .dsize = pf2_session_dsize,
46
51
  },
47
52
  .data = NULL,
48
- .flags = RUBY_TYPED_FREE_IMMEDIATELY,
53
+ .flags = 0,
49
54
  };
50
55
 
51
56
  #endif // PF2_SESSION_H
data/lib/pf2/cli.rb CHANGED
@@ -55,20 +55,12 @@ module Pf2
55
55
  opts.on('-o', '--output FILE', 'Output file') do |path|
56
56
  options[:output_file] = path
57
57
  end
58
- opts.on('--experimental-serializer', 'Enable the experimental serializer mode') do
59
- options[:experimental_serializer] = true
60
- end
61
58
  end
62
59
  option_parser.parse!(argv)
63
60
 
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
+ profile = Marshal.load(File.read(argv[0]))
62
+ report = Pf2::Reporter::FirefoxProfilerSer2.new(profile).emit
63
+ report = JSON.generate(report)
72
64
 
73
65
  if options[:output_file]
74
66
  File.write(options[:output_file], report)
@@ -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
@@ -2,6 +2,14 @@
2
2
 
3
3
  module Pf2
4
4
  module Reporter
5
+ # "Weaves" the native stack into the Ruby stack.
6
+ #
7
+ # Strategy:
8
+ # - Split the stack into Ruby and Native parts
9
+ # - Start from the root of the Native stack
10
+ # - Dig in to the native stack until we hit a rb_vm_exec(), which marks a call into Ruby code
11
+ # - Switch to Ruby stack. Keep digging until we hit a Cfunc call, then switch back to Native stack
12
+ # - Repeat until we consume the entire stack
5
13
  class StackWeaver
6
14
  def initialize(profile)
7
15
  @profile = profile
data/lib/pf2/reporter.rb CHANGED
@@ -2,5 +2,4 @@
2
2
 
3
3
  require_relative './reporter/annotate'
4
4
  require_relative './reporter/stack_weaver'
5
- require_relative './reporter/firefox_profiler'
6
5
  require_relative './reporter/firefox_profiler_ser2'
data/lib/pf2/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pf2
4
- VERSION = '0.9.0'
4
+ VERSION = '0.10.0'
5
5
  end
data/lib/pf2.rb CHANGED
@@ -8,7 +8,7 @@ module Pf2
8
8
  class Error < StandardError; end
9
9
 
10
10
  def self.start(...)
11
- @@session = Pf2::Session.new(...)
11
+ @@session = Pf2c::Session.new(...)
12
12
  @@session.start
13
13
  end
14
14