pf2 0.4.0 → 0.5.1
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 +29 -0
- data/Cargo.lock +2 -2
- data/README.md +15 -3
- 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 +32 -2
- 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 +242 -0
- data/ext/pf2/src/signal_scheduler.rs +105 -221
- data/ext/pf2/src/signal_scheduler_unsupported_platform.rs +39 -0
- 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 -4
- data/ext/pf2/src/signal_scheduler/configuration.rs +0 -31
- data/ext/pf2/src/signal_scheduler/timer_installer.rs +0 -145
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 29a7c92a2a313dec57af9a3427cd723224f64bf53cf74a488253741e1098c12d
|
4
|
+
data.tar.gz: 4958d188fdddec9fb8143a1b864e9c40d989bbb3360745eb0846687cefc97b9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2a2a7ecd0a4cb0d84f022c3ec1e0844e2c537cfad88f91b0051609006d8c2f4d596f15a1fd259709c97c1ffccbae87e96758a4bcfb70293d88fb44d7888f4071
|
7
|
+
data.tar.gz: 28b108cd63c75d7ead6f34fdd88fdaa867a9601a0217899b024176a4e5508ef9dec7395bf7ae77513de6eb4cc765be90e010146bdd114b989b33359e4ac55068
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,34 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
|
4
|
+
## [0.5.1] - 2024-03-25
|
5
|
+
|
6
|
+
### Fixed
|
7
|
+
|
8
|
+
- Fixed compilation on non-Linux environments.
|
9
|
+
|
10
|
+
|
11
|
+
## [0.5.0] - 2024-03-25
|
12
|
+
|
13
|
+
### Added
|
14
|
+
|
15
|
+
- `pf2 serve` subcommand
|
16
|
+
- `pf2 serve -- ruby target.rb`
|
17
|
+
- Profile programs without any change
|
18
|
+
- New option: `threads: :all`
|
19
|
+
- When specified, Pf2 will track all active threads.
|
20
|
+
- `threads: nil` / omitting the `threads` option has the same effect.
|
21
|
+
- Introduce `Pf2::Session` (https://github.com/osyoyu/pf2/pull/16)
|
22
|
+
- `Session` will be responsible for managing Profiles and Schedulers
|
23
|
+
|
24
|
+
### Removed
|
25
|
+
|
26
|
+
- `Pf2::SignalScheduler` and `Pf2::TimerThreadScheduler` are now hidden from Ruby.
|
27
|
+
- `track_all_threads` option is removed in favor of `threads: :all`.
|
28
|
+
|
29
|
+
|
30
|
+
## [0.4.0] - 2024-03-22
|
31
|
+
|
3
32
|
### Added
|
4
33
|
|
5
34
|
- New option: `track_all_threads`
|
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
@@ -13,6 +13,19 @@ Notable Capabilites
|
|
13
13
|
Usage
|
14
14
|
--------
|
15
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
|
+
|
16
29
|
### Profiling
|
17
30
|
|
18
31
|
Pf2 will collect samples every 10 ms of wall time by default.
|
@@ -55,11 +68,10 @@ Pf2 accepts the following configuration keys:
|
|
55
68
|
```rb
|
56
69
|
Pf2.start(
|
57
70
|
interval_ms: 49, # Integer: The sampling interval in milliseconds (default: 49)
|
58
|
-
threads: [], # Array<Thread>: A list of Ruby Threads to be tracked (default: `Thread.list`)
|
59
71
|
time_mode: :cpu, # `:cpu` or `:wall`: The sampling timer's mode
|
60
72
|
# (default: `:cpu` for SignalScheduler, `:wall` for TimerThreadScheduler)
|
61
|
-
|
62
|
-
#
|
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.
|
63
75
|
)
|
64
76
|
```
|
65
77
|
|
data/ext/pf2/src/lib.rs
CHANGED
@@ -9,8 +9,12 @@ mod profile;
|
|
9
9
|
mod profile_serializer;
|
10
10
|
mod ringbuffer;
|
11
11
|
mod sample;
|
12
|
+
mod scheduler;
|
13
|
+
mod session;
|
12
14
|
#[cfg(target_os = "linux")]
|
13
15
|
mod signal_scheduler;
|
16
|
+
#[cfg(not(target_os = "linux"))]
|
17
|
+
mod signal_scheduler_unsupported_platform;
|
14
18
|
mod timer_thread_scheduler;
|
15
19
|
mod util;
|
16
20
|
|
@@ -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
|
}
|
@@ -1,12 +1,42 @@
|
|
1
1
|
#![allow(non_snake_case)]
|
2
2
|
#![allow(non_camel_case_types)]
|
3
3
|
|
4
|
-
use libc::{clockid_t,
|
4
|
+
use libc::{clockid_t, pthread_t};
|
5
5
|
use rb_sys::{rb_check_typeddata, rb_data_type_struct, RTypedData, VALUE};
|
6
|
-
use std::ffi::{c_char, c_int};
|
6
|
+
use std::ffi::{c_char, c_int, c_void};
|
7
7
|
use std::mem::MaybeUninit;
|
8
8
|
|
9
|
+
#[cfg(target_os = "linux")]
|
10
|
+
use libc::pthread_getcpuclockid;
|
11
|
+
|
12
|
+
#[cfg(not(target_os = "linux"))]
|
13
|
+
pub unsafe fn pthread_getcpuclockid(thread: pthread_t, clk_id: *mut clockid_t) -> c_int {
|
14
|
+
unimplemented!()
|
15
|
+
}
|
16
|
+
|
9
17
|
// Types and structs from Ruby 3.4.0.
|
18
|
+
#[repr(C)]
|
19
|
+
pub struct rb_callable_method_entry_struct {
|
20
|
+
/* same fields with rb_method_entry_t */
|
21
|
+
pub flags: VALUE,
|
22
|
+
_padding_defined_class: VALUE,
|
23
|
+
pub def: *mut rb_method_definition_struct,
|
24
|
+
// ...
|
25
|
+
}
|
26
|
+
|
27
|
+
#[repr(C)]
|
28
|
+
pub struct rb_method_definition_struct {
|
29
|
+
pub type_: c_int,
|
30
|
+
_padding: [c_char; 4],
|
31
|
+
pub cfunc: rb_method_cfunc_struct,
|
32
|
+
// ...
|
33
|
+
}
|
34
|
+
|
35
|
+
#[repr(C)]
|
36
|
+
pub struct rb_method_cfunc_struct {
|
37
|
+
pub func: *mut c_void,
|
38
|
+
// ...
|
39
|
+
}
|
10
40
|
|
11
41
|
type rb_nativethread_id_t = libc::pthread_t;
|
12
42
|
|
@@ -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
|
+
}
|
@@ -0,0 +1,90 @@
|
|
1
|
+
use std::ffi::{c_int, c_void};
|
2
|
+
use std::mem;
|
3
|
+
use std::mem::ManuallyDrop;
|
4
|
+
use std::ptr::null_mut;
|
5
|
+
|
6
|
+
use rb_sys::*;
|
7
|
+
|
8
|
+
use crate::util::cstr;
|
9
|
+
|
10
|
+
use super::Session;
|
11
|
+
|
12
|
+
pub struct SessionRubyObject {
|
13
|
+
session: Option<Session>,
|
14
|
+
}
|
15
|
+
|
16
|
+
impl SessionRubyObject {
|
17
|
+
pub unsafe extern "C" fn rb_initialize(
|
18
|
+
argc: c_int,
|
19
|
+
argv: *const VALUE,
|
20
|
+
rbself: VALUE,
|
21
|
+
) -> VALUE {
|
22
|
+
let mut obj = unsafe { Self::get_struct_from(rbself) };
|
23
|
+
obj.session = Some(Session::new_from_rb_initialize(argc, argv, rbself));
|
24
|
+
Qnil.into()
|
25
|
+
}
|
26
|
+
|
27
|
+
pub unsafe extern "C" fn rb_start(rbself: VALUE) -> VALUE {
|
28
|
+
let mut obj = Self::get_struct_from(rbself);
|
29
|
+
match &mut obj.session {
|
30
|
+
Some(session) => session.start(),
|
31
|
+
None => panic!("Session is not initialized"),
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
pub unsafe extern "C" fn rb_stop(rbself: VALUE) -> VALUE {
|
36
|
+
let mut obj = Self::get_struct_from(rbself);
|
37
|
+
match &mut obj.session {
|
38
|
+
Some(session) => session.stop(),
|
39
|
+
None => panic!("Session is not initialized"),
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
// Extract the SessionRubyObject struct from a Ruby object
|
44
|
+
unsafe fn get_struct_from(obj: VALUE) -> ManuallyDrop<Box<Self>> {
|
45
|
+
unsafe {
|
46
|
+
let ptr = rb_check_typeddata(obj, &RBDATA);
|
47
|
+
ManuallyDrop::new(Box::from_raw(ptr as *mut SessionRubyObject))
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
#[allow(non_snake_case)]
|
52
|
+
pub unsafe extern "C" fn rb_alloc(_rbself: VALUE) -> VALUE {
|
53
|
+
let obj = Box::new(SessionRubyObject { session: None });
|
54
|
+
|
55
|
+
let rb_mPf2: VALUE = rb_define_module(cstr!("Pf2"));
|
56
|
+
let rb_cSession = rb_define_class_under(rb_mPf2, cstr!("Session"), rb_cObject);
|
57
|
+
// Wrap the struct into a Ruby object
|
58
|
+
rb_data_typed_object_wrap(rb_cSession, Box::into_raw(obj) as *mut c_void, &RBDATA)
|
59
|
+
}
|
60
|
+
|
61
|
+
unsafe extern "C" fn dmark(ptr: *mut c_void) {
|
62
|
+
let obj = ManuallyDrop::new(Box::from_raw(ptr as *mut SessionRubyObject));
|
63
|
+
if let Some(session) = &obj.session {
|
64
|
+
session.dmark()
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
unsafe extern "C" fn dfree(ptr: *mut c_void) {
|
69
|
+
drop(Box::from_raw(ptr as *mut SessionRubyObject));
|
70
|
+
}
|
71
|
+
|
72
|
+
unsafe extern "C" fn dsize(_: *const c_void) -> size_t {
|
73
|
+
// FIXME: Report something better
|
74
|
+
mem::size_of::<SessionRubyObject>() as size_t
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
static mut RBDATA: rb_data_type_t = rb_data_type_t {
|
79
|
+
wrap_struct_name: cstr!("SessionRubyObject"),
|
80
|
+
function: rb_data_type_struct__bindgen_ty_1 {
|
81
|
+
dmark: Some(SessionRubyObject::dmark),
|
82
|
+
dfree: Some(SessionRubyObject::dfree),
|
83
|
+
dsize: Some(SessionRubyObject::dsize),
|
84
|
+
dcompact: None,
|
85
|
+
reserved: [null_mut(); 1],
|
86
|
+
},
|
87
|
+
parent: null_mut(),
|
88
|
+
data: null_mut(),
|
89
|
+
flags: 0,
|
90
|
+
};
|