pf2 0.3.0 → 0.4.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 +9 -0
- data/README.md +2 -1
- data/ext/pf2/src/backtrace.rs +1 -0
- data/ext/pf2/src/lib.rs +2 -0
- data/ext/pf2/src/ruby_internal_apis.rs +47 -0
- data/ext/pf2/src/signal_scheduler/configuration.rs +1 -1
- data/ext/pf2/src/signal_scheduler/timer_installer.rs +58 -112
- data/ext/pf2/src/signal_scheduler.rs +3 -3
- data/lib/pf2/version.rb +1 -1
- metadata +3 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d576aec57e600cc77194fabb8c1b7b3c782c4ab76a03ec3b0f24581eb4c317ae
|
4
|
+
data.tar.gz: a290d23df802601977622f1afe39f1f2d6cd92a9350b25d203e97c3b6da6db16
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a8d72fa4ccb3395f0fdd9203885c1e0cb8a00429597a6dd756e80fb70e0d8092c084992bd9595ee21789a4aaf2550e2284b6d3f81b448dc330088b325d822d85
|
7
|
+
data.tar.gz: 8f93c98574ef7077336232bb1b51a4af1ac4ba2561914a299209a5c2879fe9af3e8998c8df264836f698fea05c8d028f7ae985de3c18e9889f05568ec2740bc1
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
### Added
|
4
|
+
|
5
|
+
- New option: `track_all_threads`
|
6
|
+
- When true, all Threads will be tracked regardless of the `threads` option.
|
7
|
+
|
8
|
+
### Removed
|
9
|
+
|
10
|
+
- The `track_new_threads` option was removed in favor of the `track_all_threads` option.
|
11
|
+
|
3
12
|
|
4
13
|
## [0.3.0] - 2024-02-05
|
5
14
|
|
data/README.md
CHANGED
@@ -8,6 +8,7 @@ Notable Capabilites
|
|
8
8
|
|
9
9
|
- Can accurately track multiple Ruby Threads' activity
|
10
10
|
- Sampling interval can be set based on per-Thread CPU usage
|
11
|
+
- Can record native (C-level) stack traces side-by-side with Ruby traces
|
11
12
|
|
12
13
|
Usage
|
13
14
|
--------
|
@@ -57,7 +58,7 @@ Pf2.start(
|
|
57
58
|
threads: [], # Array<Thread>: A list of Ruby Threads to be tracked (default: `Thread.list`)
|
58
59
|
time_mode: :cpu, # `:cpu` or `:wall`: The sampling timer's mode
|
59
60
|
# (default: `:cpu` for SignalScheduler, `:wall` for TimerThreadScheduler)
|
60
|
-
|
61
|
+
track_all_threads: true # Boolean: Whether to track all Threads regardless of `threads` option
|
61
62
|
# (default: false)
|
62
63
|
)
|
63
64
|
```
|
data/ext/pf2/src/backtrace.rs
CHANGED
data/ext/pf2/src/lib.rs
CHANGED
@@ -0,0 +1,47 @@
|
|
1
|
+
#![allow(non_snake_case)]
|
2
|
+
#![allow(non_camel_case_types)]
|
3
|
+
|
4
|
+
use libc::{clockid_t, pthread_getcpuclockid, pthread_t};
|
5
|
+
use rb_sys::{rb_check_typeddata, rb_data_type_struct, RTypedData, VALUE};
|
6
|
+
use std::ffi::{c_char, c_int};
|
7
|
+
use std::mem::MaybeUninit;
|
8
|
+
|
9
|
+
// Types and structs from Ruby 3.4.0.
|
10
|
+
|
11
|
+
type rb_nativethread_id_t = libc::pthread_t;
|
12
|
+
|
13
|
+
#[repr(C)]
|
14
|
+
struct rb_native_thread {
|
15
|
+
_padding_serial: [c_char; 4], // rb_atomic_t
|
16
|
+
_padding_vm: *mut c_int, // struct rb_vm_struct
|
17
|
+
thread_id: rb_nativethread_id_t,
|
18
|
+
// ...
|
19
|
+
}
|
20
|
+
|
21
|
+
#[repr(C)]
|
22
|
+
struct rb_thread_struct {
|
23
|
+
_padding_lt_node: [c_char; 16], // struct ccan_list_node
|
24
|
+
_padding_self: VALUE,
|
25
|
+
_padding_ractor: *mut c_int, // rb_ractor_t
|
26
|
+
_padding_vm: *mut c_int, // rb_vm_t
|
27
|
+
nt: *mut rb_native_thread,
|
28
|
+
// ...
|
29
|
+
}
|
30
|
+
type rb_thread_t = rb_thread_struct;
|
31
|
+
|
32
|
+
/// Reimplementation of the internal RTYPEDDATA_TYPE macro.
|
33
|
+
unsafe fn RTYPEDDATA_TYPE(obj: VALUE) -> *const rb_data_type_struct {
|
34
|
+
let typed: *mut RTypedData = obj as *mut RTypedData;
|
35
|
+
(*typed).type_
|
36
|
+
}
|
37
|
+
|
38
|
+
unsafe fn rb_thread_ptr(thread: VALUE) -> *mut rb_thread_t {
|
39
|
+
unsafe { rb_check_typeddata(thread, RTYPEDDATA_TYPE(thread)) as *mut rb_thread_t }
|
40
|
+
}
|
41
|
+
|
42
|
+
pub unsafe fn rb_thread_getcpuclockid(thread: VALUE) -> clockid_t {
|
43
|
+
let mut cid: clockid_t = MaybeUninit::zeroed().assume_init();
|
44
|
+
let pthread_id: pthread_t = (*(*rb_thread_ptr(thread)).nt).thread_id;
|
45
|
+
pthread_getcpuclockid(pthread_id, &mut cid as *mut clockid_t);
|
46
|
+
cid
|
47
|
+
}
|
@@ -1,31 +1,28 @@
|
|
1
|
-
use std::collections::
|
1
|
+
use std::collections::HashSet;
|
2
2
|
use std::ffi::c_void;
|
3
3
|
use std::mem;
|
4
4
|
use std::mem::ManuallyDrop;
|
5
5
|
use std::ptr::null_mut;
|
6
|
+
use std::sync::Arc;
|
6
7
|
use std::sync::{Mutex, RwLock};
|
7
|
-
use std::{collections::HashSet, sync::Arc};
|
8
8
|
|
9
9
|
use rb_sys::*;
|
10
10
|
|
11
|
-
use crate::signal_scheduler::SignalHandlerArgs;
|
12
|
-
|
13
11
|
use super::configuration::Configuration;
|
14
12
|
use crate::profile::Profile;
|
13
|
+
use crate::ruby_internal_apis::rb_thread_getcpuclockid;
|
14
|
+
use crate::signal_scheduler::{cstr, SignalHandlerArgs};
|
15
15
|
|
16
|
-
|
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).
|
16
|
+
#[derive(Debug)]
|
20
17
|
pub struct TimerInstaller {
|
21
|
-
|
18
|
+
inner: Box<Mutex<Inner>>,
|
22
19
|
}
|
23
20
|
|
24
|
-
|
21
|
+
#[derive(Debug)]
|
22
|
+
struct Inner {
|
25
23
|
configuration: Configuration,
|
26
|
-
|
27
|
-
|
28
|
-
profile: Arc<RwLock<Profile>>,
|
24
|
+
pub profile: Arc<RwLock<Profile>>,
|
25
|
+
known_threads: HashSet<VALUE>,
|
29
26
|
}
|
30
27
|
|
31
28
|
impl TimerInstaller {
|
@@ -35,153 +32,102 @@ impl TimerInstaller {
|
|
35
32
|
configuration: Configuration,
|
36
33
|
profile: Arc<RwLock<Profile>>,
|
37
34
|
) {
|
38
|
-
let
|
39
|
-
|
35
|
+
let installer = Self {
|
36
|
+
inner: Box::new(Mutex::new(Inner {
|
40
37
|
configuration: configuration.clone(),
|
41
|
-
registered_pthread_ids: HashSet::new(),
|
42
|
-
kernel_thread_id_to_ruby_thread_map: HashMap::new(),
|
43
38
|
profile,
|
39
|
+
known_threads: HashSet::new(),
|
44
40
|
})),
|
45
41
|
};
|
46
42
|
|
47
|
-
let
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
};
|
43
|
+
if let Ok(mut inner) = installer.inner.try_lock() {
|
44
|
+
for ruby_thread in configuration.target_ruby_threads.iter() {
|
45
|
+
let ruby_thread: VALUE = *ruby_thread;
|
46
|
+
inner.known_threads.insert(ruby_thread);
|
47
|
+
inner.register_timer_to_ruby_thread(ruby_thread);
|
48
|
+
}
|
49
|
+
}
|
58
50
|
|
59
|
-
if configuration.
|
51
|
+
if configuration.track_all_threads {
|
52
|
+
let ptr = Box::into_raw(installer.inner);
|
60
53
|
unsafe {
|
54
|
+
// TODO: Clean up this hook when the profiling session ends
|
61
55
|
rb_internal_thread_add_event_hook(
|
62
|
-
Some(Self::
|
63
|
-
|
56
|
+
Some(Self::on_thread_resume),
|
57
|
+
RUBY_INTERNAL_THREAD_EVENT_RESUMED,
|
64
58
|
ptr as *mut c_void,
|
65
59
|
);
|
66
60
|
};
|
67
61
|
}
|
68
62
|
}
|
69
63
|
|
70
|
-
|
71
|
-
Qnil.into()
|
72
|
-
}
|
73
|
-
|
74
|
-
// Thread resume callback
|
64
|
+
// Thread start callback
|
75
65
|
unsafe extern "C" fn on_thread_resume(
|
76
66
|
_flag: rb_event_flag_t,
|
77
67
|
data: *const rb_internal_thread_event_data,
|
78
68
|
custom_data: *mut c_void,
|
79
69
|
) {
|
80
|
-
|
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
|
-
}
|
70
|
+
let ruby_thread: VALUE = unsafe { (*data).thread };
|
94
71
|
|
95
|
-
//
|
96
|
-
let
|
97
|
-
|
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
|
-
}
|
72
|
+
// A pointer to Box<Inner> is passed as custom_data
|
73
|
+
let inner = unsafe { ManuallyDrop::new(Box::from_raw(custom_data as *mut Mutex<Inner>)) };
|
74
|
+
let mut inner = inner.lock().unwrap();
|
119
75
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
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);
|
76
|
+
if !inner.known_threads.contains(&ruby_thread) {
|
77
|
+
inner.known_threads.insert(ruby_thread);
|
78
|
+
// Install a timer for the thread
|
79
|
+
inner.register_timer_to_ruby_thread(ruby_thread);
|
80
|
+
}
|
136
81
|
}
|
82
|
+
}
|
137
83
|
|
138
|
-
|
139
|
-
fn
|
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
|
-
|
84
|
+
impl Inner {
|
85
|
+
fn register_timer_to_ruby_thread(&self, ruby_thread: VALUE) {
|
151
86
|
// NOTE: This Box is never dropped
|
152
87
|
let signal_handler_args = Box::new(SignalHandlerArgs {
|
153
|
-
profile: Arc::clone(profile),
|
154
|
-
context_ruby_thread,
|
88
|
+
profile: Arc::clone(&self.profile),
|
89
|
+
context_ruby_thread: ruby_thread,
|
155
90
|
});
|
156
91
|
|
92
|
+
// rb_funcall deadlocks when called within a THREAD_EVENT_STARTED hook
|
93
|
+
let kernel_thread_id: i32 = i32::try_from(unsafe {
|
94
|
+
rb_num2int(rb_funcall(
|
95
|
+
ruby_thread,
|
96
|
+
rb_intern(cstr!("native_thread_id")), // kernel thread ID
|
97
|
+
0,
|
98
|
+
))
|
99
|
+
})
|
100
|
+
.unwrap();
|
101
|
+
|
157
102
|
// Create a signal event
|
158
103
|
let mut sigevent: libc::sigevent = unsafe { mem::zeroed() };
|
159
104
|
// Note: SIGEV_THREAD_ID is Linux-specific. In other platforms, we would need to
|
160
|
-
// "
|
105
|
+
// "trampoline" the signal as any pthread can receive the signal.
|
161
106
|
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
|
107
|
+
sigevent.sigev_notify_thread_id = kernel_thread_id;
|
164
108
|
sigevent.sigev_signo = libc::SIGALRM;
|
165
109
|
// Pass required args to the signal handler
|
166
110
|
sigevent.sigev_value.sival_ptr = Box::into_raw(signal_handler_args) as *mut c_void;
|
167
111
|
|
168
112
|
// Create and configure timer to fire every _interval_ ms of CPU time
|
169
113
|
let mut timer: libc::timer_t = unsafe { mem::zeroed() };
|
170
|
-
let clockid = match configuration.time_mode {
|
171
|
-
crate::signal_scheduler::TimeMode::CpuTime =>
|
114
|
+
let clockid = match self.configuration.time_mode {
|
115
|
+
crate::signal_scheduler::TimeMode::CpuTime => unsafe {
|
116
|
+
rb_thread_getcpuclockid(ruby_thread)
|
117
|
+
},
|
172
118
|
crate::signal_scheduler::TimeMode::WallTime => libc::CLOCK_MONOTONIC,
|
173
119
|
};
|
174
120
|
let err = unsafe { libc::timer_create(clockid, &mut sigevent, &mut timer) };
|
175
121
|
if err != 0 {
|
176
122
|
panic!("timer_create failed: {}", err);
|
177
123
|
}
|
178
|
-
let itimerspec = Self::duration_to_itimerspec(&configuration.interval);
|
124
|
+
let itimerspec = Self::duration_to_itimerspec(&self.configuration.interval);
|
179
125
|
let err = unsafe { libc::timer_settime(timer, 0, &itimerspec, null_mut()) };
|
180
126
|
if err != 0 {
|
181
127
|
panic!("timer_settime failed: {}", err);
|
182
128
|
}
|
183
129
|
|
184
|
-
log::debug!("timer registered for thread {}",
|
130
|
+
log::debug!("timer registered for thread {}", ruby_thread);
|
185
131
|
}
|
186
132
|
|
187
133
|
fn duration_to_itimerspec(duration: &std::time::Duration) -> libc::itimerspec {
|
@@ -56,7 +56,7 @@ impl SignalScheduler {
|
|
56
56
|
rb_intern(cstr!("interval_ms")),
|
57
57
|
rb_intern(cstr!("threads")),
|
58
58
|
rb_intern(cstr!("time_mode")),
|
59
|
-
rb_intern(cstr!("
|
59
|
+
rb_intern(cstr!("track_all_threads")),
|
60
60
|
]
|
61
61
|
.as_mut_ptr(),
|
62
62
|
0,
|
@@ -99,7 +99,7 @@ impl SignalScheduler {
|
|
99
99
|
} else {
|
100
100
|
configuration::TimeMode::CpuTime
|
101
101
|
};
|
102
|
-
let
|
102
|
+
let track_all_threads: bool = if kwargs_values[3] != Qundef as VALUE {
|
103
103
|
RTEST(kwargs_values[3])
|
104
104
|
} else {
|
105
105
|
false
|
@@ -117,7 +117,7 @@ impl SignalScheduler {
|
|
117
117
|
interval,
|
118
118
|
target_ruby_threads,
|
119
119
|
time_mode,
|
120
|
-
|
120
|
+
track_all_threads,
|
121
121
|
});
|
122
122
|
|
123
123
|
Qnil.into()
|
data/lib/pf2/version.rb
CHANGED
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.4.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-22 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: rake-compiler
|
@@ -52,7 +51,6 @@ dependencies:
|
|
52
51
|
- - ">="
|
53
52
|
- !ruby/object:Gem::Version
|
54
53
|
version: '0'
|
55
|
-
description:
|
56
54
|
email:
|
57
55
|
- osyoyu@osyoyu.com
|
58
56
|
executables:
|
@@ -156,6 +154,7 @@ files:
|
|
156
154
|
- ext/pf2/src/profile_serializer.rs
|
157
155
|
- ext/pf2/src/ringbuffer.rs
|
158
156
|
- ext/pf2/src/ruby_init.rs
|
157
|
+
- ext/pf2/src/ruby_internal_apis.rs
|
159
158
|
- ext/pf2/src/sample.rs
|
160
159
|
- ext/pf2/src/siginfo_t.c
|
161
160
|
- ext/pf2/src/signal_scheduler.rs
|
@@ -175,7 +174,6 @@ metadata:
|
|
175
174
|
homepage_uri: https://github.com/osyoyu/pf2
|
176
175
|
source_code_uri: https://github.com/osyoyu/pf2
|
177
176
|
changelog_uri: https://github.com/osyoyu/pf2/blob/master/CHANGELOG.md
|
178
|
-
post_install_message:
|
179
177
|
rdoc_options: []
|
180
178
|
require_paths:
|
181
179
|
- lib
|
@@ -191,7 +189,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
191
189
|
version: '0'
|
192
190
|
requirements: []
|
193
191
|
rubygems_version: 3.6.0.dev
|
194
|
-
signing_key:
|
195
192
|
specification_version: 4
|
196
193
|
summary: Yet another Ruby profiler
|
197
194
|
test_files: []
|