pf2 0.9.0 → 0.11.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/.document +3 -0
  3. data/.rdoc_options +6 -0
  4. data/CHANGELOG.md +36 -0
  5. data/README.md +31 -4
  6. data/Rakefile +8 -9
  7. data/doc/development.md +6 -0
  8. data/ext/pf2/debug.h +12 -0
  9. data/ext/pf2/extconf.rb +23 -6
  10. data/ext/pf2/pf2.c +17 -0
  11. data/ext/{pf2c → pf2}/sample.c +7 -3
  12. data/ext/pf2/sample.h +27 -0
  13. data/ext/{pf2c → pf2}/serializer.c +1 -1
  14. data/ext/{pf2c → pf2}/session.c +116 -31
  15. data/ext/{pf2c → pf2}/session.h +7 -2
  16. data/lib/pf2/cli.rb +3 -11
  17. data/lib/pf2/reporter/firefox_profiler_ser2.rb +17 -13
  18. data/lib/pf2/reporter/stack_weaver.rb +9 -1
  19. data/lib/pf2/reporter.rb +0 -1
  20. data/lib/pf2/version.rb +1 -1
  21. data/lib/pf2.rb +1 -2
  22. metadata +41 -129
  23. data/Cargo.lock +0 -630
  24. data/Cargo.toml +0 -3
  25. data/crates/backtrace-sys2/.gitignore +0 -1
  26. data/crates/backtrace-sys2/Cargo.toml +0 -9
  27. data/crates/backtrace-sys2/build.rs +0 -45
  28. data/crates/backtrace-sys2/src/lib.rs +0 -5
  29. data/crates/backtrace-sys2/src/libbacktrace/.gitignore +0 -15
  30. data/crates/backtrace-sys2/src/libbacktrace/Isaac.Newton-Opticks.txt +0 -9286
  31. data/crates/backtrace-sys2/src/libbacktrace/LICENSE +0 -29
  32. data/crates/backtrace-sys2/src/libbacktrace/Makefile.am +0 -708
  33. data/crates/backtrace-sys2/src/libbacktrace/Makefile.in +0 -2820
  34. data/crates/backtrace-sys2/src/libbacktrace/README.md +0 -46
  35. data/crates/backtrace-sys2/src/libbacktrace/aclocal.m4 +0 -864
  36. data/crates/backtrace-sys2/src/libbacktrace/alloc.c +0 -167
  37. data/crates/backtrace-sys2/src/libbacktrace/allocfail.c +0 -136
  38. data/crates/backtrace-sys2/src/libbacktrace/allocfail.sh +0 -104
  39. data/crates/backtrace-sys2/src/libbacktrace/atomic.c +0 -113
  40. data/crates/backtrace-sys2/src/libbacktrace/backtrace-supported.h.in +0 -66
  41. data/crates/backtrace-sys2/src/libbacktrace/backtrace.c +0 -129
  42. data/crates/backtrace-sys2/src/libbacktrace/backtrace.h +0 -189
  43. data/crates/backtrace-sys2/src/libbacktrace/btest.c +0 -517
  44. data/crates/backtrace-sys2/src/libbacktrace/compile +0 -348
  45. data/crates/backtrace-sys2/src/libbacktrace/config/enable.m4 +0 -38
  46. data/crates/backtrace-sys2/src/libbacktrace/config/lead-dot.m4 +0 -31
  47. data/crates/backtrace-sys2/src/libbacktrace/config/libtool.m4 +0 -7545
  48. data/crates/backtrace-sys2/src/libbacktrace/config/ltoptions.m4 +0 -369
  49. data/crates/backtrace-sys2/src/libbacktrace/config/ltsugar.m4 +0 -123
  50. data/crates/backtrace-sys2/src/libbacktrace/config/ltversion.m4 +0 -23
  51. data/crates/backtrace-sys2/src/libbacktrace/config/lt~obsolete.m4 +0 -98
  52. data/crates/backtrace-sys2/src/libbacktrace/config/multi.m4 +0 -68
  53. data/crates/backtrace-sys2/src/libbacktrace/config/override.m4 +0 -117
  54. data/crates/backtrace-sys2/src/libbacktrace/config/unwind_ipinfo.m4 +0 -37
  55. data/crates/backtrace-sys2/src/libbacktrace/config/warnings.m4 +0 -227
  56. data/crates/backtrace-sys2/src/libbacktrace/config.guess +0 -1700
  57. data/crates/backtrace-sys2/src/libbacktrace/config.h.in +0 -185
  58. data/crates/backtrace-sys2/src/libbacktrace/config.sub +0 -1885
  59. data/crates/backtrace-sys2/src/libbacktrace/configure +0 -15929
  60. data/crates/backtrace-sys2/src/libbacktrace/configure.ac +0 -632
  61. data/crates/backtrace-sys2/src/libbacktrace/dwarf.c +0 -4409
  62. data/crates/backtrace-sys2/src/libbacktrace/edtest.c +0 -120
  63. data/crates/backtrace-sys2/src/libbacktrace/edtest2.c +0 -43
  64. data/crates/backtrace-sys2/src/libbacktrace/elf.c +0 -7465
  65. data/crates/backtrace-sys2/src/libbacktrace/fileline.c +0 -407
  66. data/crates/backtrace-sys2/src/libbacktrace/filenames.h +0 -52
  67. data/crates/backtrace-sys2/src/libbacktrace/filetype.awk +0 -13
  68. data/crates/backtrace-sys2/src/libbacktrace/install-debuginfo-for-buildid.sh.in +0 -65
  69. data/crates/backtrace-sys2/src/libbacktrace/install-sh +0 -501
  70. data/crates/backtrace-sys2/src/libbacktrace/instrumented_alloc.c +0 -114
  71. data/crates/backtrace-sys2/src/libbacktrace/internal.h +0 -428
  72. data/crates/backtrace-sys2/src/libbacktrace/ltmain.sh +0 -8636
  73. data/crates/backtrace-sys2/src/libbacktrace/macho.c +0 -1361
  74. data/crates/backtrace-sys2/src/libbacktrace/missing +0 -215
  75. data/crates/backtrace-sys2/src/libbacktrace/mmap.c +0 -331
  76. data/crates/backtrace-sys2/src/libbacktrace/mmapio.c +0 -110
  77. data/crates/backtrace-sys2/src/libbacktrace/move-if-change +0 -83
  78. data/crates/backtrace-sys2/src/libbacktrace/mtest.c +0 -410
  79. data/crates/backtrace-sys2/src/libbacktrace/nounwind.c +0 -66
  80. data/crates/backtrace-sys2/src/libbacktrace/pecoff.c +0 -1123
  81. data/crates/backtrace-sys2/src/libbacktrace/posix.c +0 -104
  82. data/crates/backtrace-sys2/src/libbacktrace/print.c +0 -117
  83. data/crates/backtrace-sys2/src/libbacktrace/read.c +0 -110
  84. data/crates/backtrace-sys2/src/libbacktrace/simple.c +0 -108
  85. data/crates/backtrace-sys2/src/libbacktrace/sort.c +0 -108
  86. data/crates/backtrace-sys2/src/libbacktrace/state.c +0 -72
  87. data/crates/backtrace-sys2/src/libbacktrace/stest.c +0 -137
  88. data/crates/backtrace-sys2/src/libbacktrace/test-driver +0 -148
  89. data/crates/backtrace-sys2/src/libbacktrace/test_format.c +0 -55
  90. data/crates/backtrace-sys2/src/libbacktrace/testlib.c +0 -234
  91. data/crates/backtrace-sys2/src/libbacktrace/testlib.h +0 -110
  92. data/crates/backtrace-sys2/src/libbacktrace/ttest.c +0 -161
  93. data/crates/backtrace-sys2/src/libbacktrace/unittest.c +0 -92
  94. data/crates/backtrace-sys2/src/libbacktrace/unknown.c +0 -65
  95. data/crates/backtrace-sys2/src/libbacktrace/xcoff.c +0 -1617
  96. data/crates/backtrace-sys2/src/libbacktrace/xztest.c +0 -508
  97. data/crates/backtrace-sys2/src/libbacktrace/zstdtest.c +0 -523
  98. data/crates/backtrace-sys2/src/libbacktrace/ztest.c +0 -541
  99. data/ext/pf2/Cargo.toml +0 -25
  100. data/ext/pf2/build.rs +0 -10
  101. data/ext/pf2/src/backtrace.rs +0 -127
  102. data/ext/pf2/src/lib.rs +0 -22
  103. data/ext/pf2/src/profile.rs +0 -69
  104. data/ext/pf2/src/profile_serializer.rs +0 -241
  105. data/ext/pf2/src/ringbuffer.rs +0 -150
  106. data/ext/pf2/src/ruby_c_api_helper.c +0 -6
  107. data/ext/pf2/src/ruby_init.rs +0 -40
  108. data/ext/pf2/src/ruby_internal_apis.rs +0 -77
  109. data/ext/pf2/src/sample.rs +0 -67
  110. data/ext/pf2/src/scheduler.rs +0 -10
  111. data/ext/pf2/src/serialization/profile.rs +0 -48
  112. data/ext/pf2/src/serialization/serializer.rs +0 -329
  113. data/ext/pf2/src/serialization.rs +0 -2
  114. data/ext/pf2/src/session/configuration.rs +0 -114
  115. data/ext/pf2/src/session/new_thread_watcher.rs +0 -80
  116. data/ext/pf2/src/session/ruby_object.rs +0 -90
  117. data/ext/pf2/src/session.rs +0 -248
  118. data/ext/pf2/src/siginfo_t.c +0 -5
  119. data/ext/pf2/src/signal_scheduler.rs +0 -201
  120. data/ext/pf2/src/signal_scheduler_unsupported_platform.rs +0 -39
  121. data/ext/pf2/src/timer_thread_scheduler.rs +0 -179
  122. data/ext/pf2/src/util.rs +0 -31
  123. data/ext/pf2c/extconf.rb +0 -21
  124. data/ext/pf2c/pf2.c +0 -17
  125. data/ext/pf2c/sample.h +0 -22
  126. data/lib/pf2/reporter/firefox_profiler.rb +0 -397
  127. data/lib/pf2/session.rb +0 -9
  128. data/rust-toolchain.toml +0 -2
  129. data/rustfmt.toml +0 -1
  130. /data/ext/{pf2c → pf2}/backtrace_state.c +0 -0
  131. /data/ext/{pf2c → pf2}/backtrace_state.h +0 -0
  132. /data/ext/{pf2c → pf2}/configuration.c +0 -0
  133. /data/ext/{pf2c → pf2}/configuration.h +0 -0
  134. /data/ext/{pf2c → pf2}/pf2.h +0 -0
  135. /data/ext/{pf2c → pf2}/ringbuffer.c +0 -0
  136. /data/ext/{pf2c → pf2}/ringbuffer.h +0 -0
  137. /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: 74e3ac2900007b7750e3c3274833e1f713b6e85570db0aa738761ea99b517fe8
4
+ data.tar.gz: 68ffc8d681328e4d5542e4bd4b3a98edfd08d69fa1b54342fdccbe8f9495bcf0
5
5
  SHA512:
6
- metadata.gz: 43449c0433cfdc390aecf5335f2a8631d11a0a41ac3afc6fc469909e5fd28c45bde16de8fbcff5229c16551d26a2b47210ff2342dea5b190a47ca128e9656c80
7
- data.tar.gz: e6f5f2481932100cd82504dcb3b24e71b123b1263a2588b584cf5e72e53817f2004c7ffe6e96ff12f9815975ddfeac679219468a1af9223f6b5a367b3ec0b5e6
6
+ metadata.gz: 4f1e67870eac32734a7532f46f29b74589ec6d71c59c925fc02456f473a516218fbbf46fed730558f0b9525c585dbb2fbc088fcf9cbfb47e7f4b89adda7f4974
7
+ data.tar.gz: 8f5ec74f112eb65a512dbcd7002b0db9a4da0d3a2124d3bd2e7106412c71009b36f92ff442efc87910a5a8829094c06e1ee93169c829ab893dd8e1bd86a81a4f
data/.document ADDED
@@ -0,0 +1,3 @@
1
+ ext/
2
+ lib/
3
+ *.md
data/.rdoc_options ADDED
@@ -0,0 +1,6 @@
1
+ title: Pf2
2
+ main_page: README.md
3
+ encoding: UTF-8
4
+
5
+ autolink_excluded_words:
6
+ - Pf2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.11.0] - 2025-12-27
4
+
5
+ ### Added
6
+
7
+ - RDoc documentation is now online - https://osyoyu.github.io/pf2/
8
+ - Native stack consolidation now supports LTO-ed binaries (@hanazuki)
9
+
10
+ ### Changed
11
+
12
+ - `Pf2c` module is now completely removed. `Pf2c::Session` has been merged as `Pf2::Session`.
13
+
14
+ ### Fixed
15
+
16
+ - Fixed an bug where the program crashes when a `Pf2::Session` is GC'd before profiling starts.
17
+ - Fixed an bug where the program crashes when the native stack was more than 200 frames deep.
18
+
19
+
20
+ ## [0.10.0] - 2025-12-26
21
+
22
+ ### Added
23
+
24
+ **This version contains a complete rewrite of the profiler!**
25
+
26
+ - The default sample collection backend has been switched to the new C-based backend.
27
+ - The previous Rust-based backed has been removed. Use v0.9.0 if you need it.
28
+ - macOS / non-Linux platform support!
29
+ - 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.
30
+
31
+ ### Changed
32
+
33
+ - `logger` is now declared as a dependency (Ruby 4.0 compat).
34
+
35
+
3
36
  ## [0.9.0] - 2025-03-22
4
37
 
5
38
  ## Added
@@ -11,6 +44,7 @@
11
44
 
12
45
  - Set SA_RESTART flag to reduce EINTRs in profiled code
13
46
 
47
+
14
48
  ## [0.8.0] - 2025-01-27
15
49
 
16
50
  ## Added
@@ -19,12 +53,14 @@
19
53
  - This serializer is more efficient and has a smaller memory footprint than the default serializer.
20
54
  - Ser2 still lacks some features, such as weaving of native stacks.
21
55
 
56
+
22
57
  ## [0.7.1] - 2025-01-02
23
58
 
24
59
  ### Fixed
25
60
 
26
61
  - Reverted Cargo.lock version to 3 to support older versions of Rust (<1.78).
27
62
 
63
+
28
64
  ## [0.7.0] - 2025-01-03
29
65
 
30
66
  ### Changed
data/README.md CHANGED
@@ -3,6 +3,9 @@ Pf2
3
3
 
4
4
  A experimental sampling-based profiler for Ruby 3.3+.
5
5
 
6
+ - GitHub: https://github.com/osyoyu/pf2
7
+ - Documentation: https://osyoyu.github.io/pf2/
8
+
6
9
  Notable Capabilites
7
10
  --------
8
11
 
@@ -13,6 +16,25 @@ Notable Capabilites
13
16
  Usage
14
17
  --------
15
18
 
19
+ ### Installation
20
+
21
+ You will need a C compiler to build the native extension.
22
+
23
+ Add this line to your application's Gemfile:
24
+
25
+ ```ruby
26
+ gem 'pf2'
27
+
28
+ # When using the main branch, specify submodules: true
29
+ gem 'pf2', git: 'https://github.com/osyoyu/pf2.git', submodules: true
30
+ ```
31
+
32
+ Pf2 can be installed as a standalone CLI tool as well.
33
+
34
+ ```console
35
+ gem install pf2
36
+ ```
37
+
16
38
  ### Quickstart
17
39
 
18
40
  Run your Ruby program through `pf2 serve`.
@@ -116,7 +138,7 @@ Schedulers determine when to execute sample collection, based on configuration (
116
138
 
117
139
  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
140
 
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.
141
+ 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
142
 
121
143
  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
144
 
@@ -128,11 +150,16 @@ Another scheduler is the `TimerThreadScheduler`, which maintains a time-keeping
128
150
 
129
151
  This scheduler is wall-time only, and does not support CPU-time based profiling.
130
152
 
131
- Future Plans
153
+ #### macOS Support
154
+
155
+ On platforms where `timer_create()` is not supported (namely macOS), Pf2 falls back to `setitimer()`.
156
+
157
+
158
+ Wishlist
132
159
  --------
133
160
 
134
- - Remove known limitations, if possible
135
- - Implement a "tracing" scheduler, using the C TracePoint API
161
+ - [Flame Scopes](https://www.brendangregg.com/flamescope.html)
162
+ - More unit/e2e tests
136
163
  - more
137
164
 
138
165
  Development
data/Rakefile CHANGED
@@ -1,18 +1,13 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rake/extensiontask'
3
3
  require 'minitest/test_task'
4
+ require 'rdoc/task'
4
5
 
5
6
  task default: %i[]
6
7
 
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
8
+ Rake::ExtensionTask.new 'pf2' do |ext|
9
+ ext.name = 'pf2'
10
+ ext.lib_dir = 'lib/pf2'
16
11
  end
17
12
 
18
13
  Minitest::TestTask.create(:test) do |t|
@@ -21,3 +16,7 @@ Minitest::TestTask.create(:test) do |t|
21
16
  t.warning = false
22
17
  t.test_globs = ["test/**/*_test.rb"]
23
18
  end
19
+
20
+ RDoc::Task.new do |doc|
21
+ doc.rdoc_dir = "_site" # for GitHub pages
22
+ end
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
data/ext/pf2/pf2.c ADDED
@@ -0,0 +1,17 @@
1
+ #include <ruby.h>
2
+
3
+ #include "session.h"
4
+
5
+ VALUE rb_mPf2;
6
+
7
+ RUBY_FUNC_EXPORTED void
8
+ Init_pf2(void)
9
+ {
10
+ rb_mPf2 = rb_define_module("Pf2");
11
+ VALUE rb_mPf2_cSession = rb_define_class_under(rb_mPf2, "Session", rb_cObject);
12
+ rb_define_alloc_func(rb_mPf2_cSession, pf2_session_alloc);
13
+ rb_define_method(rb_mPf2_cSession, "initialize", rb_pf2_session_initialize, -1);
14
+ rb_define_method(rb_mPf2_cSession, "start", rb_pf2_session_start, 0);
15
+ rb_define_method(rb_mPf2_cSession, "stop", rb_pf2_session_stop, 0);
16
+ rb_define_method(rb_mPf2_cSession, "configuration", rb_pf2_session_configuration, 0);
17
+ }
@@ -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>
@@ -8,8 +9,6 @@
8
9
  #include "backtrace_state.h"
9
10
  #include "sample.h"
10
11
 
11
- const int PF2_SAMPLE_MAX_NATIVE_DEPTH = 300;
12
-
13
12
  static int capture_native_backtrace(struct pf2_sample *sample);
14
13
  static int backtrace_on_ok(void *data, uintptr_t pc);
15
14
 
@@ -17,13 +16,18 @@ static int backtrace_on_ok(void *data, uintptr_t pc);
17
16
  bool
18
17
  pf2_sample_capture(struct pf2_sample *sample)
19
18
  {
19
+ // Initialize sample
20
+ memset(sample, 0, sizeof(struct pf2_sample));
21
+
20
22
  // Record the current time
21
23
  struct timespec now;
22
24
  clock_gettime(CLOCK_MONOTONIC, &now);
23
25
  sample->timestamp_ns = (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec;
24
26
 
27
+ sample->context_pthread = pthread_self();
28
+
25
29
  // Obtain the current stack from Ruby
26
- sample->depth = rb_profile_frames(0, 200, sample->cmes, sample->linenos);
30
+ sample->depth = rb_profile_frames(0, PF2_SAMPLE_MAX_RUBY_DEPTH, sample->cmes, sample->linenos);
27
31
 
28
32
  // Capture C-level backtrace
29
33
  sample->native_stack_depth = capture_native_backtrace(sample);
data/ext/pf2/sample.h ADDED
@@ -0,0 +1,27 @@
1
+ #ifndef PF2_SAMPLE_H
2
+ #define PF2_SAMPLE_H
3
+
4
+ #include <pthread.h>
5
+
6
+ #include <ruby.h>
7
+
8
+ #define PF2_SAMPLE_MAX_RUBY_DEPTH 200
9
+ #define PF2_SAMPLE_MAX_NATIVE_DEPTH 300
10
+
11
+ struct pf2_sample {
12
+ pthread_t context_pthread;
13
+
14
+ int depth;
15
+ VALUE cmes[PF2_SAMPLE_MAX_RUBY_DEPTH];
16
+ int linenos[PF2_SAMPLE_MAX_RUBY_DEPTH];
17
+
18
+ size_t native_stack_depth;
19
+ uintptr_t native_stack[PF2_SAMPLE_MAX_NATIVE_DEPTH];
20
+
21
+ uint64_t consumed_time_ns;
22
+ uint64_t timestamp_ns;
23
+ };
24
+
25
+ bool pf2_sample_capture(struct pf2_sample *sample);
26
+
27
+ #endif // PF2_SAMPLE_H
@@ -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
@@ -261,19 +318,32 @@ pf2_session_alloc(VALUE self)
261
318
  rb_raise(rb_eNoMemError, "Failed to allocate memory");
262
319
  }
263
320
 
321
+ // is_running
322
+ session->is_running = false;
323
+
324
+ // timer
325
+ #ifdef HAVE_TIMER_CREATE
326
+ session->timer = (timer_t)0;
327
+ #else
328
+ session->timer = (struct itimerval){0};
329
+ #endif
330
+
331
+ // rbuf
264
332
  session->rbuf = pf2_ringbuffer_new(1000);
265
333
  if (session->rbuf == NULL) {
266
334
  rb_raise(rb_eNoMemError, "Failed to allocate memory");
267
335
  }
268
336
 
337
+ // is_marking
269
338
  atomic_store_explicit(&session->is_marking, false, memory_order_relaxed);
339
+
340
+ // collector_thread
270
341
  session->collector_thread = malloc(sizeof(pthread_t));
271
342
  if (session->collector_thread == NULL) {
272
343
  rb_raise(rb_eNoMemError, "Failed to allocate memory");
273
344
  }
274
345
 
275
- session->duration_ns = 0;
276
-
346
+ // samples, samples_index, samples_capacity
277
347
  session->samples_index = 0;
278
348
  session->samples_capacity = 500; // 10 seconds worth of samples at 50 Hz
279
349
  session->samples = malloc(sizeof(struct pf2_sample) * session->samples_capacity);
@@ -281,6 +351,14 @@ pf2_session_alloc(VALUE self)
281
351
  rb_raise(rb_eNoMemError, "Failed to allocate memory");
282
352
  }
283
353
 
354
+ // start_time_realtime, start_time
355
+ session->start_time_realtime = (struct timespec){0};
356
+ session->start_time = (struct timespec){0};
357
+
358
+ // duration_ns
359
+ session->duration_ns = 0;
360
+
361
+ // configuration
284
362
  session->configuration = NULL;
285
363
 
286
364
  return TypedData_Wrap_Struct(self, &pf2_session_type, session);
@@ -323,8 +401,15 @@ pf2_session_dmark(void *sess)
323
401
  void
324
402
  pf2_session_dfree(void *sess)
325
403
  {
326
- // TODO: Ensure the uninstall process is complete before freeing the session
327
404
  struct pf2_session *session = sess;
405
+
406
+ assert(session->is_running == false || session->is_running == true);
407
+
408
+ // Stop the session if it's still running
409
+ if (session->is_running) {
410
+ pf2_session_stop(session);
411
+ }
412
+
328
413
  pf2_configuration_free(session->configuration);
329
414
  pf2_ringbuffer_free(session->rbuf);
330
415
  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;
@@ -38,14 +43,14 @@ void pf2_session_dfree(void *sess);
38
43
  size_t pf2_session_dsize(const void *sess);
39
44
 
40
45
  static const rb_data_type_t pf2_session_type = {
41
- .wrap_struct_name = "Pf2c::Session",
46
+ .wrap_struct_name = "Pf2::Session",
42
47
  .function = {
43
48
  .dmark = pf2_session_dmark,
44
49
  .dfree = pf2_session_dfree,
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)