pf2 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c4b713a9333a657f9b2259cdbae91e9aabde4312c2791a4dce517ce7d30cc46a
|
4
|
+
data.tar.gz: f01a941327ac6f2ce8b116a466a85a3021ffe0f612063b0f1466e7a31b373984
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf8c6b94d2a2ccee5912388a4f2f43f09612f9f3ba5a4ff10d615c7798bde30eb6061f03a9c79fa1ffe946da5f4a065eeae2bbe75ce1bb99821ce5253cc3ba9f
|
7
|
+
data.tar.gz: 50bd5c325fded53cf1585245469232c3fb184d1b3b893df0deb35a8f5d410046c3c821b1a27d468df6a3bb9ac8e2d7a12011aaa94a8d6889ca013dd4ec1f8eb1
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,37 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
3
|
|
4
|
+
## [0.5.0] - 2024-03-25
|
5
|
+
|
6
|
+
### Added
|
7
|
+
|
8
|
+
- `pf2 serve` subcommand
|
9
|
+
- `pf2 serve -- ruby target.rb`
|
10
|
+
- Profile programs without any change
|
11
|
+
- New option: `threads: :all`
|
12
|
+
- When specified, Pf2 will track all active threads.
|
13
|
+
- `threads: nil` / omitting the `threads` option has the same effect.
|
14
|
+
- Introduce `Pf2::Session` (https://github.com/osyoyu/pf2/pull/16)
|
15
|
+
- `Session` will be responsible for managing Profiles and Schedulers
|
16
|
+
|
17
|
+
### Removed
|
18
|
+
|
19
|
+
- `Pf2::SignalScheduler` and `Pf2::TimerThreadScheduler` are now hidden from Ruby.
|
20
|
+
- `track_all_threads` option is removed in favor of `threads: :all`.
|
21
|
+
|
22
|
+
|
23
|
+
## [0.4.0] - 2024-03-22
|
24
|
+
|
25
|
+
### Added
|
26
|
+
|
27
|
+
- New option: `track_all_threads`
|
28
|
+
- When true, all Threads will be tracked regardless of the `threads` option.
|
29
|
+
|
30
|
+
### Removed
|
31
|
+
|
32
|
+
- The `track_new_threads` option was removed in favor of the `track_all_threads` option.
|
33
|
+
|
34
|
+
|
4
35
|
## [0.3.0] - 2024-02-05
|
5
36
|
|
6
37
|
### Added
|
data/Cargo.lock
CHANGED
@@ -456,9 +456,9 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
|
456
456
|
|
457
457
|
[[package]]
|
458
458
|
name = "shlex"
|
459
|
-
version = "1.
|
459
|
+
version = "1.3.0"
|
460
460
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
461
|
-
checksum = "
|
461
|
+
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
462
462
|
|
463
463
|
[[package]]
|
464
464
|
name = "syn"
|
data/README.md
CHANGED
@@ -8,10 +8,24 @@ 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
|
--------
|
14
15
|
|
16
|
+
### Quickstart
|
17
|
+
|
18
|
+
Run your Ruby program through `pf2 serve`.
|
19
|
+
Wait a while until Pf2 collects profiles (or until the target program exits), then open the displayed link for visualization.
|
20
|
+
|
21
|
+
```
|
22
|
+
$ pf2 serve -- ruby target.rb
|
23
|
+
[Pf2] Listening on localhost:51502.
|
24
|
+
[Pf2] Open https://profiler.firefox.com/from-url/http%3A%2F%2Flocalhost%3A51502%2Fprofile for visualization.
|
25
|
+
|
26
|
+
I'm the target program!
|
27
|
+
```
|
28
|
+
|
15
29
|
### Profiling
|
16
30
|
|
17
31
|
Pf2 will collect samples every 10 ms of wall time by default.
|
@@ -54,11 +68,10 @@ Pf2 accepts the following configuration keys:
|
|
54
68
|
```rb
|
55
69
|
Pf2.start(
|
56
70
|
interval_ms: 49, # Integer: The sampling interval in milliseconds (default: 49)
|
57
|
-
threads: [], # Array<Thread>: A list of Ruby Threads to be tracked (default: `Thread.list`)
|
58
71
|
time_mode: :cpu, # `:cpu` or `:wall`: The sampling timer's mode
|
59
72
|
# (default: `:cpu` for SignalScheduler, `:wall` for TimerThreadScheduler)
|
60
|
-
|
61
|
-
#
|
73
|
+
threads: [th1, th2], # `Array<Thread>` | `:all`: A list of Ruby Threads to be tracked.
|
74
|
+
# When `:all` or unspecified, Pf2 will track all active Threads.
|
62
75
|
)
|
63
76
|
```
|
64
77
|
|
data/ext/pf2/src/backtrace.rs
CHANGED
data/ext/pf2/src/lib.rs
CHANGED
@@ -6,6 +6,7 @@ use rb_sys::*;
|
|
6
6
|
|
7
7
|
use crate::backtrace::Backtrace;
|
8
8
|
use crate::profile::Profile;
|
9
|
+
use crate::util::RTEST;
|
9
10
|
|
10
11
|
#[derive(Debug, Deserialize, Serialize)]
|
11
12
|
pub struct ProfileSerializer {
|
@@ -62,6 +63,10 @@ struct FrameTableEntry {
|
|
62
63
|
id: FrameTableId,
|
63
64
|
entry_type: FrameTableEntryType,
|
64
65
|
full_label: String,
|
66
|
+
file_name: Option<String>,
|
67
|
+
function_first_lineno: Option<i32>,
|
68
|
+
callsite_lineno: Option<i32>,
|
69
|
+
address: Option<usize>,
|
65
70
|
}
|
66
71
|
|
67
72
|
#[derive(Debug, Deserialize, Serialize)]
|
@@ -77,6 +82,11 @@ struct ProfileSample {
|
|
77
82
|
stack_tree_id: StackTreeNodeId,
|
78
83
|
}
|
79
84
|
|
85
|
+
struct NativeFunctionFrame {
|
86
|
+
pub symbol_name: String,
|
87
|
+
pub address: Option<usize>,
|
88
|
+
}
|
89
|
+
|
80
90
|
impl ProfileSerializer {
|
81
91
|
pub fn serialize(profile: &Profile) -> String {
|
82
92
|
let mut sequence = 1;
|
@@ -92,49 +102,46 @@ impl ProfileSerializer {
|
|
92
102
|
|
93
103
|
// Process C-level stack
|
94
104
|
|
95
|
-
|
96
|
-
//
|
97
|
-
// so we keep the expanded stack frame in this Vec.
|
98
|
-
let mut c_stack: Vec<String> = vec![];
|
105
|
+
let mut c_stack: Vec<NativeFunctionFrame> = vec![];
|
106
|
+
// Rebuild the original backtrace (including inlined functions) from the PC.
|
99
107
|
for i in 0..sample.c_backtrace_pcs[0] {
|
100
108
|
let pc = sample.c_backtrace_pcs[i + 1];
|
101
109
|
Backtrace::backtrace_syminfo(
|
102
110
|
&profile.backtrace_state,
|
103
111
|
pc,
|
104
|
-
|_pc: usize, symname: *const c_char,
|
112
|
+
|_pc: usize, symname: *const c_char, symval: usize, _symsize: usize| {
|
105
113
|
if symname.is_null() {
|
106
|
-
c_stack.push(
|
114
|
+
c_stack.push(NativeFunctionFrame {
|
115
|
+
symbol_name: "(no symbol information)".to_owned(),
|
116
|
+
address: None,
|
117
|
+
});
|
107
118
|
} else {
|
108
|
-
c_stack.push(
|
119
|
+
c_stack.push(NativeFunctionFrame {
|
120
|
+
symbol_name: CStr::from_ptr(symname)
|
121
|
+
.to_str()
|
122
|
+
.unwrap()
|
123
|
+
.to_owned(),
|
124
|
+
address: Some(symval),
|
125
|
+
});
|
109
126
|
}
|
110
127
|
},
|
111
128
|
Some(Backtrace::backtrace_error_callback),
|
112
129
|
);
|
113
130
|
}
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
let mut reached_ruby = false;
|
119
|
-
c_stack.retain(|frame| {
|
120
|
-
if reached_ruby {
|
121
|
-
return false;
|
122
|
-
}
|
123
|
-
if frame.contains("pf2") {
|
124
|
-
return false;
|
125
|
-
}
|
126
|
-
if frame.contains("rb_vm_exec") || frame.contains("vm_call_cfunc_with_frame") {
|
127
|
-
reached_ruby = true;
|
128
|
-
return false;
|
131
|
+
for frame in c_stack.iter() {
|
132
|
+
if frame.symbol_name.contains("pf2") {
|
133
|
+
// Skip Pf2-related frames
|
134
|
+
continue;
|
129
135
|
}
|
130
|
-
true
|
131
|
-
});
|
132
136
|
|
133
|
-
for frame in c_stack.iter() {
|
134
137
|
merged_stack.push(FrameTableEntry {
|
135
|
-
id: calculate_id_for_c_frame(frame),
|
138
|
+
id: calculate_id_for_c_frame(&frame.symbol_name),
|
136
139
|
entry_type: FrameTableEntryType::Native,
|
137
|
-
full_label: frame.
|
140
|
+
full_label: frame.symbol_name.clone(),
|
141
|
+
file_name: None,
|
142
|
+
function_first_lineno: None,
|
143
|
+
callsite_lineno: None,
|
144
|
+
address: frame.address,
|
138
145
|
});
|
139
146
|
}
|
140
147
|
|
@@ -143,15 +150,52 @@ impl ProfileSerializer {
|
|
143
150
|
let ruby_stack_depth = sample.line_count;
|
144
151
|
for i in 0..ruby_stack_depth {
|
145
152
|
let frame: VALUE = sample.frames[i as usize];
|
153
|
+
let lineno: i32 = sample.linenos[i as usize];
|
154
|
+
let address: Option<usize> = {
|
155
|
+
let cme = frame
|
156
|
+
as *mut crate::ruby_internal_apis::rb_callable_method_entry_struct;
|
157
|
+
let cme = &*cme;
|
158
|
+
|
159
|
+
if (*(cme.def)).type_ == 1 {
|
160
|
+
// The cme is a Cfunc
|
161
|
+
Some((*(cme.def)).cfunc.func as usize)
|
162
|
+
} else {
|
163
|
+
// The cme is an ISeq (Ruby code) or some other type
|
164
|
+
None
|
165
|
+
}
|
166
|
+
};
|
167
|
+
let mut frame_full_label: VALUE = rb_profile_frame_full_label(frame);
|
168
|
+
let frame_full_label: String = if RTEST(frame_full_label) {
|
169
|
+
CStr::from_ptr(rb_string_value_cstr(&mut frame_full_label))
|
170
|
+
.to_str()
|
171
|
+
.unwrap()
|
172
|
+
.to_owned()
|
173
|
+
} else {
|
174
|
+
"(unknown)".to_owned()
|
175
|
+
};
|
176
|
+
let mut frame_path: VALUE = rb_profile_frame_path(frame);
|
177
|
+
let frame_path: String = if RTEST(frame_path) {
|
178
|
+
CStr::from_ptr(rb_string_value_cstr(&mut frame_path))
|
179
|
+
.to_str()
|
180
|
+
.unwrap()
|
181
|
+
.to_owned()
|
182
|
+
} else {
|
183
|
+
"(unknown)".to_owned()
|
184
|
+
};
|
185
|
+
let frame_first_lineno: VALUE = rb_profile_frame_first_lineno(frame);
|
186
|
+
let frame_first_lineno: Option<i32> = if RTEST(frame_first_lineno) {
|
187
|
+
Some(rb_num2int(frame_first_lineno).try_into().unwrap())
|
188
|
+
} else {
|
189
|
+
None
|
190
|
+
};
|
146
191
|
merged_stack.push(FrameTableEntry {
|
147
192
|
id: frame,
|
148
193
|
entry_type: FrameTableEntryType::Ruby,
|
149
|
-
full_label:
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
.to_owned(),
|
194
|
+
full_label: frame_full_label,
|
195
|
+
file_name: Some(frame_path),
|
196
|
+
function_first_lineno: frame_first_lineno,
|
197
|
+
callsite_lineno: Some(lineno),
|
198
|
+
address,
|
155
199
|
});
|
156
200
|
}
|
157
201
|
|
data/ext/pf2/src/ruby_init.rs
CHANGED
@@ -2,9 +2,7 @@
|
|
2
2
|
|
3
3
|
use rb_sys::*;
|
4
4
|
|
5
|
-
|
6
|
-
use crate::signal_scheduler::SignalScheduler;
|
7
|
-
use crate::timer_thread_scheduler::TimerThreadScheduler;
|
5
|
+
use crate::session::ruby_object::SessionRubyObject;
|
8
6
|
use crate::util::*;
|
9
7
|
|
10
8
|
#[allow(non_snake_case)]
|
@@ -21,53 +19,24 @@ extern "C" fn Init_pf2() {
|
|
21
19
|
unsafe {
|
22
20
|
let rb_mPf2: VALUE = rb_define_module(cstr!("Pf2"));
|
23
21
|
|
24
|
-
|
25
|
-
|
26
|
-
let rb_mPf2_SignalScheduler =
|
27
|
-
rb_define_class_under(rb_mPf2, cstr!("SignalScheduler"), rb_cObject);
|
28
|
-
rb_define_alloc_func(rb_mPf2_SignalScheduler, Some(SignalScheduler::rb_alloc));
|
29
|
-
rb_define_method(
|
30
|
-
rb_mPf2_SignalScheduler,
|
31
|
-
cstr!("initialize"),
|
32
|
-
Some(to_ruby_cfunc_with_args(SignalScheduler::rb_initialize)),
|
33
|
-
-1,
|
34
|
-
);
|
35
|
-
rb_define_method(
|
36
|
-
rb_mPf2_SignalScheduler,
|
37
|
-
cstr!("start"),
|
38
|
-
Some(to_ruby_cfunc_with_no_args(SignalScheduler::rb_start)),
|
39
|
-
0,
|
40
|
-
);
|
41
|
-
rb_define_method(
|
42
|
-
rb_mPf2_SignalScheduler,
|
43
|
-
cstr!("stop"),
|
44
|
-
Some(to_ruby_cfunc_with_no_args(SignalScheduler::rb_stop)),
|
45
|
-
0,
|
46
|
-
);
|
47
|
-
}
|
48
|
-
|
49
|
-
let rb_mPf2_TimerThreadScheduler =
|
50
|
-
rb_define_class_under(rb_mPf2, cstr!("TimerThreadScheduler"), rb_cObject);
|
51
|
-
rb_define_alloc_func(
|
52
|
-
rb_mPf2_TimerThreadScheduler,
|
53
|
-
Some(TimerThreadScheduler::rb_alloc),
|
54
|
-
);
|
22
|
+
let rb_mPf2_Session = rb_define_class_under(rb_mPf2, cstr!("Session"), rb_cObject);
|
23
|
+
rb_define_alloc_func(rb_mPf2_Session, Some(SessionRubyObject::rb_alloc));
|
55
24
|
rb_define_method(
|
56
|
-
|
25
|
+
rb_mPf2_Session,
|
57
26
|
cstr!("initialize"),
|
58
|
-
Some(to_ruby_cfunc_with_args(
|
27
|
+
Some(to_ruby_cfunc_with_args(SessionRubyObject::rb_initialize)),
|
59
28
|
-1,
|
60
29
|
);
|
61
30
|
rb_define_method(
|
62
|
-
|
31
|
+
rb_mPf2_Session,
|
63
32
|
cstr!("start"),
|
64
|
-
Some(to_ruby_cfunc_with_no_args(
|
33
|
+
Some(to_ruby_cfunc_with_no_args(SessionRubyObject::rb_start)),
|
65
34
|
0,
|
66
35
|
);
|
67
36
|
rb_define_method(
|
68
|
-
|
37
|
+
rb_mPf2_Session,
|
69
38
|
cstr!("stop"),
|
70
|
-
Some(to_ruby_cfunc_with_no_args(
|
39
|
+
Some(to_ruby_cfunc_with_no_args(SessionRubyObject::rb_stop)),
|
71
40
|
0,
|
72
41
|
);
|
73
42
|
}
|
@@ -0,0 +1,70 @@
|
|
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, c_void};
|
7
|
+
use std::mem::MaybeUninit;
|
8
|
+
|
9
|
+
// Types and structs from Ruby 3.4.0.
|
10
|
+
|
11
|
+
#[repr(C)]
|
12
|
+
pub struct rb_callable_method_entry_struct {
|
13
|
+
/* same fields with rb_method_entry_t */
|
14
|
+
pub flags: VALUE,
|
15
|
+
_padding_defined_class: VALUE,
|
16
|
+
pub def: *mut rb_method_definition_struct,
|
17
|
+
// ...
|
18
|
+
}
|
19
|
+
|
20
|
+
#[repr(C)]
|
21
|
+
pub struct rb_method_definition_struct {
|
22
|
+
pub type_: c_int,
|
23
|
+
_padding: [c_char; 4],
|
24
|
+
pub cfunc: rb_method_cfunc_struct,
|
25
|
+
// ...
|
26
|
+
}
|
27
|
+
|
28
|
+
#[repr(C)]
|
29
|
+
pub struct rb_method_cfunc_struct {
|
30
|
+
pub func: *mut c_void,
|
31
|
+
// ...
|
32
|
+
}
|
33
|
+
|
34
|
+
type rb_nativethread_id_t = libc::pthread_t;
|
35
|
+
|
36
|
+
#[repr(C)]
|
37
|
+
struct rb_native_thread {
|
38
|
+
_padding_serial: [c_char; 4], // rb_atomic_t
|
39
|
+
_padding_vm: *mut c_int, // struct rb_vm_struct
|
40
|
+
thread_id: rb_nativethread_id_t,
|
41
|
+
// ...
|
42
|
+
}
|
43
|
+
|
44
|
+
#[repr(C)]
|
45
|
+
struct rb_thread_struct {
|
46
|
+
_padding_lt_node: [c_char; 16], // struct ccan_list_node
|
47
|
+
_padding_self: VALUE,
|
48
|
+
_padding_ractor: *mut c_int, // rb_ractor_t
|
49
|
+
_padding_vm: *mut c_int, // rb_vm_t
|
50
|
+
nt: *mut rb_native_thread,
|
51
|
+
// ...
|
52
|
+
}
|
53
|
+
type rb_thread_t = rb_thread_struct;
|
54
|
+
|
55
|
+
/// Reimplementation of the internal RTYPEDDATA_TYPE macro.
|
56
|
+
unsafe fn RTYPEDDATA_TYPE(obj: VALUE) -> *const rb_data_type_struct {
|
57
|
+
let typed: *mut RTypedData = obj as *mut RTypedData;
|
58
|
+
(*typed).type_
|
59
|
+
}
|
60
|
+
|
61
|
+
unsafe fn rb_thread_ptr(thread: VALUE) -> *mut rb_thread_t {
|
62
|
+
unsafe { rb_check_typeddata(thread, RTYPEDDATA_TYPE(thread)) as *mut rb_thread_t }
|
63
|
+
}
|
64
|
+
|
65
|
+
pub unsafe fn rb_thread_getcpuclockid(thread: VALUE) -> clockid_t {
|
66
|
+
let mut cid: clockid_t = MaybeUninit::zeroed().assume_init();
|
67
|
+
let pthread_id: pthread_t = (*(*rb_thread_ptr(thread)).nt).thread_id;
|
68
|
+
pthread_getcpuclockid(pthread_id, &mut cid as *mut clockid_t);
|
69
|
+
cid
|
70
|
+
}
|
@@ -0,0 +1,106 @@
|
|
1
|
+
use std::collections::HashSet;
|
2
|
+
use std::str::FromStr;
|
3
|
+
use std::time::Duration;
|
4
|
+
|
5
|
+
use rb_sys::*;
|
6
|
+
|
7
|
+
use crate::util::cstr;
|
8
|
+
|
9
|
+
pub const DEFAULT_SCHEDULER: Scheduler = Scheduler::Signal;
|
10
|
+
pub const DEFAULT_INTERVAL: Duration = Duration::from_millis(49);
|
11
|
+
pub const DEFAULT_TIME_MODE: TimeMode = TimeMode::CpuTime;
|
12
|
+
|
13
|
+
#[derive(Clone, Debug)]
|
14
|
+
pub struct Configuration {
|
15
|
+
pub scheduler: Scheduler,
|
16
|
+
pub interval: Duration,
|
17
|
+
pub time_mode: TimeMode,
|
18
|
+
pub target_ruby_threads: Threads,
|
19
|
+
}
|
20
|
+
|
21
|
+
#[derive(Clone, Debug, PartialEq)]
|
22
|
+
pub enum Scheduler {
|
23
|
+
Signal,
|
24
|
+
TimerThread,
|
25
|
+
}
|
26
|
+
|
27
|
+
impl FromStr for Scheduler {
|
28
|
+
type Err = ();
|
29
|
+
|
30
|
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
31
|
+
match s {
|
32
|
+
"signal" => Ok(Self::Signal),
|
33
|
+
"timer_thread" => Ok(Self::TimerThread),
|
34
|
+
_ => Err(()),
|
35
|
+
}
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
#[derive(Clone, Debug, PartialEq)]
|
40
|
+
pub enum TimeMode {
|
41
|
+
CpuTime,
|
42
|
+
WallTime,
|
43
|
+
}
|
44
|
+
|
45
|
+
impl FromStr for TimeMode {
|
46
|
+
type Err = ();
|
47
|
+
|
48
|
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
49
|
+
match s {
|
50
|
+
"cpu" => Ok(Self::CpuTime),
|
51
|
+
"wall" => Ok(Self::WallTime),
|
52
|
+
_ => Err(()),
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
#[derive(Clone, Debug, PartialEq)]
|
58
|
+
pub enum Threads {
|
59
|
+
All,
|
60
|
+
Targeted(HashSet<VALUE>),
|
61
|
+
}
|
62
|
+
|
63
|
+
impl Configuration {
|
64
|
+
pub fn validate(&self) -> Result<(), String> {
|
65
|
+
if self.scheduler == Scheduler::TimerThread && self.time_mode == TimeMode::CpuTime {
|
66
|
+
return Err("TimerThread scheduler does not support `time_mode: :cpu`.".to_owned());
|
67
|
+
}
|
68
|
+
if self.scheduler == Scheduler::TimerThread && self.target_ruby_threads == Threads::All {
|
69
|
+
return Err(concat!(
|
70
|
+
"TimerThread scheduler does not support `threads: :all` at the moment. ",
|
71
|
+
"Consider using `threads: Thread.list` for watching all threads at profiler start."
|
72
|
+
)
|
73
|
+
.to_owned());
|
74
|
+
}
|
75
|
+
|
76
|
+
Ok(())
|
77
|
+
}
|
78
|
+
|
79
|
+
pub fn to_rb_hash(&self) -> VALUE {
|
80
|
+
let hash: VALUE = unsafe { rb_hash_new() };
|
81
|
+
unsafe {
|
82
|
+
rb_hash_aset(
|
83
|
+
hash,
|
84
|
+
rb_id2sym(rb_intern(cstr!("scheduler"))),
|
85
|
+
rb_id2sym(rb_intern(match self.scheduler {
|
86
|
+
Scheduler::Signal => cstr!("signal"),
|
87
|
+
Scheduler::TimerThread => cstr!("timer_thread"),
|
88
|
+
})),
|
89
|
+
);
|
90
|
+
rb_hash_aset(
|
91
|
+
hash,
|
92
|
+
rb_id2sym(rb_intern(cstr!("interval_ms"))),
|
93
|
+
rb_int2inum(self.interval.as_millis().try_into().unwrap()),
|
94
|
+
);
|
95
|
+
rb_hash_aset(
|
96
|
+
hash,
|
97
|
+
rb_id2sym(rb_intern(cstr!("time_mode"))),
|
98
|
+
rb_id2sym(rb_intern(match self.time_mode {
|
99
|
+
TimeMode::CpuTime => cstr!("cpu"),
|
100
|
+
TimeMode::WallTime => cstr!("wall"),
|
101
|
+
})),
|
102
|
+
);
|
103
|
+
}
|
104
|
+
hash
|
105
|
+
}
|
106
|
+
}
|
@@ -0,0 +1,80 @@
|
|
1
|
+
use std::collections::HashSet;
|
2
|
+
use std::ffi::c_void;
|
3
|
+
use std::mem::ManuallyDrop;
|
4
|
+
use std::ptr::null_mut;
|
5
|
+
use std::rc::Rc;
|
6
|
+
use std::sync::Mutex;
|
7
|
+
|
8
|
+
use rb_sys::*;
|
9
|
+
|
10
|
+
/// A helper to watch new Ruby threads.
|
11
|
+
///
|
12
|
+
/// `NewThreadWatcher` operates on the Events Hooks API.
|
13
|
+
/// Instead of relying on the `THREAD_EVENT_STARTED` event, it combines the
|
14
|
+
/// `THREAD_EVENT_RESUMED` event and an internal _known-threads_ record.
|
15
|
+
///
|
16
|
+
/// This is to support operations requiring the underlying pthread. Ruby Threads
|
17
|
+
/// are not guaranteed to be fully initialized at the time
|
18
|
+
/// `THREAD_EVENT_STARTED` is triggered; i.e. the underlying pthread has not
|
19
|
+
/// been created yet and `Thread#native_thread_id` returns `nil`.
|
20
|
+
pub struct NewThreadWatcher {
|
21
|
+
inner: Rc<Mutex<Inner>>,
|
22
|
+
event_hook: *mut rb_internal_thread_event_hook_t,
|
23
|
+
}
|
24
|
+
|
25
|
+
struct Inner {
|
26
|
+
known_threads: HashSet<VALUE>,
|
27
|
+
on_new_thread: Box<dyn Fn(VALUE)>,
|
28
|
+
}
|
29
|
+
|
30
|
+
impl NewThreadWatcher {
|
31
|
+
pub fn watch<F>(callback: F) -> Self
|
32
|
+
where
|
33
|
+
F: Fn(VALUE) + 'static,
|
34
|
+
{
|
35
|
+
let mut watcher = Self {
|
36
|
+
inner: Rc::new(Mutex::new(Inner {
|
37
|
+
known_threads: HashSet::new(),
|
38
|
+
on_new_thread: Box::new(callback),
|
39
|
+
})),
|
40
|
+
event_hook: null_mut(),
|
41
|
+
};
|
42
|
+
|
43
|
+
let inner_ptr = Rc::into_raw(Rc::clone(&watcher.inner));
|
44
|
+
unsafe {
|
45
|
+
watcher.event_hook = rb_internal_thread_add_event_hook(
|
46
|
+
Some(Self::on_thread_resume),
|
47
|
+
RUBY_INTERNAL_THREAD_EVENT_RESUMED,
|
48
|
+
inner_ptr as *mut c_void,
|
49
|
+
);
|
50
|
+
};
|
51
|
+
|
52
|
+
watcher
|
53
|
+
}
|
54
|
+
|
55
|
+
unsafe extern "C" fn on_thread_resume(
|
56
|
+
_flag: rb_event_flag_t,
|
57
|
+
data: *const rb_internal_thread_event_data,
|
58
|
+
custom_data: *mut c_void,
|
59
|
+
) {
|
60
|
+
let ruby_thread: VALUE = unsafe { (*data).thread };
|
61
|
+
|
62
|
+
// A pointer to Box<Inner> is passed as custom_data
|
63
|
+
let inner = unsafe { ManuallyDrop::new(Box::from_raw(custom_data as *mut Mutex<Inner>)) };
|
64
|
+
let mut inner = inner.lock().unwrap();
|
65
|
+
|
66
|
+
if !inner.known_threads.contains(&ruby_thread) {
|
67
|
+
inner.known_threads.insert(ruby_thread);
|
68
|
+
(inner.on_new_thread)(ruby_thread);
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
impl Drop for NewThreadWatcher {
|
74
|
+
fn drop(&mut self) {
|
75
|
+
log::trace!("Cleaning up event hook");
|
76
|
+
unsafe {
|
77
|
+
rb_internal_thread_remove_event_hook(self.event_hook);
|
78
|
+
}
|
79
|
+
}
|
80
|
+
}
|