pf2 0.8.0 → 1.0.0.alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +20 -4
  4. data/Rakefile +1 -0
  5. data/doc/development.md +17 -0
  6. data/examples/mandelbrot.rb +69 -0
  7. data/examples/mandelbrot_ractor.rb +77 -0
  8. data/ext/pf2/backtrace_state.c +10 -0
  9. data/ext/pf2/backtrace_state.h +10 -0
  10. data/ext/pf2/configuration.c +90 -0
  11. data/ext/pf2/configuration.h +23 -0
  12. data/ext/pf2/debug.h +12 -0
  13. data/ext/pf2/extconf.rb +23 -6
  14. data/ext/pf2/pf2.c +17 -0
  15. data/ext/pf2/pf2.h +8 -0
  16. data/ext/pf2/ringbuffer.c +74 -0
  17. data/ext/pf2/ringbuffer.h +24 -0
  18. data/ext/pf2/sample.c +76 -0
  19. data/ext/pf2/sample.h +26 -0
  20. data/ext/pf2/serializer.c +377 -0
  21. data/ext/pf2/serializer.h +58 -0
  22. data/ext/pf2/session.c +394 -0
  23. data/ext/pf2/session.h +56 -0
  24. data/lib/pf2/cli.rb +25 -11
  25. data/lib/pf2/reporter/annotate.rb +101 -0
  26. data/lib/pf2/reporter/firefox_profiler_ser2.rb +17 -13
  27. data/lib/pf2/reporter/stack_weaver.rb +8 -0
  28. data/lib/pf2/reporter.rb +1 -1
  29. data/lib/pf2/version.rb +1 -1
  30. data/lib/pf2.rb +1 -1
  31. data/vendor/libbacktrace/.gitignore +5 -0
  32. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/README.md +1 -1
  33. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/configure +23 -0
  34. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/configure.ac +10 -0
  35. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/dwarf.c +199 -15
  36. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/elf.c +20 -14
  37. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/fileline.c +2 -2
  38. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/macho.c +2 -2
  39. data/{crates/backtrace-sys2/src → vendor}/libbacktrace/pecoff.c +2 -2
  40. metadata +111 -111
  41. data/Cargo.lock +0 -630
  42. data/Cargo.toml +0 -3
  43. data/crates/backtrace-sys2/.gitignore +0 -1
  44. data/crates/backtrace-sys2/Cargo.toml +0 -9
  45. data/crates/backtrace-sys2/build.rs +0 -45
  46. data/crates/backtrace-sys2/src/lib.rs +0 -5
  47. data/crates/backtrace-sys2/src/libbacktrace/.gitignore +0 -15
  48. data/ext/pf2/Cargo.toml +0 -25
  49. data/ext/pf2/build.rs +0 -10
  50. data/ext/pf2/src/backtrace.rs +0 -127
  51. data/ext/pf2/src/lib.rs +0 -22
  52. data/ext/pf2/src/profile.rs +0 -69
  53. data/ext/pf2/src/profile_serializer.rs +0 -241
  54. data/ext/pf2/src/ringbuffer.rs +0 -150
  55. data/ext/pf2/src/ruby_c_api_helper.c +0 -6
  56. data/ext/pf2/src/ruby_init.rs +0 -40
  57. data/ext/pf2/src/ruby_internal_apis.rs +0 -77
  58. data/ext/pf2/src/sample.rs +0 -67
  59. data/ext/pf2/src/scheduler.rs +0 -10
  60. data/ext/pf2/src/serialization/profile.rs +0 -48
  61. data/ext/pf2/src/serialization/serializer.rs +0 -329
  62. data/ext/pf2/src/serialization.rs +0 -2
  63. data/ext/pf2/src/session/configuration.rs +0 -114
  64. data/ext/pf2/src/session/new_thread_watcher.rs +0 -80
  65. data/ext/pf2/src/session/ruby_object.rs +0 -90
  66. data/ext/pf2/src/session.rs +0 -248
  67. data/ext/pf2/src/siginfo_t.c +0 -5
  68. data/ext/pf2/src/signal_scheduler.rs +0 -201
  69. data/ext/pf2/src/signal_scheduler_unsupported_platform.rs +0 -39
  70. data/ext/pf2/src/timer_thread_scheduler.rs +0 -179
  71. data/ext/pf2/src/util.rs +0 -31
  72. data/lib/pf2/reporter/firefox_profiler.rb +0 -397
  73. data/rust-toolchain.toml +0 -2
  74. data/rustfmt.toml +0 -1
  75. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/Isaac.Newton-Opticks.txt +0 -0
  76. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/LICENSE +0 -0
  77. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/Makefile.am +0 -0
  78. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/Makefile.in +0 -0
  79. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/aclocal.m4 +0 -0
  80. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/alloc.c +0 -0
  81. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/allocfail.c +0 -0
  82. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/allocfail.sh +0 -0
  83. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/atomic.c +0 -0
  84. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/backtrace-supported.h.in +0 -0
  85. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/backtrace.c +0 -0
  86. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/backtrace.h +0 -0
  87. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/btest.c +0 -0
  88. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/compile +0 -0
  89. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/enable.m4 +0 -0
  90. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/lead-dot.m4 +0 -0
  91. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/libtool.m4 +0 -0
  92. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/ltoptions.m4 +0 -0
  93. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/ltsugar.m4 +0 -0
  94. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/ltversion.m4 +0 -0
  95. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/lt~obsolete.m4 +0 -0
  96. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/multi.m4 +0 -0
  97. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/override.m4 +0 -0
  98. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/unwind_ipinfo.m4 +0 -0
  99. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/warnings.m4 +0 -0
  100. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config.guess +0 -0
  101. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config.h.in +0 -0
  102. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config.sub +0 -0
  103. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/edtest.c +0 -0
  104. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/edtest2.c +0 -0
  105. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/filenames.h +0 -0
  106. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/filetype.awk +0 -0
  107. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/install-debuginfo-for-buildid.sh.in +0 -0
  108. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/install-sh +0 -0
  109. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/instrumented_alloc.c +0 -0
  110. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/internal.h +0 -0
  111. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/ltmain.sh +0 -0
  112. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/missing +0 -0
  113. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/mmap.c +0 -0
  114. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/mmapio.c +0 -0
  115. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/move-if-change +0 -0
  116. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/mtest.c +0 -0
  117. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/nounwind.c +0 -0
  118. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/posix.c +0 -0
  119. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/print.c +0 -0
  120. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/read.c +0 -0
  121. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/simple.c +0 -0
  122. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/sort.c +0 -0
  123. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/state.c +0 -0
  124. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/stest.c +0 -0
  125. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/test-driver +0 -0
  126. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/test_format.c +0 -0
  127. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/testlib.c +0 -0
  128. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/testlib.h +0 -0
  129. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/ttest.c +0 -0
  130. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/unittest.c +0 -0
  131. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/unknown.c +0 -0
  132. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/xcoff.c +0 -0
  133. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/xztest.c +0 -0
  134. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/zstdtest.c +0 -0
  135. /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/ztest.c +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5c67ca63f5a380dd45c2a95dc089f433ff9f8f82c6faf69b9cece4364e1c372
4
- data.tar.gz: a07f99f52fd9ccff04d031f6a6d195f44d17c11a6fc885a47a92190503c0cac8
3
+ metadata.gz: 9f219b8aa5b5281a6ed662a085e00f82636ab52365d2b0f7a8d61a3e06bfc9c6
4
+ data.tar.gz: 162e1eae488afe17e33291f63f807642aa304d7df8069cb165e3a25ce6f10895
5
5
  SHA512:
6
- metadata.gz: 1a3f015a08e3c16d44263d8fe413a755b0e3a3c151fbc0144eeb120670b76ac7bcfb297ce3a6284c283bac158b1c7d1892d48af867fc4e52a2126944e71c0f57
7
- data.tar.gz: e4a3c4198895216ac1a2e706e3732d8c086683eeee3b08a484d28de367d28b3d43fc507909318713cd8e67429d6eb831fd9cff71060966c0383716a8699fb0f2
6
+ metadata.gz: 37b6a1aa4f6ab0753d86983d76cbdfae4b3dfbb7464afb0388aa113d728725f09eabcabb2ae7c2ffadc5ab797be75675b5b27b465c20565d5d69f30291d4837f
7
+ data.tar.gz: 7b69aea55c8873cfd6e28fba3bdddd09325e4c73e1e3e44a5be48fcb9ad776b03e811c707fc049ad8ccc1590e7c011032be7f4053e3d15a5ba5b50c4344db656
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.9.0] - 2025-03-22
4
+
5
+ ## Added
6
+
7
+ - `pf2 annotate` command
8
+ - A new sample collection backend implemented in C
9
+
10
+ ## Changed
11
+
12
+ - Set SA_RESTART flag to reduce EINTRs in profiled code
13
+
3
14
  ## [0.8.0] - 2025-01-27
4
15
 
5
16
  ## Added
data/README.md CHANGED
@@ -61,6 +61,12 @@ Profiles can be visualized using the [Firefox Profiler](https://profiler.firefox
61
61
  $ pf2 report -o report.json my_program.pf2profile
62
62
  ```
63
63
 
64
+ Alternatively, `pf2 annotate` can be used to display hit counts side-by-side with source code.
65
+
66
+ ```console
67
+ $ pf2 annotate my_program.pf2prof
68
+ ```
69
+
64
70
  ### Configuration
65
71
 
66
72
  Pf2 accepts the following configuration keys:
@@ -110,7 +116,7 @@ Schedulers determine when to execute sample collection, based on configuration (
110
116
 
111
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.
112
118
 
113
- 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.
114
120
 
115
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.
116
122
 
@@ -122,13 +128,23 @@ Another scheduler is the `TimerThreadScheduler`, which maintains a time-keeping
122
128
 
123
129
  This scheduler is wall-time only, and does not support CPU-time based profiling.
124
130
 
125
- 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
126
137
  --------
127
138
 
128
- - Remove known limitations, if possible
129
- - Implement a "tracing" scheduler, using the C TracePoint API
139
+ - [Flame Scopes](https://www.brendangregg.com/flamescope.html)
140
+ - More unit/e2e tests
130
141
  - more
131
142
 
143
+ Development
144
+ --------
145
+
146
+ See [doc/development.md](doc/development.md).
147
+
132
148
 
133
149
  License
134
150
  --------
data/Rakefile CHANGED
@@ -5,6 +5,7 @@ require 'minitest/test_task'
5
5
  task default: %i[]
6
6
 
7
7
  Rake::ExtensionTask.new 'pf2' do |ext|
8
+ ext.name = 'pf2'
8
9
  ext.lib_dir = 'lib/pf2'
9
10
  end
10
11
 
@@ -0,0 +1,17 @@
1
+ Pf2 Development
2
+ ===========
3
+
4
+ Setup
5
+ --------
6
+
7
+ - `git submodule update --init`
8
+
9
+
10
+ Releasing
11
+ --------
12
+
13
+ - Update CHANGELOG.md
14
+ - Update version in lib/pf2/version.rb
15
+ - Run `bundle install` to update Gemfile.lock
16
+ - Commit changes
17
+ - Run `bundle exec rake release`
@@ -0,0 +1,69 @@
1
+ # mandelbrot
2
+ #
3
+ # Generate a Mandelbrot set image using multiple threads.
4
+
5
+ require 'bundler/inline'
6
+
7
+ gemfile do
8
+ source 'https://rubygems.org'
9
+ gem 'chunky_png'
10
+ end
11
+
12
+ require 'pf2'
13
+
14
+ def mandelbrot_pixel(x, y, width, height, max_iter)
15
+ real_part = (x - width / 2.0) * 4.0 / width
16
+ imag_part = (y - height / 2.0) * 4.0 / height
17
+
18
+ c = Complex(real_part, imag_part)
19
+ z = 0
20
+ iter = 0
21
+
22
+ while iter < max_iter && z.magnitude <= 2
23
+ z = z * z + c
24
+ iter += 1
25
+ end
26
+
27
+ iter
28
+ end
29
+
30
+ def generate_mandelbrot_image(width, height, max_iter, num_threads)
31
+ image = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT)
32
+ threads = []
33
+ num_threads.times do |thread_id|
34
+ threads << Thread.new(thread_id) do |tid|
35
+ start_row = tid * (height / num_threads)
36
+ end_row = (tid + 1) * (height / num_threads)
37
+
38
+ (start_row...end_row).each do |y|
39
+ width.times do |x|
40
+ color_value = mandelbrot_pixel(x, y, width, height, max_iter)
41
+ color = ChunkyPNG::Color.grayscale(color_value * 255 / max_iter)
42
+ image[x, y] = color
43
+ end
44
+ end
45
+ end
46
+ end
47
+ threads.each(&:join)
48
+ image
49
+ end
50
+
51
+ # Parameters
52
+ width = 800
53
+ height = 800
54
+ max_iter = 1000
55
+ threads = 16
56
+
57
+ puts "width: #{width}, height: #{height}, max_iter: #{max_iter}, threads: #{threads}"
58
+
59
+ Pf2.start
60
+
61
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
62
+ generate_mandelbrot_image(width, height, max_iter, threads)
63
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
64
+
65
+ profile = Pf2.stop
66
+ File.binwrite("mandelbrot.pf2prof", Marshal.dump(profile))
67
+
68
+ elapsed = end_time - start_time
69
+ puts "Complete in #{elapsed} seconds"
@@ -0,0 +1,77 @@
1
+ # mandelbrot_ractor
2
+ #
3
+ # This script demonstrates how to profile a Ruby program that uses Ractors.
4
+
5
+ require 'bundler/inline'
6
+
7
+ gemfile do
8
+ source 'https://rubygems.org'
9
+ gem 'chunky_png'
10
+ end
11
+
12
+ def mandelbrot_pixel(x, y, width, height, max_iter)
13
+ real_part = (x - width / 2.0) * 4.0 / width
14
+ imag_part = (y - height / 2.0) * 4.0 / height
15
+
16
+ c = Complex(real_part, imag_part)
17
+ z = 0
18
+ iter = 0
19
+
20
+ while iter < max_iter && z.magnitude <= 2
21
+ z = z * z + c
22
+ iter += 1
23
+ end
24
+
25
+ iter
26
+ end
27
+
28
+ def generate_mandelbrot_image(width, height, max_iter, num_ractors)
29
+ ractors = []
30
+ num_ractors.times do |ractor_id|
31
+ ractors << Ractor.new(width, height, max_iter, num_ractors, ractor_id) do |width, height, max_iter, num_ractors, rid|
32
+ image = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT)
33
+
34
+ start_row = rid * (height / num_ractors)
35
+ end_row = (rid + 1) * (height / num_ractors)
36
+
37
+ (start_row...end_row).each do |y|
38
+ width.times do |x|
39
+ color_value = mandelbrot_pixel(x, y, width, height, max_iter)
40
+ color = ChunkyPNG::Color.grayscale(color_value * 255 / max_iter)
41
+ image[x, y] = color
42
+ end
43
+ end
44
+
45
+ Ractor.yield image
46
+ end
47
+ end
48
+ image_parts = ractors.map(&:take)
49
+
50
+ # Merge image_parts into a single image
51
+ image = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT)
52
+ image_parts.each do |image_part|
53
+ image_part.height.times do |y|
54
+ image_part.width.times do |x|
55
+ if !image_part[x, y].nil?
56
+ image[x, y] = image_part[x, y]
57
+ end
58
+ end
59
+ end
60
+ end
61
+ image
62
+ end
63
+
64
+ # Parameters
65
+ width = 800
66
+ height = 800
67
+ max_iter = 1000
68
+ ractors = 4
69
+
70
+ puts "width: #{width}, height: #{height}, max_iter: #{max_iter}, ractors: #{ractors}"
71
+
72
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
73
+ generate_mandelbrot_image(width, height, max_iter, ractors)
74
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
75
+
76
+ elapsed = end_time - start_time
77
+ puts "Complete in #{elapsed} seconds"
@@ -0,0 +1,10 @@
1
+ #include <stdio.h>
2
+ #include "backtrace_state.h"
3
+
4
+ struct backtrace_state *global_backtrace_state = NULL;
5
+
6
+ void
7
+ pf2_backtrace_print_error(void *data, const char *msg, int errnum)
8
+ {
9
+ printf("libbacktrace error callback: %s (errnum %d)\n", msg, errnum);
10
+ }
@@ -0,0 +1,10 @@
1
+ #ifndef PF2_BACKTRACE_H
2
+ #define PF2_BACKTRACE_H
3
+
4
+ #include <backtrace.h>
5
+
6
+ extern struct backtrace_state *global_backtrace_state;
7
+
8
+ void pf2_backtrace_print_error(void *data, const char *msg, int errnum);
9
+
10
+ #endif // PF2_BACKTRACE_H
@@ -0,0 +1,90 @@
1
+ #include <ruby.h>
2
+ #include <stdlib.h>
3
+
4
+ #include "configuration.h"
5
+
6
+ static int extract_interval_ms(VALUE options_hash);
7
+ static enum pf2_time_mode extract_time_mode(VALUE options_hash);
8
+
9
+ struct pf2_configuration *
10
+ pf2_configuration_new_from_options_hash(VALUE options_hash)
11
+ {
12
+ struct pf2_configuration *config = malloc(sizeof(struct pf2_configuration));
13
+ if (!config) {
14
+ rb_raise(rb_eRuntimeError, "Failed to allocate configuration");
15
+ }
16
+
17
+ config->interval_ms = extract_interval_ms(options_hash);
18
+ config->time_mode = extract_time_mode(options_hash);
19
+
20
+ return config;
21
+ }
22
+
23
+ static int
24
+ extract_interval_ms(VALUE options_hash)
25
+ {
26
+ if (options_hash == Qnil) {
27
+ return PF2_DEFAULT_INTERVAL_MS;
28
+ }
29
+
30
+ VALUE interval_ms = rb_hash_aref(options_hash, ID2SYM(rb_intern("interval_ms")));
31
+ if (interval_ms == Qundef || interval_ms == Qnil) {
32
+ return PF2_DEFAULT_INTERVAL_MS;
33
+ }
34
+
35
+ return NUM2INT(interval_ms);
36
+ }
37
+
38
+ static enum pf2_time_mode
39
+ extract_time_mode(VALUE options_hash)
40
+ {
41
+ if (options_hash == Qnil) {
42
+ return PF2_DEFAULT_TIME_MODE;
43
+ }
44
+
45
+ VALUE time_mode = rb_hash_aref(options_hash, ID2SYM(rb_intern("time_mode")));
46
+ if (time_mode == Qundef || time_mode == Qnil) {
47
+ return PF2_DEFAULT_TIME_MODE;
48
+ }
49
+
50
+ if (time_mode == ID2SYM(rb_intern("cpu"))) {
51
+ return PF2_TIME_MODE_CPU_TIME;
52
+ } else if (time_mode == ID2SYM(rb_intern("wall"))) {
53
+ return PF2_TIME_MODE_WALL_TIME;
54
+ } else {
55
+ VALUE time_mode_str = rb_obj_as_string(time_mode);
56
+ rb_raise(rb_eArgError, "Invalid time mode: %s", StringValueCStr(time_mode_str));
57
+ }
58
+ }
59
+
60
+ void
61
+ pf2_configuration_free(struct pf2_configuration *config)
62
+ {
63
+ free(config);
64
+ }
65
+
66
+ VALUE
67
+ pf2_configuration_to_ruby_hash(struct pf2_configuration *config)
68
+ {
69
+ VALUE hash = rb_hash_new();
70
+
71
+ // interval_ms
72
+ rb_hash_aset(hash, ID2SYM(rb_intern("interval_ms")), INT2NUM(config->interval_ms));
73
+
74
+ // time_mode
75
+ VALUE time_mode_sym;
76
+ switch (config->time_mode) {
77
+ case PF2_TIME_MODE_CPU_TIME:
78
+ time_mode_sym = ID2SYM(rb_intern("cpu"));
79
+ break;
80
+ case PF2_TIME_MODE_WALL_TIME:
81
+ time_mode_sym = ID2SYM(rb_intern("wall"));
82
+ break;
83
+ default:
84
+ rb_raise(rb_eRuntimeError, "Invalid time mode");
85
+ break;
86
+ }
87
+ rb_hash_aset(hash, ID2SYM(rb_intern("time_mode")), time_mode_sym);
88
+
89
+ return hash;
90
+ }
@@ -0,0 +1,23 @@
1
+ #ifndef PF2_CONFIGURATION_H
2
+ #define PF2_CONFIGURATION_H
3
+
4
+ #include <ruby.h>
5
+
6
+ enum pf2_time_mode {
7
+ PF2_TIME_MODE_CPU_TIME,
8
+ PF2_TIME_MODE_WALL_TIME,
9
+ };
10
+
11
+ struct pf2_configuration {
12
+ int interval_ms;
13
+ enum pf2_time_mode time_mode;
14
+ };
15
+
16
+ #define PF2_DEFAULT_INTERVAL_MS 9
17
+ #define PF2_DEFAULT_TIME_MODE PF2_TIME_MODE_CPU_TIME
18
+
19
+ struct pf2_configuration *pf2_configuration_new_from_options_hash(VALUE options_hash);
20
+ void pf2_configuration_free(struct pf2_configuration *config);
21
+ VALUE pf2_configuration_to_ruby_hash(struct pf2_configuration *config);
22
+
23
+ #endif // PF2_CONFIGURATION_H
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_mPf2c;
6
+
7
+ RUBY_FUNC_EXPORTED void
8
+ Init_pf2(void)
9
+ {
10
+ rb_mPf2c = rb_define_module("Pf2c");
11
+ VALUE rb_mPf2c_cSession = rb_define_class_under(rb_mPf2c, "Session", rb_cObject);
12
+ rb_define_alloc_func(rb_mPf2c_cSession, pf2_session_alloc);
13
+ rb_define_method(rb_mPf2c_cSession, "initialize", rb_pf2_session_initialize, -1);
14
+ rb_define_method(rb_mPf2c_cSession, "start", rb_pf2_session_start, 0);
15
+ rb_define_method(rb_mPf2c_cSession, "stop", rb_pf2_session_stop, 0);
16
+ rb_define_method(rb_mPf2c_cSession, "configuration", rb_pf2_session_configuration, 0);
17
+ }
data/ext/pf2/pf2.h ADDED
@@ -0,0 +1,8 @@
1
+ #ifndef PF2_H
2
+ #define PF2_H
3
+
4
+ #include <ruby.h>
5
+
6
+ RUBY_FUNC_EXPORTED void Init_pf2(void);
7
+
8
+ #endif // PF2_H
@@ -0,0 +1,74 @@
1
+ #include <stdbool.h>
2
+ #include <stdlib.h>
3
+
4
+ #include "ringbuffer.h"
5
+
6
+ struct pf2_ringbuffer *
7
+ pf2_ringbuffer_new(int size) {
8
+ if (size <= 0) { return NULL; }
9
+
10
+ struct pf2_ringbuffer *ringbuf = malloc(sizeof(struct pf2_ringbuffer));
11
+ if (!ringbuf) { goto err; }
12
+ ringbuf->size = size + 1; // One extra slot is required to distinguish full from empty
13
+ ringbuf->head = 0;
14
+ ringbuf->tail = 0;
15
+ ringbuf->samples = malloc(ringbuf->size * sizeof(struct pf2_sample));
16
+ if (!ringbuf->samples) { goto err_free_ringbuf; }
17
+ return ringbuf;
18
+
19
+ err_free_ringbuf:
20
+ free(ringbuf);
21
+ err:
22
+ return NULL;
23
+ }
24
+
25
+ void
26
+ pf2_ringbuffer_free(struct pf2_ringbuffer *ringbuf) {
27
+ free(ringbuf->samples);
28
+ free(ringbuf);
29
+ }
30
+
31
+ // Returns 0 on success, 1 on failure (buffer full).
32
+ bool
33
+ pf2_ringbuffer_push(struct pf2_ringbuffer *ringbuf, struct pf2_sample *sample) {
34
+ // Tail is only modified by the producer thread (us), so relaxed ordering is sufficient
35
+ const int current_tail = atomic_load_explicit(&ringbuf->tail, memory_order_relaxed);
36
+ const int next_tail = (current_tail + 1) % ringbuf->size;
37
+
38
+ // Check head to see if buffer is full. If next_tail == head, the buffer is full.
39
+ // Use acquire ordering to synchronize with the head update in pf2_ringbuffer_pop().
40
+ // This ensures we see the latest head value.
41
+ if (next_tail == atomic_load_explicit(&ringbuf->head, memory_order_acquire)) {
42
+ return false; // Buffer full
43
+ }
44
+
45
+ // Copy the sample from the provided input pointer to the buffer.
46
+ ringbuf->samples[current_tail] = *sample;
47
+
48
+ // Use release ordering when updating tail to ensure the sample write is visible
49
+ // to the consumer before they see the new tail value
50
+ atomic_store_explicit(&ringbuf->tail, next_tail, memory_order_release);
51
+ return true;
52
+ }
53
+
54
+ // Returns 0 on success, 1 on failure (buffer empty).
55
+ bool
56
+ pf2_ringbuffer_pop(struct pf2_ringbuffer *ringbuf, struct pf2_sample *out) {
57
+ // Head won't be modifed by the producer thread. It is safe to use relaxed ordering.
58
+ const int current_head = atomic_load_explicit(&ringbuf->head, memory_order_relaxed);
59
+
60
+ // Check tail to see if buffer is empty. If head == tail, the buffer is empty.
61
+ // Use acquire ordering to synchronize with the tail update in pf2_ringbuffer_push().
62
+ // This ensures we see the latest tail value.
63
+ if (current_head == atomic_load_explicit(&ringbuf->tail, memory_order_acquire)) {
64
+ return false; // Buffer empty
65
+ }
66
+
67
+ // Copy the sample from the buffer to the provided output pointer.
68
+ *out = ringbuf->samples[current_head];
69
+
70
+ // Use release ordering when updating head to ensure the sample read is complete
71
+ // before the producer sees the new head value
72
+ atomic_store_explicit(&ringbuf->head, (current_head + 1) % ringbuf->size, memory_order_release);
73
+ return true;
74
+ }
@@ -0,0 +1,24 @@
1
+ #ifndef PF2_RINGBUFFER_H
2
+ #define PF2_RINGBUFFER_H
3
+
4
+ #include <stdatomic.h>
5
+ #include <stdbool.h>
6
+
7
+ #include "sample.h"
8
+
9
+ // A lock-free ringbuffer for storing pf2_sample structs.
10
+ // Thread safe for single-producer single-consumer (SPSC) use.
11
+ struct pf2_ringbuffer {
12
+ int size;
13
+ atomic_int head;
14
+ atomic_int tail;
15
+ struct pf2_sample *samples;
16
+ };
17
+
18
+ struct pf2_ringbuffer * pf2_ringbuffer_new(int size);
19
+ void pf2_ringbuffer_free(struct pf2_ringbuffer *ringbuf);
20
+ // async-signal-safe
21
+ bool pf2_ringbuffer_push(struct pf2_ringbuffer *ringbuf, struct pf2_sample *sample);
22
+ bool pf2_ringbuffer_pop(struct pf2_ringbuffer *ringbuf, struct pf2_sample *out);
23
+
24
+ #endif // RINGBUFFER_H
data/ext/pf2/sample.c ADDED
@@ -0,0 +1,76 @@
1
+ #include <stdbool.h>
2
+ #include <time.h>
3
+ #include <pthread.h>
4
+
5
+ #include <backtrace.h>
6
+ #include <ruby.h>
7
+ #include <ruby/debug.h>
8
+
9
+ #include "backtrace_state.h"
10
+ #include "sample.h"
11
+
12
+ const int PF2_SAMPLE_MAX_NATIVE_DEPTH = 300;
13
+
14
+ static int capture_native_backtrace(struct pf2_sample *sample);
15
+ static int backtrace_on_ok(void *data, uintptr_t pc);
16
+
17
+ // Capture a sample from the current thread.
18
+ bool
19
+ pf2_sample_capture(struct pf2_sample *sample)
20
+ {
21
+ // Initialize sample
22
+ memset(sample, 0, sizeof(struct pf2_sample));
23
+
24
+ // Record the current time
25
+ struct timespec now;
26
+ clock_gettime(CLOCK_MONOTONIC, &now);
27
+ sample->timestamp_ns = (uint64_t)now.tv_sec * 1000000000ULL + (uint64_t)now.tv_nsec;
28
+
29
+ sample->context_pthread = pthread_self();
30
+
31
+ // Obtain the current stack from Ruby
32
+ sample->depth = rb_profile_frames(0, 200, sample->cmes, sample->linenos);
33
+
34
+ // Capture C-level backtrace
35
+ sample->native_stack_depth = capture_native_backtrace(sample);
36
+
37
+ return true;
38
+ }
39
+
40
+ // Struct to be passed to backtrace_on_ok
41
+ struct bt_data {
42
+ struct pf2_sample *pf2_sample;
43
+ int index;
44
+ };
45
+
46
+ static int
47
+ capture_native_backtrace(struct pf2_sample *sample)
48
+ {
49
+ struct backtrace_state *state = global_backtrace_state;
50
+ assert(state != NULL);
51
+
52
+ struct bt_data data;
53
+ data.pf2_sample = sample;
54
+ data.index = 0;
55
+
56
+ // Capture the current PC
57
+ // Skip the first 2 frames (capture_native_backtrace, sigprof_handler)
58
+ backtrace_simple(state, 2, backtrace_on_ok, pf2_backtrace_print_error, &data);
59
+
60
+ return data.index;
61
+ }
62
+
63
+ static int
64
+ backtrace_on_ok(void *data, uintptr_t pc)
65
+ {
66
+ struct bt_data *bt_data = (struct bt_data *)data;
67
+ struct pf2_sample *sample = bt_data->pf2_sample;
68
+
69
+ // Store the PC value
70
+ if (bt_data->index < PF2_SAMPLE_MAX_NATIVE_DEPTH) {
71
+ sample->native_stack[bt_data->index] = pc;
72
+ bt_data->index++;
73
+ }
74
+
75
+ return 0; // Continue backtrace
76
+ }
data/ext/pf2/sample.h ADDED
@@ -0,0 +1,26 @@
1
+ #ifndef PF2_SAMPLE_H
2
+ #define PF2_SAMPLE_H
3
+
4
+ #include <pthread.h>
5
+
6
+ #include <ruby.h>
7
+
8
+ extern const int PF2_SAMPLE_MAX_NATIVE_DEPTH;
9
+
10
+ struct pf2_sample {
11
+ pthread_t context_pthread;
12
+
13
+ int depth;
14
+ VALUE cmes[200];
15
+ int linenos[200];
16
+
17
+ size_t native_stack_depth;
18
+ uintptr_t native_stack[200];
19
+
20
+ uint64_t consumed_time_ns;
21
+ uint64_t timestamp_ns;
22
+ };
23
+
24
+ bool pf2_sample_capture(struct pf2_sample *sample);
25
+
26
+ #endif // PF2_SAMPLE_H