pf2 0.8.0 → 1.0.0.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +20 -4
- data/Rakefile +1 -0
- data/doc/development.md +17 -0
- data/examples/mandelbrot.rb +69 -0
- data/examples/mandelbrot_ractor.rb +77 -0
- data/ext/pf2/backtrace_state.c +10 -0
- data/ext/pf2/backtrace_state.h +10 -0
- data/ext/pf2/configuration.c +90 -0
- data/ext/pf2/configuration.h +23 -0
- data/ext/pf2/debug.h +12 -0
- data/ext/pf2/extconf.rb +23 -6
- data/ext/pf2/pf2.c +17 -0
- data/ext/pf2/pf2.h +8 -0
- data/ext/pf2/ringbuffer.c +74 -0
- data/ext/pf2/ringbuffer.h +24 -0
- data/ext/pf2/sample.c +76 -0
- data/ext/pf2/sample.h +26 -0
- data/ext/pf2/serializer.c +377 -0
- data/ext/pf2/serializer.h +58 -0
- data/ext/pf2/session.c +394 -0
- data/ext/pf2/session.h +56 -0
- data/lib/pf2/cli.rb +25 -11
- data/lib/pf2/reporter/annotate.rb +101 -0
- data/lib/pf2/reporter/firefox_profiler_ser2.rb +17 -13
- data/lib/pf2/reporter/stack_weaver.rb +8 -0
- data/lib/pf2/reporter.rb +1 -1
- data/lib/pf2/version.rb +1 -1
- data/lib/pf2.rb +1 -1
- data/vendor/libbacktrace/.gitignore +5 -0
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/README.md +1 -1
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/configure +23 -0
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/configure.ac +10 -0
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/dwarf.c +199 -15
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/elf.c +20 -14
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/fileline.c +2 -2
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/macho.c +2 -2
- data/{crates/backtrace-sys2/src → vendor}/libbacktrace/pecoff.c +2 -2
- metadata +111 -111
- data/Cargo.lock +0 -630
- data/Cargo.toml +0 -3
- data/crates/backtrace-sys2/.gitignore +0 -1
- data/crates/backtrace-sys2/Cargo.toml +0 -9
- data/crates/backtrace-sys2/build.rs +0 -45
- data/crates/backtrace-sys2/src/lib.rs +0 -5
- data/crates/backtrace-sys2/src/libbacktrace/.gitignore +0 -15
- data/ext/pf2/Cargo.toml +0 -25
- data/ext/pf2/build.rs +0 -10
- data/ext/pf2/src/backtrace.rs +0 -127
- data/ext/pf2/src/lib.rs +0 -22
- data/ext/pf2/src/profile.rs +0 -69
- data/ext/pf2/src/profile_serializer.rs +0 -241
- data/ext/pf2/src/ringbuffer.rs +0 -150
- data/ext/pf2/src/ruby_c_api_helper.c +0 -6
- data/ext/pf2/src/ruby_init.rs +0 -40
- data/ext/pf2/src/ruby_internal_apis.rs +0 -77
- data/ext/pf2/src/sample.rs +0 -67
- data/ext/pf2/src/scheduler.rs +0 -10
- data/ext/pf2/src/serialization/profile.rs +0 -48
- data/ext/pf2/src/serialization/serializer.rs +0 -329
- data/ext/pf2/src/serialization.rs +0 -2
- data/ext/pf2/src/session/configuration.rs +0 -114
- data/ext/pf2/src/session/new_thread_watcher.rs +0 -80
- data/ext/pf2/src/session/ruby_object.rs +0 -90
- data/ext/pf2/src/session.rs +0 -248
- data/ext/pf2/src/siginfo_t.c +0 -5
- data/ext/pf2/src/signal_scheduler.rs +0 -201
- data/ext/pf2/src/signal_scheduler_unsupported_platform.rs +0 -39
- data/ext/pf2/src/timer_thread_scheduler.rs +0 -179
- data/ext/pf2/src/util.rs +0 -31
- data/lib/pf2/reporter/firefox_profiler.rb +0 -397
- data/rust-toolchain.toml +0 -2
- data/rustfmt.toml +0 -1
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/Isaac.Newton-Opticks.txt +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/LICENSE +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/Makefile.am +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/Makefile.in +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/aclocal.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/alloc.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/allocfail.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/allocfail.sh +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/atomic.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/backtrace-supported.h.in +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/backtrace.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/backtrace.h +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/btest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/compile +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/enable.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/lead-dot.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/libtool.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/ltoptions.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/ltsugar.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/ltversion.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/lt~obsolete.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/multi.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/override.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/unwind_ipinfo.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config/warnings.m4 +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config.guess +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config.h.in +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/config.sub +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/edtest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/edtest2.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/filenames.h +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/filetype.awk +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/install-debuginfo-for-buildid.sh.in +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/install-sh +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/instrumented_alloc.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/internal.h +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/ltmain.sh +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/missing +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/mmap.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/mmapio.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/move-if-change +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/mtest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/nounwind.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/posix.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/print.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/read.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/simple.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/sort.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/state.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/stest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/test-driver +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/test_format.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/testlib.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/testlib.h +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/ttest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/unittest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/unknown.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/xcoff.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/xztest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/zstdtest.c +0 -0
- /data/{crates/backtrace-sys2/src → vendor}/libbacktrace/ztest.c +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f219b8aa5b5281a6ed662a085e00f82636ab52365d2b0f7a8d61a3e06bfc9c6
|
4
|
+
data.tar.gz: 162e1eae488afe17e33291f63f807642aa304d7df8069cb165e3a25ce6f10895
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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
|
-
-
|
129
|
-
-
|
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
data/doc/development.md
ADDED
@@ -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,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
data/ext/pf2/extconf.rb
CHANGED
@@ -1,10 +1,27 @@
|
|
1
1
|
require 'mkmf'
|
2
|
-
require '
|
2
|
+
require 'mini_portile2'
|
3
3
|
|
4
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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,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
|