pf2 0.3.0 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/Cargo.lock +2 -2
- data/README.md +16 -3
- data/ext/pf2/src/backtrace.rs +1 -0
- data/ext/pf2/src/lib.rs +4 -0
- data/ext/pf2/src/profile_serializer.rs +77 -33
- data/ext/pf2/src/ruby_init.rs +9 -40
- data/ext/pf2/src/ruby_internal_apis.rs +70 -0
- data/ext/pf2/src/scheduler.rs +10 -0
- data/ext/pf2/src/session/configuration.rs +106 -0
- data/ext/pf2/src/session/new_thread_watcher.rs +80 -0
- data/ext/pf2/src/session/ruby_object.rs +90 -0
- data/ext/pf2/src/session.rs +227 -0
- data/ext/pf2/src/signal_scheduler.rs +105 -221
- data/ext/pf2/src/timer_thread_scheduler.rs +92 -240
- data/lib/pf2/cli.rb +69 -7
- data/lib/pf2/reporter.rb +105 -4
- data/lib/pf2/serve.rb +60 -0
- data/lib/pf2/session.rb +7 -0
- data/lib/pf2/version.rb +1 -1
- data/lib/pf2.rb +7 -14
- metadata +24 -8
- data/ext/pf2/src/signal_scheduler/configuration.rs +0 -31
- data/ext/pf2/src/signal_scheduler/timer_installer.rs +0 -199
data/lib/pf2/session.rb
ADDED
data/lib/pf2/version.rb
CHANGED
data/lib/pf2.rb
CHANGED
@@ -1,31 +1,24 @@
|
|
1
1
|
require_relative 'pf2/pf2'
|
2
|
+
require_relative 'pf2/session'
|
2
3
|
require_relative 'pf2/version'
|
3
4
|
|
4
5
|
module Pf2
|
5
6
|
class Error < StandardError; end
|
6
7
|
|
7
|
-
def self.default_scheduler_class
|
8
|
-
# SignalScheduler is Linux-only. Use TimerThreadScheduler on other platforms.
|
9
|
-
if defined?(SignalScheduler)
|
10
|
-
SignalScheduler
|
11
|
-
else
|
12
|
-
TimerThreadScheduler
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
8
|
def self.start(...)
|
17
|
-
@@
|
18
|
-
@@
|
9
|
+
@@session = Pf2::Session.new(...)
|
10
|
+
@@session.start
|
19
11
|
end
|
20
12
|
|
21
|
-
def self.stop
|
22
|
-
@@
|
13
|
+
def self.stop
|
14
|
+
@@session.stop
|
23
15
|
end
|
24
16
|
|
25
17
|
def self.profile(&block)
|
26
18
|
raise ArgumentError, "block required" unless block_given?
|
27
|
-
start(
|
19
|
+
start(threads: Thread.list)
|
28
20
|
yield
|
29
21
|
stop
|
22
|
+
@@session = nil # let GC clean up the session
|
30
23
|
end
|
31
24
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pf2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daisuke Aritomo
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date: 2024-
|
10
|
+
date: 2024-03-24 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: rake-compiler
|
@@ -38,6 +37,20 @@ dependencies:
|
|
38
37
|
- - "~>"
|
39
38
|
- !ruby/object:Gem::Version
|
40
39
|
version: 0.9.63
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: webrick
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
41
54
|
- !ruby/object:Gem::Dependency
|
42
55
|
name: minitest
|
43
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,7 +65,6 @@ dependencies:
|
|
52
65
|
- - ">="
|
53
66
|
- !ruby/object:Gem::Version
|
54
67
|
version: '0'
|
55
|
-
description:
|
56
68
|
email:
|
57
69
|
- osyoyu@osyoyu.com
|
58
70
|
executables:
|
@@ -156,16 +168,22 @@ files:
|
|
156
168
|
- ext/pf2/src/profile_serializer.rs
|
157
169
|
- ext/pf2/src/ringbuffer.rs
|
158
170
|
- ext/pf2/src/ruby_init.rs
|
171
|
+
- ext/pf2/src/ruby_internal_apis.rs
|
159
172
|
- ext/pf2/src/sample.rs
|
173
|
+
- ext/pf2/src/scheduler.rs
|
174
|
+
- ext/pf2/src/session.rs
|
175
|
+
- ext/pf2/src/session/configuration.rs
|
176
|
+
- ext/pf2/src/session/new_thread_watcher.rs
|
177
|
+
- ext/pf2/src/session/ruby_object.rs
|
160
178
|
- ext/pf2/src/siginfo_t.c
|
161
179
|
- ext/pf2/src/signal_scheduler.rs
|
162
|
-
- ext/pf2/src/signal_scheduler/configuration.rs
|
163
|
-
- ext/pf2/src/signal_scheduler/timer_installer.rs
|
164
180
|
- ext/pf2/src/timer_thread_scheduler.rs
|
165
181
|
- ext/pf2/src/util.rs
|
166
182
|
- lib/pf2.rb
|
167
183
|
- lib/pf2/cli.rb
|
168
184
|
- lib/pf2/reporter.rb
|
185
|
+
- lib/pf2/serve.rb
|
186
|
+
- lib/pf2/session.rb
|
169
187
|
- lib/pf2/version.rb
|
170
188
|
homepage: https://github.com/osyoyu/pf2
|
171
189
|
licenses:
|
@@ -175,7 +193,6 @@ metadata:
|
|
175
193
|
homepage_uri: https://github.com/osyoyu/pf2
|
176
194
|
source_code_uri: https://github.com/osyoyu/pf2
|
177
195
|
changelog_uri: https://github.com/osyoyu/pf2/blob/master/CHANGELOG.md
|
178
|
-
post_install_message:
|
179
196
|
rdoc_options: []
|
180
197
|
require_paths:
|
181
198
|
- lib
|
@@ -191,7 +208,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
191
208
|
version: '0'
|
192
209
|
requirements: []
|
193
210
|
rubygems_version: 3.6.0.dev
|
194
|
-
signing_key:
|
195
211
|
specification_version: 4
|
196
212
|
summary: Yet another Ruby profiler
|
197
213
|
test_files: []
|
@@ -1,31 +0,0 @@
|
|
1
|
-
use std::collections::HashSet;
|
2
|
-
use std::str::FromStr;
|
3
|
-
use std::time::Duration;
|
4
|
-
|
5
|
-
use rb_sys::VALUE;
|
6
|
-
|
7
|
-
#[derive(Clone, Debug)]
|
8
|
-
pub struct Configuration {
|
9
|
-
pub interval: Duration,
|
10
|
-
pub time_mode: TimeMode,
|
11
|
-
pub target_ruby_threads: HashSet<VALUE>,
|
12
|
-
pub track_new_threads: bool,
|
13
|
-
}
|
14
|
-
|
15
|
-
#[derive(Clone, Debug)]
|
16
|
-
pub enum TimeMode {
|
17
|
-
CpuTime,
|
18
|
-
WallTime,
|
19
|
-
}
|
20
|
-
|
21
|
-
impl FromStr for TimeMode {
|
22
|
-
type Err = ();
|
23
|
-
|
24
|
-
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
25
|
-
match s {
|
26
|
-
"cpu" => Ok(Self::CpuTime),
|
27
|
-
"wall" => Ok(Self::WallTime),
|
28
|
-
_ => Err(()),
|
29
|
-
}
|
30
|
-
}
|
31
|
-
}
|
@@ -1,199 +0,0 @@
|
|
1
|
-
use std::collections::HashMap;
|
2
|
-
use std::ffi::c_void;
|
3
|
-
use std::mem;
|
4
|
-
use std::mem::ManuallyDrop;
|
5
|
-
use std::ptr::null_mut;
|
6
|
-
use std::sync::{Mutex, RwLock};
|
7
|
-
use std::{collections::HashSet, sync::Arc};
|
8
|
-
|
9
|
-
use rb_sys::*;
|
10
|
-
|
11
|
-
use crate::signal_scheduler::SignalHandlerArgs;
|
12
|
-
|
13
|
-
use super::configuration::Configuration;
|
14
|
-
use crate::profile::Profile;
|
15
|
-
|
16
|
-
// We could avoid deferring the timer creation by combining pthread_getcpuclockid(3) and timer_create(2) here,
|
17
|
-
// but we're not doing so since (1) Ruby does not expose the pthread_self() of a Ruby Thread
|
18
|
-
// (which is actually stored in th->nt->thread_id), and (2) pthread_getcpuclockid(3) is not portable
|
19
|
-
// in the first place (e.g. not available on macOS).
|
20
|
-
pub struct TimerInstaller {
|
21
|
-
internal: Box<Mutex<Internal>>,
|
22
|
-
}
|
23
|
-
|
24
|
-
struct Internal {
|
25
|
-
configuration: Configuration,
|
26
|
-
registered_pthread_ids: HashSet<libc::pthread_t>,
|
27
|
-
kernel_thread_id_to_ruby_thread_map: HashMap<libc::pid_t, VALUE>,
|
28
|
-
profile: Arc<RwLock<Profile>>,
|
29
|
-
}
|
30
|
-
|
31
|
-
impl TimerInstaller {
|
32
|
-
// Register a callback that gets called when a Ruby Thread is resumed.
|
33
|
-
// The callback should create a timer for the thread.
|
34
|
-
pub fn install_timer_to_ruby_threads(
|
35
|
-
configuration: Configuration,
|
36
|
-
profile: Arc<RwLock<Profile>>,
|
37
|
-
) {
|
38
|
-
let registrar = Self {
|
39
|
-
internal: Box::new(Mutex::new(Internal {
|
40
|
-
configuration: configuration.clone(),
|
41
|
-
registered_pthread_ids: HashSet::new(),
|
42
|
-
kernel_thread_id_to_ruby_thread_map: HashMap::new(),
|
43
|
-
profile,
|
44
|
-
})),
|
45
|
-
};
|
46
|
-
|
47
|
-
let ptr = Box::into_raw(registrar.internal);
|
48
|
-
unsafe {
|
49
|
-
rb_internal_thread_add_event_hook(
|
50
|
-
Some(Self::on_thread_resume),
|
51
|
-
RUBY_INTERNAL_THREAD_EVENT_RESUMED,
|
52
|
-
ptr as *mut c_void,
|
53
|
-
);
|
54
|
-
// Spawn a no-op Thread to fire the event hook
|
55
|
-
// (at least 2 Ruby Threads must be active for the RESUMED hook to be fired)
|
56
|
-
rb_thread_create(Some(Self::do_nothing), null_mut());
|
57
|
-
};
|
58
|
-
|
59
|
-
if configuration.track_new_threads {
|
60
|
-
unsafe {
|
61
|
-
rb_internal_thread_add_event_hook(
|
62
|
-
Some(Self::on_thread_start),
|
63
|
-
RUBY_INTERNAL_THREAD_EVENT_STARTED,
|
64
|
-
ptr as *mut c_void,
|
65
|
-
);
|
66
|
-
};
|
67
|
-
}
|
68
|
-
}
|
69
|
-
|
70
|
-
unsafe extern "C" fn do_nothing(_: *mut c_void) -> VALUE {
|
71
|
-
Qnil.into()
|
72
|
-
}
|
73
|
-
|
74
|
-
// Thread resume callback
|
75
|
-
unsafe extern "C" fn on_thread_resume(
|
76
|
-
_flag: rb_event_flag_t,
|
77
|
-
data: *const rb_internal_thread_event_data,
|
78
|
-
custom_data: *mut c_void,
|
79
|
-
) {
|
80
|
-
// The SignalScheduler (as a Ruby obj) should be passed as custom_data
|
81
|
-
let internal =
|
82
|
-
unsafe { ManuallyDrop::new(Box::from_raw(custom_data as *mut Mutex<Internal>)) };
|
83
|
-
let mut internal = internal.lock().unwrap();
|
84
|
-
|
85
|
-
// Check if the current thread is a target Ruby Thread
|
86
|
-
let current_ruby_thread: VALUE = unsafe { (*data).thread };
|
87
|
-
if !internal
|
88
|
-
.configuration
|
89
|
-
.target_ruby_threads
|
90
|
-
.contains(¤t_ruby_thread)
|
91
|
-
{
|
92
|
-
return;
|
93
|
-
}
|
94
|
-
|
95
|
-
// Check if the current thread is already registered
|
96
|
-
let current_pthread_id = unsafe { libc::pthread_self() };
|
97
|
-
if internal
|
98
|
-
.registered_pthread_ids
|
99
|
-
.contains(¤t_pthread_id)
|
100
|
-
{
|
101
|
-
return;
|
102
|
-
}
|
103
|
-
|
104
|
-
// Record the pthread ID of the current thread
|
105
|
-
internal.registered_pthread_ids.insert(current_pthread_id);
|
106
|
-
// Keep a mapping from kernel thread ID to Ruby Thread
|
107
|
-
internal
|
108
|
-
.kernel_thread_id_to_ruby_thread_map
|
109
|
-
.insert(unsafe { libc::gettid() }, current_ruby_thread);
|
110
|
-
|
111
|
-
Self::register_timer_to_current_thread(
|
112
|
-
&internal.configuration,
|
113
|
-
&internal.profile,
|
114
|
-
&internal.kernel_thread_id_to_ruby_thread_map,
|
115
|
-
);
|
116
|
-
|
117
|
-
// TODO: Remove the hook when all threads have been registered
|
118
|
-
}
|
119
|
-
|
120
|
-
// Thread resume callback
|
121
|
-
unsafe extern "C" fn on_thread_start(
|
122
|
-
_flag: rb_event_flag_t,
|
123
|
-
data: *const rb_internal_thread_event_data,
|
124
|
-
custom_data: *mut c_void,
|
125
|
-
) {
|
126
|
-
// The SignalScheduler (as a Ruby obj) should be passed as custom_data
|
127
|
-
let internal =
|
128
|
-
unsafe { ManuallyDrop::new(Box::from_raw(custom_data as *mut Mutex<Internal>)) };
|
129
|
-
let mut internal = internal.lock().unwrap();
|
130
|
-
|
131
|
-
let current_ruby_thread: VALUE = unsafe { (*data).thread };
|
132
|
-
internal
|
133
|
-
.configuration
|
134
|
-
.target_ruby_threads
|
135
|
-
.insert(current_ruby_thread);
|
136
|
-
}
|
137
|
-
|
138
|
-
// Creates a new POSIX timer which invocates sampling for the thread that called this function.
|
139
|
-
fn register_timer_to_current_thread(
|
140
|
-
configuration: &Configuration,
|
141
|
-
profile: &Arc<RwLock<Profile>>,
|
142
|
-
kernel_thread_id_to_ruby_thread_map: &HashMap<libc::pid_t, VALUE>,
|
143
|
-
) {
|
144
|
-
let current_pthread_id = unsafe { libc::pthread_self() };
|
145
|
-
let context_ruby_thread: VALUE = unsafe {
|
146
|
-
*(kernel_thread_id_to_ruby_thread_map
|
147
|
-
.get(&(libc::gettid()))
|
148
|
-
.unwrap())
|
149
|
-
};
|
150
|
-
|
151
|
-
// NOTE: This Box is never dropped
|
152
|
-
let signal_handler_args = Box::new(SignalHandlerArgs {
|
153
|
-
profile: Arc::clone(profile),
|
154
|
-
context_ruby_thread,
|
155
|
-
});
|
156
|
-
|
157
|
-
// Create a signal event
|
158
|
-
let mut sigevent: libc::sigevent = unsafe { mem::zeroed() };
|
159
|
-
// Note: SIGEV_THREAD_ID is Linux-specific. In other platforms, we would need to
|
160
|
-
// "tranpoline" the signal as any pthread can receive the signal.
|
161
|
-
sigevent.sigev_notify = libc::SIGEV_THREAD_ID;
|
162
|
-
sigevent.sigev_notify_thread_id =
|
163
|
-
unsafe { libc::syscall(libc::SYS_gettid).try_into().unwrap() }; // The kernel thread ID
|
164
|
-
sigevent.sigev_signo = libc::SIGALRM;
|
165
|
-
// Pass required args to the signal handler
|
166
|
-
sigevent.sigev_value.sival_ptr = Box::into_raw(signal_handler_args) as *mut c_void;
|
167
|
-
|
168
|
-
// Create and configure timer to fire every _interval_ ms of CPU time
|
169
|
-
let mut timer: libc::timer_t = unsafe { mem::zeroed() };
|
170
|
-
let clockid = match configuration.time_mode {
|
171
|
-
crate::signal_scheduler::TimeMode::CpuTime => libc::CLOCK_THREAD_CPUTIME_ID,
|
172
|
-
crate::signal_scheduler::TimeMode::WallTime => libc::CLOCK_MONOTONIC,
|
173
|
-
};
|
174
|
-
let err = unsafe { libc::timer_create(clockid, &mut sigevent, &mut timer) };
|
175
|
-
if err != 0 {
|
176
|
-
panic!("timer_create failed: {}", err);
|
177
|
-
}
|
178
|
-
let itimerspec = Self::duration_to_itimerspec(&configuration.interval);
|
179
|
-
let err = unsafe { libc::timer_settime(timer, 0, &itimerspec, null_mut()) };
|
180
|
-
if err != 0 {
|
181
|
-
panic!("timer_settime failed: {}", err);
|
182
|
-
}
|
183
|
-
|
184
|
-
log::debug!("timer registered for thread {}", current_pthread_id);
|
185
|
-
}
|
186
|
-
|
187
|
-
fn duration_to_itimerspec(duration: &std::time::Duration) -> libc::itimerspec {
|
188
|
-
let nanos = duration.as_nanos();
|
189
|
-
let seconds_part: i64 = (nanos / 1_000_000_000).try_into().unwrap();
|
190
|
-
let nanos_part: i64 = (nanos % 1_000_000_000).try_into().unwrap();
|
191
|
-
|
192
|
-
let mut its: libc::itimerspec = unsafe { mem::zeroed() };
|
193
|
-
its.it_interval.tv_sec = seconds_part;
|
194
|
-
its.it_interval.tv_nsec = nanos_part;
|
195
|
-
its.it_value.tv_sec = seconds_part;
|
196
|
-
its.it_value.tv_nsec = nanos_part;
|
197
|
-
its
|
198
|
-
}
|
199
|
-
}
|