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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 929c0f24675525d7d52bd0293ec45b9383579517119baac5b28da2bfe720d7d5
4
- data.tar.gz: e75df94cf2085aea9d218b431e44ccda5f87e537a1c566ea67ae4ef04a03cb8a
3
+ metadata.gz: c4b713a9333a657f9b2259cdbae91e9aabde4312c2791a4dce517ce7d30cc46a
4
+ data.tar.gz: f01a941327ac6f2ce8b116a466a85a3021ffe0f612063b0f1466e7a31b373984
5
5
  SHA512:
6
- metadata.gz: 2e28b5f4cbb99dc101b7f9a9092b1602d682f1001fa1c86a538d0d966a4c670df5fc5ecb971d8a25e093c3c3bdbe115b772ed40be50b8027cbc09fe6664c1db5
7
- data.tar.gz: cf63fc413c60dda6fc0a360d39701cffc36aceeb98dc5d1d92a6e42761fa2ba1d2381e67a9a4af10fcb51fbf6e4b32afc89ff9307370fa542db278c6e76d40ae
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.2.0"
459
+ version = "1.3.0"
460
460
  source = "registry+https://github.com/rust-lang/crates.io-index"
461
- checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
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
- track_new_threads: true # Boolean: Whether to automatically track Threads spawned after profiler start
61
- # (default: false)
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
 
@@ -42,6 +42,7 @@ impl Backtrace {
42
42
  }
43
43
  }
44
44
 
45
+ #[allow(dead_code)] // TODO: Remove this
45
46
  pub fn backtrace_pcinfo<F>(
46
47
  state: &BacktraceState,
47
48
  pc: usize,
data/ext/pf2/src/lib.rs CHANGED
@@ -9,7 +9,11 @@ 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;
14
16
  mod timer_thread_scheduler;
15
17
  mod util;
18
+
19
+ mod ruby_internal_apis;
@@ -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
- // A vec to keep the "programmer's" C stack trace.
96
- // A single PC may be mapped to multiple inlined frames,
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, _symval: usize, _symsize: usize| {
112
+ |_pc: usize, symname: *const c_char, symval: usize, _symsize: usize| {
105
113
  if symname.is_null() {
106
- c_stack.push("(no symbol information)".to_owned());
114
+ c_stack.push(NativeFunctionFrame {
115
+ symbol_name: "(no symbol information)".to_owned(),
116
+ address: None,
117
+ });
107
118
  } else {
108
- c_stack.push(CStr::from_ptr(symname).to_str().unwrap().to_owned());
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
- // Strip the C stack trace:
116
- // - Remove Pf2-related frames which are always captured
117
- // - Remove frames below rb_vm_exec
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.to_string(),
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: CStr::from_ptr(rb_string_value_cstr(
150
- &mut rb_profile_frame_full_label(frame),
151
- ))
152
- .to_str()
153
- .unwrap()
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
 
@@ -2,9 +2,7 @@
2
2
 
3
3
  use rb_sys::*;
4
4
 
5
- #[cfg(target_os = "linux")]
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
- #[cfg(target_os = "linux")]
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
- rb_mPf2_TimerThreadScheduler,
25
+ rb_mPf2_Session,
57
26
  cstr!("initialize"),
58
- Some(to_ruby_cfunc_with_args(TimerThreadScheduler::rb_initialize)),
27
+ Some(to_ruby_cfunc_with_args(SessionRubyObject::rb_initialize)),
59
28
  -1,
60
29
  );
61
30
  rb_define_method(
62
- rb_mPf2_TimerThreadScheduler,
31
+ rb_mPf2_Session,
63
32
  cstr!("start"),
64
- Some(to_ruby_cfunc_with_no_args(TimerThreadScheduler::rb_start)),
33
+ Some(to_ruby_cfunc_with_no_args(SessionRubyObject::rb_start)),
65
34
  0,
66
35
  );
67
36
  rb_define_method(
68
- rb_mPf2_TimerThreadScheduler,
37
+ rb_mPf2_Session,
69
38
  cstr!("stop"),
70
- Some(to_ruby_cfunc_with_no_args(TimerThreadScheduler::rb_stop)),
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,10 @@
1
+ use rb_sys::{size_t, VALUE};
2
+
3
+ pub trait Scheduler {
4
+ fn start(&self) -> VALUE;
5
+ fn stop(&self) -> VALUE;
6
+ fn on_new_thread(&self, thread: VALUE);
7
+ fn dmark(&self);
8
+ fn dfree(&self);
9
+ fn dsize(&self) -> size_t;
10
+ }
@@ -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
+ }