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
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
|
+
}
|