pf2 0.3.0 → 0.4.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: d576aec57e600cc77194fabb8c1b7b3c782c4ab76a03ec3b0f24581eb4c317ae
4
+ data.tar.gz: a290d23df802601977622f1afe39f1f2d6cd92a9350b25d203e97c3b6da6db16
5
5
  SHA512:
6
- metadata.gz: 2e28b5f4cbb99dc101b7f9a9092b1602d682f1001fa1c86a538d0d966a4c670df5fc5ecb971d8a25e093c3c3bdbe115b772ed40be50b8027cbc09fe6664c1db5
7
- data.tar.gz: cf63fc413c60dda6fc0a360d39701cffc36aceeb98dc5d1d92a6e42761fa2ba1d2381e67a9a4af10fcb51fbf6e4b32afc89ff9307370fa542db278c6e76d40ae
6
+ metadata.gz: a8d72fa4ccb3395f0fdd9203885c1e0cb8a00429597a6dd756e80fb70e0d8092c084992bd9595ee21789a4aaf2550e2284b6d3f81b448dc330088b325d822d85
7
+ data.tar.gz: 8f93c98574ef7077336232bb1b51a4af1ac4ba2561914a299209a5c2879fe9af3e8998c8df264836f698fea05c8d028f7ae985de3c18e9889f05568ec2740bc1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ### Added
4
+
5
+ - New option: `track_all_threads`
6
+ - When true, all Threads will be tracked regardless of the `threads` option.
7
+
8
+ ### Removed
9
+
10
+ - The `track_new_threads` option was removed in favor of the `track_all_threads` option.
11
+
3
12
 
4
13
  ## [0.3.0] - 2024-02-05
5
14
 
data/README.md CHANGED
@@ -8,6 +8,7 @@ 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
  --------
@@ -57,7 +58,7 @@ Pf2.start(
57
58
  threads: [], # Array<Thread>: A list of Ruby Threads to be tracked (default: `Thread.list`)
58
59
  time_mode: :cpu, # `:cpu` or `:wall`: The sampling timer's mode
59
60
  # (default: `:cpu` for SignalScheduler, `:wall` for TimerThreadScheduler)
60
- track_new_threads: true # Boolean: Whether to automatically track Threads spawned after profiler start
61
+ track_all_threads: true # Boolean: Whether to track all Threads regardless of `threads` option
61
62
  # (default: false)
62
63
  )
63
64
  ```
@@ -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
@@ -13,3 +13,5 @@ mod sample;
13
13
  mod signal_scheduler;
14
14
  mod timer_thread_scheduler;
15
15
  mod util;
16
+
17
+ mod ruby_internal_apis;
@@ -0,0 +1,47 @@
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};
7
+ use std::mem::MaybeUninit;
8
+
9
+ // Types and structs from Ruby 3.4.0.
10
+
11
+ type rb_nativethread_id_t = libc::pthread_t;
12
+
13
+ #[repr(C)]
14
+ struct rb_native_thread {
15
+ _padding_serial: [c_char; 4], // rb_atomic_t
16
+ _padding_vm: *mut c_int, // struct rb_vm_struct
17
+ thread_id: rb_nativethread_id_t,
18
+ // ...
19
+ }
20
+
21
+ #[repr(C)]
22
+ struct rb_thread_struct {
23
+ _padding_lt_node: [c_char; 16], // struct ccan_list_node
24
+ _padding_self: VALUE,
25
+ _padding_ractor: *mut c_int, // rb_ractor_t
26
+ _padding_vm: *mut c_int, // rb_vm_t
27
+ nt: *mut rb_native_thread,
28
+ // ...
29
+ }
30
+ type rb_thread_t = rb_thread_struct;
31
+
32
+ /// Reimplementation of the internal RTYPEDDATA_TYPE macro.
33
+ unsafe fn RTYPEDDATA_TYPE(obj: VALUE) -> *const rb_data_type_struct {
34
+ let typed: *mut RTypedData = obj as *mut RTypedData;
35
+ (*typed).type_
36
+ }
37
+
38
+ unsafe fn rb_thread_ptr(thread: VALUE) -> *mut rb_thread_t {
39
+ unsafe { rb_check_typeddata(thread, RTYPEDDATA_TYPE(thread)) as *mut rb_thread_t }
40
+ }
41
+
42
+ pub unsafe fn rb_thread_getcpuclockid(thread: VALUE) -> clockid_t {
43
+ let mut cid: clockid_t = MaybeUninit::zeroed().assume_init();
44
+ let pthread_id: pthread_t = (*(*rb_thread_ptr(thread)).nt).thread_id;
45
+ pthread_getcpuclockid(pthread_id, &mut cid as *mut clockid_t);
46
+ cid
47
+ }
@@ -9,7 +9,7 @@ pub struct Configuration {
9
9
  pub interval: Duration,
10
10
  pub time_mode: TimeMode,
11
11
  pub target_ruby_threads: HashSet<VALUE>,
12
- pub track_new_threads: bool,
12
+ pub track_all_threads: bool,
13
13
  }
14
14
 
15
15
  #[derive(Clone, Debug)]
@@ -1,31 +1,28 @@
1
- use std::collections::HashMap;
1
+ use std::collections::HashSet;
2
2
  use std::ffi::c_void;
3
3
  use std::mem;
4
4
  use std::mem::ManuallyDrop;
5
5
  use std::ptr::null_mut;
6
+ use std::sync::Arc;
6
7
  use std::sync::{Mutex, RwLock};
7
- use std::{collections::HashSet, sync::Arc};
8
8
 
9
9
  use rb_sys::*;
10
10
 
11
- use crate::signal_scheduler::SignalHandlerArgs;
12
-
13
11
  use super::configuration::Configuration;
14
12
  use crate::profile::Profile;
13
+ use crate::ruby_internal_apis::rb_thread_getcpuclockid;
14
+ use crate::signal_scheduler::{cstr, SignalHandlerArgs};
15
15
 
16
- // We could avoid deferring the timer creation by combining pthread_getcpuclockid(3) and timer_create(2) here,
17
- // but we're not doing so since (1) Ruby does not expose the pthread_self() of a Ruby Thread
18
- // (which is actually stored in th->nt->thread_id), and (2) pthread_getcpuclockid(3) is not portable
19
- // in the first place (e.g. not available on macOS).
16
+ #[derive(Debug)]
20
17
  pub struct TimerInstaller {
21
- internal: Box<Mutex<Internal>>,
18
+ inner: Box<Mutex<Inner>>,
22
19
  }
23
20
 
24
- struct Internal {
21
+ #[derive(Debug)]
22
+ struct Inner {
25
23
  configuration: Configuration,
26
- registered_pthread_ids: HashSet<libc::pthread_t>,
27
- kernel_thread_id_to_ruby_thread_map: HashMap<libc::pid_t, VALUE>,
28
- profile: Arc<RwLock<Profile>>,
24
+ pub profile: Arc<RwLock<Profile>>,
25
+ known_threads: HashSet<VALUE>,
29
26
  }
30
27
 
31
28
  impl TimerInstaller {
@@ -35,153 +32,102 @@ impl TimerInstaller {
35
32
  configuration: Configuration,
36
33
  profile: Arc<RwLock<Profile>>,
37
34
  ) {
38
- let registrar = Self {
39
- internal: Box::new(Mutex::new(Internal {
35
+ let installer = Self {
36
+ inner: Box::new(Mutex::new(Inner {
40
37
  configuration: configuration.clone(),
41
- registered_pthread_ids: HashSet::new(),
42
- kernel_thread_id_to_ruby_thread_map: HashMap::new(),
43
38
  profile,
39
+ known_threads: HashSet::new(),
44
40
  })),
45
41
  };
46
42
 
47
- let ptr = Box::into_raw(registrar.internal);
48
- unsafe {
49
- rb_internal_thread_add_event_hook(
50
- Some(Self::on_thread_resume),
51
- RUBY_INTERNAL_THREAD_EVENT_RESUMED,
52
- ptr as *mut c_void,
53
- );
54
- // Spawn a no-op Thread to fire the event hook
55
- // (at least 2 Ruby Threads must be active for the RESUMED hook to be fired)
56
- rb_thread_create(Some(Self::do_nothing), null_mut());
57
- };
43
+ if let Ok(mut inner) = installer.inner.try_lock() {
44
+ for ruby_thread in configuration.target_ruby_threads.iter() {
45
+ let ruby_thread: VALUE = *ruby_thread;
46
+ inner.known_threads.insert(ruby_thread);
47
+ inner.register_timer_to_ruby_thread(ruby_thread);
48
+ }
49
+ }
58
50
 
59
- if configuration.track_new_threads {
51
+ if configuration.track_all_threads {
52
+ let ptr = Box::into_raw(installer.inner);
60
53
  unsafe {
54
+ // TODO: Clean up this hook when the profiling session ends
61
55
  rb_internal_thread_add_event_hook(
62
- Some(Self::on_thread_start),
63
- RUBY_INTERNAL_THREAD_EVENT_STARTED,
56
+ Some(Self::on_thread_resume),
57
+ RUBY_INTERNAL_THREAD_EVENT_RESUMED,
64
58
  ptr as *mut c_void,
65
59
  );
66
60
  };
67
61
  }
68
62
  }
69
63
 
70
- unsafe extern "C" fn do_nothing(_: *mut c_void) -> VALUE {
71
- Qnil.into()
72
- }
73
-
74
- // Thread resume callback
64
+ // Thread start callback
75
65
  unsafe extern "C" fn on_thread_resume(
76
66
  _flag: rb_event_flag_t,
77
67
  data: *const rb_internal_thread_event_data,
78
68
  custom_data: *mut c_void,
79
69
  ) {
80
- // The SignalScheduler (as a Ruby obj) should be passed as custom_data
81
- let internal =
82
- unsafe { ManuallyDrop::new(Box::from_raw(custom_data as *mut Mutex<Internal>)) };
83
- let mut internal = internal.lock().unwrap();
84
-
85
- // Check if the current thread is a target Ruby Thread
86
- let current_ruby_thread: VALUE = unsafe { (*data).thread };
87
- if !internal
88
- .configuration
89
- .target_ruby_threads
90
- .contains(&current_ruby_thread)
91
- {
92
- return;
93
- }
70
+ let ruby_thread: VALUE = unsafe { (*data).thread };
94
71
 
95
- // Check if the current thread is already registered
96
- let current_pthread_id = unsafe { libc::pthread_self() };
97
- if internal
98
- .registered_pthread_ids
99
- .contains(&current_pthread_id)
100
- {
101
- return;
102
- }
103
-
104
- // Record the pthread ID of the current thread
105
- internal.registered_pthread_ids.insert(current_pthread_id);
106
- // Keep a mapping from kernel thread ID to Ruby Thread
107
- internal
108
- .kernel_thread_id_to_ruby_thread_map
109
- .insert(unsafe { libc::gettid() }, current_ruby_thread);
110
-
111
- Self::register_timer_to_current_thread(
112
- &internal.configuration,
113
- &internal.profile,
114
- &internal.kernel_thread_id_to_ruby_thread_map,
115
- );
116
-
117
- // TODO: Remove the hook when all threads have been registered
118
- }
72
+ // A pointer to Box<Inner> is passed as custom_data
73
+ let inner = unsafe { ManuallyDrop::new(Box::from_raw(custom_data as *mut Mutex<Inner>)) };
74
+ let mut inner = inner.lock().unwrap();
119
75
 
120
- // Thread resume callback
121
- unsafe extern "C" fn on_thread_start(
122
- _flag: rb_event_flag_t,
123
- data: *const rb_internal_thread_event_data,
124
- custom_data: *mut c_void,
125
- ) {
126
- // The SignalScheduler (as a Ruby obj) should be passed as custom_data
127
- let internal =
128
- unsafe { ManuallyDrop::new(Box::from_raw(custom_data as *mut Mutex<Internal>)) };
129
- let mut internal = internal.lock().unwrap();
130
-
131
- let current_ruby_thread: VALUE = unsafe { (*data).thread };
132
- internal
133
- .configuration
134
- .target_ruby_threads
135
- .insert(current_ruby_thread);
76
+ if !inner.known_threads.contains(&ruby_thread) {
77
+ inner.known_threads.insert(ruby_thread);
78
+ // Install a timer for the thread
79
+ inner.register_timer_to_ruby_thread(ruby_thread);
80
+ }
136
81
  }
82
+ }
137
83
 
138
- // Creates a new POSIX timer which invocates sampling for the thread that called this function.
139
- fn register_timer_to_current_thread(
140
- configuration: &Configuration,
141
- profile: &Arc<RwLock<Profile>>,
142
- kernel_thread_id_to_ruby_thread_map: &HashMap<libc::pid_t, VALUE>,
143
- ) {
144
- let current_pthread_id = unsafe { libc::pthread_self() };
145
- let context_ruby_thread: VALUE = unsafe {
146
- *(kernel_thread_id_to_ruby_thread_map
147
- .get(&(libc::gettid()))
148
- .unwrap())
149
- };
150
-
84
+ impl Inner {
85
+ fn register_timer_to_ruby_thread(&self, ruby_thread: VALUE) {
151
86
  // NOTE: This Box is never dropped
152
87
  let signal_handler_args = Box::new(SignalHandlerArgs {
153
- profile: Arc::clone(profile),
154
- context_ruby_thread,
88
+ profile: Arc::clone(&self.profile),
89
+ context_ruby_thread: ruby_thread,
155
90
  });
156
91
 
92
+ // rb_funcall deadlocks when called within a THREAD_EVENT_STARTED hook
93
+ let kernel_thread_id: i32 = i32::try_from(unsafe {
94
+ rb_num2int(rb_funcall(
95
+ ruby_thread,
96
+ rb_intern(cstr!("native_thread_id")), // kernel thread ID
97
+ 0,
98
+ ))
99
+ })
100
+ .unwrap();
101
+
157
102
  // Create a signal event
158
103
  let mut sigevent: libc::sigevent = unsafe { mem::zeroed() };
159
104
  // Note: SIGEV_THREAD_ID is Linux-specific. In other platforms, we would need to
160
- // "tranpoline" the signal as any pthread can receive the signal.
105
+ // "trampoline" the signal as any pthread can receive the signal.
161
106
  sigevent.sigev_notify = libc::SIGEV_THREAD_ID;
162
- sigevent.sigev_notify_thread_id =
163
- unsafe { libc::syscall(libc::SYS_gettid).try_into().unwrap() }; // The kernel thread ID
107
+ sigevent.sigev_notify_thread_id = kernel_thread_id;
164
108
  sigevent.sigev_signo = libc::SIGALRM;
165
109
  // Pass required args to the signal handler
166
110
  sigevent.sigev_value.sival_ptr = Box::into_raw(signal_handler_args) as *mut c_void;
167
111
 
168
112
  // Create and configure timer to fire every _interval_ ms of CPU time
169
113
  let mut timer: libc::timer_t = unsafe { mem::zeroed() };
170
- let clockid = match configuration.time_mode {
171
- crate::signal_scheduler::TimeMode::CpuTime => libc::CLOCK_THREAD_CPUTIME_ID,
114
+ let clockid = match self.configuration.time_mode {
115
+ crate::signal_scheduler::TimeMode::CpuTime => unsafe {
116
+ rb_thread_getcpuclockid(ruby_thread)
117
+ },
172
118
  crate::signal_scheduler::TimeMode::WallTime => libc::CLOCK_MONOTONIC,
173
119
  };
174
120
  let err = unsafe { libc::timer_create(clockid, &mut sigevent, &mut timer) };
175
121
  if err != 0 {
176
122
  panic!("timer_create failed: {}", err);
177
123
  }
178
- let itimerspec = Self::duration_to_itimerspec(&configuration.interval);
124
+ let itimerspec = Self::duration_to_itimerspec(&self.configuration.interval);
179
125
  let err = unsafe { libc::timer_settime(timer, 0, &itimerspec, null_mut()) };
180
126
  if err != 0 {
181
127
  panic!("timer_settime failed: {}", err);
182
128
  }
183
129
 
184
- log::debug!("timer registered for thread {}", current_pthread_id);
130
+ log::debug!("timer registered for thread {}", ruby_thread);
185
131
  }
186
132
 
187
133
  fn duration_to_itimerspec(duration: &std::time::Duration) -> libc::itimerspec {
@@ -56,7 +56,7 @@ impl SignalScheduler {
56
56
  rb_intern(cstr!("interval_ms")),
57
57
  rb_intern(cstr!("threads")),
58
58
  rb_intern(cstr!("time_mode")),
59
- rb_intern(cstr!("track_new_threads")),
59
+ rb_intern(cstr!("track_all_threads")),
60
60
  ]
61
61
  .as_mut_ptr(),
62
62
  0,
@@ -99,7 +99,7 @@ impl SignalScheduler {
99
99
  } else {
100
100
  configuration::TimeMode::CpuTime
101
101
  };
102
- let track_new_threads: bool = if kwargs_values[3] != Qundef as VALUE {
102
+ let track_all_threads: bool = if kwargs_values[3] != Qundef as VALUE {
103
103
  RTEST(kwargs_values[3])
104
104
  } else {
105
105
  false
@@ -117,7 +117,7 @@ impl SignalScheduler {
117
117
  interval,
118
118
  target_ruby_threads,
119
119
  time_mode,
120
- track_new_threads,
120
+ track_all_threads,
121
121
  });
122
122
 
123
123
  Qnil.into()
data/lib/pf2/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pf2
2
- VERSION = '0.3.0'
2
+ VERSION = '0.4.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pf2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daisuke Aritomo
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-02-05 00:00:00.000000000 Z
10
+ date: 2024-03-22 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rake-compiler
@@ -52,7 +51,6 @@ dependencies:
52
51
  - - ">="
53
52
  - !ruby/object:Gem::Version
54
53
  version: '0'
55
- description:
56
54
  email:
57
55
  - osyoyu@osyoyu.com
58
56
  executables:
@@ -156,6 +154,7 @@ files:
156
154
  - ext/pf2/src/profile_serializer.rs
157
155
  - ext/pf2/src/ringbuffer.rs
158
156
  - ext/pf2/src/ruby_init.rs
157
+ - ext/pf2/src/ruby_internal_apis.rs
159
158
  - ext/pf2/src/sample.rs
160
159
  - ext/pf2/src/siginfo_t.c
161
160
  - ext/pf2/src/signal_scheduler.rs
@@ -175,7 +174,6 @@ metadata:
175
174
  homepage_uri: https://github.com/osyoyu/pf2
176
175
  source_code_uri: https://github.com/osyoyu/pf2
177
176
  changelog_uri: https://github.com/osyoyu/pf2/blob/master/CHANGELOG.md
178
- post_install_message:
179
177
  rdoc_options: []
180
178
  require_paths:
181
179
  - lib
@@ -191,7 +189,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
191
189
  version: '0'
192
190
  requirements: []
193
191
  rubygems_version: 3.6.0.dev
194
- signing_key:
195
192
  specification_version: 4
196
193
  summary: Yet another Ruby profiler
197
194
  test_files: []