pf2 0.3.0 → 0.4.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: 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: []