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