pf2 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +7 -0
- data/Cargo.lock +481 -0
- data/Cargo.toml +3 -0
- data/README.md +99 -13
- data/ext/pf2/Cargo.toml +24 -0
- data/ext/pf2/build.rs +3 -0
- data/ext/pf2/extconf.rb +6 -1
- data/ext/pf2/src/lib.rs +14 -0
- data/ext/pf2/src/profile.rs +50 -0
- data/ext/pf2/src/profile_serializer.rs +130 -0
- data/ext/pf2/src/ringbuffer.rs +145 -0
- data/ext/pf2/src/ruby_init.rs +62 -0
- data/ext/pf2/src/sample.rs +45 -0
- data/ext/pf2/src/siginfo_t.c +5 -0
- data/ext/pf2/src/signal_scheduler/configuration.rs +24 -0
- data/ext/pf2/src/signal_scheduler/timer_installer.rs +192 -0
- data/ext/pf2/src/signal_scheduler.rs +242 -0
- data/ext/pf2/src/timer_thread_scheduler.rs +243 -0
- data/ext/pf2/src/util.rs +30 -0
- data/lib/pf2/cli.rb +1 -1
- data/lib/pf2/reporter.rb +36 -11
- data/lib/pf2/version.rb +1 -1
- data/lib/pf2.rb +23 -5
- metadata +34 -5
- data/ext/pf2/pf2.c +0 -246
@@ -0,0 +1,242 @@
|
|
1
|
+
#![deny(unsafe_op_in_unsafe_fn)]
|
2
|
+
|
3
|
+
mod configuration;
|
4
|
+
mod timer_installer;
|
5
|
+
|
6
|
+
use self::configuration::{Configuration, TimeMode};
|
7
|
+
use self::timer_installer::TimerInstaller;
|
8
|
+
use crate::profile::Profile;
|
9
|
+
use crate::profile_serializer::ProfileSerializer;
|
10
|
+
use crate::sample::Sample;
|
11
|
+
|
12
|
+
use core::panic;
|
13
|
+
use std::collections::HashSet;
|
14
|
+
use std::ffi::{c_int, c_void, CString};
|
15
|
+
use std::mem::ManuallyDrop;
|
16
|
+
use std::sync::{Arc, RwLock};
|
17
|
+
use std::thread;
|
18
|
+
use std::time::Duration;
|
19
|
+
use std::{mem, ptr::null_mut};
|
20
|
+
|
21
|
+
use rb_sys::*;
|
22
|
+
|
23
|
+
use crate::util::*;
|
24
|
+
|
25
|
+
#[derive(Debug)]
|
26
|
+
pub struct SignalScheduler {
|
27
|
+
configuration: configuration::Configuration,
|
28
|
+
profile: Option<Arc<RwLock<Profile>>>,
|
29
|
+
}
|
30
|
+
|
31
|
+
pub struct SignalHandlerArgs {
|
32
|
+
profile: Arc<RwLock<Profile>>,
|
33
|
+
context_ruby_thread: VALUE,
|
34
|
+
}
|
35
|
+
|
36
|
+
impl SignalScheduler {
|
37
|
+
fn new() -> Self {
|
38
|
+
Self {
|
39
|
+
configuration: Configuration {
|
40
|
+
time_mode: TimeMode::CpuTime,
|
41
|
+
},
|
42
|
+
profile: None,
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
fn start(
|
47
|
+
&mut self,
|
48
|
+
_rbself: VALUE,
|
49
|
+
ruby_threads_rary: VALUE,
|
50
|
+
track_new_threads: VALUE,
|
51
|
+
) -> VALUE {
|
52
|
+
let track_new_threads = RTEST(track_new_threads);
|
53
|
+
|
54
|
+
let profile = Arc::new(RwLock::new(Profile::new()));
|
55
|
+
self.start_profile_buffer_flusher_thread(&profile);
|
56
|
+
self.install_signal_handler();
|
57
|
+
|
58
|
+
let mut target_ruby_threads = HashSet::new();
|
59
|
+
unsafe {
|
60
|
+
for i in 0..RARRAY_LEN(ruby_threads_rary) {
|
61
|
+
let ruby_thread: VALUE = rb_ary_entry(ruby_threads_rary, i);
|
62
|
+
target_ruby_threads.insert(ruby_thread);
|
63
|
+
}
|
64
|
+
}
|
65
|
+
TimerInstaller::install_timer_to_ruby_threads(
|
66
|
+
self.configuration.clone(),
|
67
|
+
&target_ruby_threads,
|
68
|
+
Arc::clone(&profile),
|
69
|
+
track_new_threads,
|
70
|
+
);
|
71
|
+
|
72
|
+
self.profile = Some(profile);
|
73
|
+
|
74
|
+
Qtrue.into()
|
75
|
+
}
|
76
|
+
|
77
|
+
fn stop(&mut self, _rbself: VALUE) -> VALUE {
|
78
|
+
if let Some(profile) = &self.profile {
|
79
|
+
// Finalize
|
80
|
+
match profile.try_write() {
|
81
|
+
Ok(mut profile) => {
|
82
|
+
profile.flush_temporary_sample_buffer();
|
83
|
+
}
|
84
|
+
Err(_) => {
|
85
|
+
println!("[pf2 ERROR] stop: Failed to acquire profile lock.");
|
86
|
+
return Qfalse.into();
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
let profile = profile.try_read().unwrap();
|
91
|
+
log::debug!("Number of samples: {}", profile.samples.len());
|
92
|
+
|
93
|
+
let serialized = ProfileSerializer::serialize(&profile);
|
94
|
+
let serialized = CString::new(serialized).unwrap();
|
95
|
+
unsafe { rb_str_new_cstr(serialized.as_ptr()) }
|
96
|
+
} else {
|
97
|
+
panic!("stop() called before start()");
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
// Install signal handler for profiling events to the current process.
|
102
|
+
fn install_signal_handler(&self) {
|
103
|
+
let mut sa: libc::sigaction = unsafe { mem::zeroed() };
|
104
|
+
sa.sa_sigaction = Self::signal_handler as usize;
|
105
|
+
sa.sa_flags = libc::SA_SIGINFO;
|
106
|
+
let err = unsafe { libc::sigaction(libc::SIGALRM, &sa, null_mut()) };
|
107
|
+
if err != 0 {
|
108
|
+
panic!("sigaction failed: {}", err);
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
// Respond to the signal and collect a sample.
|
113
|
+
// This function is called when a timer fires.
|
114
|
+
//
|
115
|
+
// Expected to be async-signal-safe, but the current implementation is not.
|
116
|
+
extern "C" fn signal_handler(
|
117
|
+
_sig: c_int,
|
118
|
+
info: *mut libc::siginfo_t,
|
119
|
+
_ucontext: *mut libc::ucontext_t,
|
120
|
+
) {
|
121
|
+
let args = unsafe {
|
122
|
+
let ptr = extract_si_value_sival_ptr(info) as *mut SignalHandlerArgs;
|
123
|
+
ManuallyDrop::new(Box::from_raw(ptr))
|
124
|
+
};
|
125
|
+
|
126
|
+
let mut profile = match args.profile.try_write() {
|
127
|
+
Ok(profile) => profile,
|
128
|
+
Err(_) => {
|
129
|
+
// FIXME: Do we want to properly collect GC samples? I don't know yet.
|
130
|
+
log::trace!("Failed to acquire profile lock (garbage collection possibly in progress). Dropping sample.");
|
131
|
+
return;
|
132
|
+
}
|
133
|
+
};
|
134
|
+
|
135
|
+
let sample = Sample::capture(args.context_ruby_thread); // NOT async-signal-safe
|
136
|
+
if profile.temporary_sample_buffer.push(sample).is_err() {
|
137
|
+
log::debug!("Temporary sample buffer full. Dropping sample.");
|
138
|
+
}
|
139
|
+
}
|
140
|
+
|
141
|
+
fn start_profile_buffer_flusher_thread(&self, profile: &Arc<RwLock<Profile>>) {
|
142
|
+
let profile = Arc::clone(profile);
|
143
|
+
thread::spawn(move || loop {
|
144
|
+
log::trace!("Flushing temporary sample buffer");
|
145
|
+
match profile.try_write() {
|
146
|
+
Ok(mut profile) => {
|
147
|
+
profile.flush_temporary_sample_buffer();
|
148
|
+
}
|
149
|
+
Err(_) => {
|
150
|
+
log::debug!("flusher: Failed to acquire profile lock");
|
151
|
+
}
|
152
|
+
}
|
153
|
+
thread::sleep(Duration::from_millis(500));
|
154
|
+
});
|
155
|
+
}
|
156
|
+
|
157
|
+
// Ruby Methods
|
158
|
+
|
159
|
+
pub unsafe extern "C" fn rb_start(
|
160
|
+
rbself: VALUE,
|
161
|
+
ruby_threads: VALUE,
|
162
|
+
track_new_threads: VALUE,
|
163
|
+
) -> VALUE {
|
164
|
+
let mut collector = unsafe { Self::get_struct_from(rbself) };
|
165
|
+
collector.start(rbself, ruby_threads, track_new_threads)
|
166
|
+
}
|
167
|
+
|
168
|
+
pub unsafe extern "C" fn rb_stop(rbself: VALUE) -> VALUE {
|
169
|
+
let mut collector = unsafe { Self::get_struct_from(rbself) };
|
170
|
+
collector.stop(rbself)
|
171
|
+
}
|
172
|
+
|
173
|
+
// Functions for TypedData
|
174
|
+
|
175
|
+
// Extract the SignalScheduler struct from a Ruby object
|
176
|
+
unsafe fn get_struct_from(obj: VALUE) -> ManuallyDrop<Box<Self>> {
|
177
|
+
unsafe {
|
178
|
+
let ptr = rb_check_typeddata(obj, &RBDATA);
|
179
|
+
ManuallyDrop::new(Box::from_raw(ptr as *mut SignalScheduler))
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
#[allow(non_snake_case)]
|
184
|
+
pub unsafe extern "C" fn rb_alloc(_rbself: VALUE) -> VALUE {
|
185
|
+
let collector = Box::new(SignalScheduler::new());
|
186
|
+
unsafe { Arc::increment_strong_count(&collector) };
|
187
|
+
|
188
|
+
unsafe {
|
189
|
+
let rb_mPf2: VALUE = rb_define_module(cstr!("Pf2"));
|
190
|
+
let rb_cSignalScheduler =
|
191
|
+
rb_define_class_under(rb_mPf2, cstr!("SignalScheduler"), rb_cObject);
|
192
|
+
|
193
|
+
// "Wrap" the SignalScheduler struct into a Ruby object
|
194
|
+
rb_data_typed_object_wrap(
|
195
|
+
rb_cSignalScheduler,
|
196
|
+
Box::into_raw(collector) as *mut c_void,
|
197
|
+
&RBDATA,
|
198
|
+
)
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
unsafe extern "C" fn dmark(ptr: *mut c_void) {
|
203
|
+
unsafe {
|
204
|
+
let collector = ManuallyDrop::new(Box::from_raw(ptr as *mut SignalScheduler));
|
205
|
+
if let Some(profile) = &collector.profile {
|
206
|
+
match profile.read() {
|
207
|
+
Ok(profile) => {
|
208
|
+
profile.dmark();
|
209
|
+
}
|
210
|
+
Err(_) => {
|
211
|
+
panic!("[pf2 FATAL] dmark: Failed to acquire profile lock.");
|
212
|
+
}
|
213
|
+
}
|
214
|
+
}
|
215
|
+
}
|
216
|
+
}
|
217
|
+
|
218
|
+
unsafe extern "C" fn dfree(ptr: *mut c_void) {
|
219
|
+
unsafe {
|
220
|
+
drop(Box::from_raw(ptr as *mut SignalScheduler));
|
221
|
+
}
|
222
|
+
}
|
223
|
+
|
224
|
+
unsafe extern "C" fn dsize(_: *const c_void) -> size_t {
|
225
|
+
// FIXME: Report something better
|
226
|
+
mem::size_of::<SignalScheduler>() as size_t
|
227
|
+
}
|
228
|
+
}
|
229
|
+
|
230
|
+
static mut RBDATA: rb_data_type_t = rb_data_type_t {
|
231
|
+
wrap_struct_name: cstr!("SignalScheduler"),
|
232
|
+
function: rb_data_type_struct__bindgen_ty_1 {
|
233
|
+
dmark: Some(SignalScheduler::dmark),
|
234
|
+
dfree: Some(SignalScheduler::dfree),
|
235
|
+
dsize: Some(SignalScheduler::dsize),
|
236
|
+
dcompact: None,
|
237
|
+
reserved: [null_mut(); 1],
|
238
|
+
},
|
239
|
+
parent: null_mut(),
|
240
|
+
data: null_mut(),
|
241
|
+
flags: 0,
|
242
|
+
};
|
@@ -0,0 +1,243 @@
|
|
1
|
+
#![deny(unsafe_op_in_unsafe_fn)]
|
2
|
+
|
3
|
+
use std::ffi::{c_void, CString};
|
4
|
+
use std::mem::ManuallyDrop;
|
5
|
+
use std::ptr::null_mut;
|
6
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
7
|
+
use std::sync::{Arc, RwLock};
|
8
|
+
use std::thread;
|
9
|
+
use std::time::Duration;
|
10
|
+
|
11
|
+
use rb_sys::*;
|
12
|
+
|
13
|
+
use crate::profile::Profile;
|
14
|
+
use crate::profile_serializer::ProfileSerializer;
|
15
|
+
use crate::sample::Sample;
|
16
|
+
use crate::util::*;
|
17
|
+
|
18
|
+
#[derive(Clone, Debug)]
|
19
|
+
pub struct TimerThreadScheduler {
|
20
|
+
ruby_threads: Arc<RwLock<Vec<VALUE>>>,
|
21
|
+
profile: Option<Arc<RwLock<Profile>>>,
|
22
|
+
stop_requested: Arc<AtomicBool>,
|
23
|
+
}
|
24
|
+
|
25
|
+
#[derive(Debug)]
|
26
|
+
struct PostponedJobArgs {
|
27
|
+
ruby_threads: Arc<RwLock<Vec<VALUE>>>,
|
28
|
+
profile: Arc<RwLock<Profile>>,
|
29
|
+
}
|
30
|
+
|
31
|
+
impl TimerThreadScheduler {
|
32
|
+
fn new() -> Self {
|
33
|
+
TimerThreadScheduler {
|
34
|
+
ruby_threads: Arc::new(RwLock::new(vec![])),
|
35
|
+
profile: None,
|
36
|
+
stop_requested: Arc::new(AtomicBool::new(false)),
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
fn start(&mut self, _rbself: VALUE, ruby_threads: VALUE) -> VALUE {
|
41
|
+
// Register threads
|
42
|
+
let stored_threads = &mut self.ruby_threads.try_write().unwrap();
|
43
|
+
unsafe {
|
44
|
+
for i in 0..RARRAY_LEN(ruby_threads) {
|
45
|
+
stored_threads.push(rb_ary_entry(ruby_threads, i));
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
// Create Profile
|
50
|
+
let profile = Arc::new(RwLock::new(Profile::new()));
|
51
|
+
self.start_profile_buffer_flusher_thread(&profile);
|
52
|
+
|
53
|
+
// Start monitoring thread
|
54
|
+
let stop_requested = Arc::clone(&self.stop_requested);
|
55
|
+
let postponed_job_args: Box<PostponedJobArgs> = Box::new(PostponedJobArgs {
|
56
|
+
ruby_threads: Arc::clone(&self.ruby_threads),
|
57
|
+
profile: Arc::clone(&profile),
|
58
|
+
});
|
59
|
+
let postponed_job_handle: rb_postponed_job_handle_t = unsafe {
|
60
|
+
rb_postponed_job_preregister(
|
61
|
+
0,
|
62
|
+
Some(Self::postponed_job),
|
63
|
+
Box::into_raw(postponed_job_args) as *mut c_void, // FIXME: leak
|
64
|
+
)
|
65
|
+
};
|
66
|
+
thread::spawn(move || Self::thread_main_loop(stop_requested, postponed_job_handle));
|
67
|
+
|
68
|
+
self.profile = Some(profile);
|
69
|
+
|
70
|
+
Qtrue.into()
|
71
|
+
}
|
72
|
+
|
73
|
+
fn thread_main_loop(
|
74
|
+
stop_requested: Arc<AtomicBool>,
|
75
|
+
postponed_job_handle: rb_postponed_job_handle_t,
|
76
|
+
) {
|
77
|
+
loop {
|
78
|
+
if stop_requested.fetch_and(true, Ordering::Relaxed) {
|
79
|
+
break;
|
80
|
+
}
|
81
|
+
unsafe {
|
82
|
+
rb_postponed_job_trigger(postponed_job_handle);
|
83
|
+
}
|
84
|
+
// sleep for 50 ms
|
85
|
+
thread::sleep(Duration::from_millis(50));
|
86
|
+
}
|
87
|
+
}
|
88
|
+
|
89
|
+
fn stop(&self, _rbself: VALUE) -> VALUE {
|
90
|
+
// Stop the collector thread
|
91
|
+
self.stop_requested.store(true, Ordering::Relaxed);
|
92
|
+
|
93
|
+
if let Some(profile) = &self.profile {
|
94
|
+
// Finalize
|
95
|
+
match profile.try_write() {
|
96
|
+
Ok(mut profile) => {
|
97
|
+
profile.flush_temporary_sample_buffer();
|
98
|
+
}
|
99
|
+
Err(_) => {
|
100
|
+
println!("[pf2 ERROR] stop: Failed to acquire profile lock.");
|
101
|
+
return Qfalse.into();
|
102
|
+
}
|
103
|
+
}
|
104
|
+
|
105
|
+
let profile = profile.try_read().unwrap();
|
106
|
+
log::debug!("Number of samples: {}", profile.samples.len());
|
107
|
+
|
108
|
+
let serialized = ProfileSerializer::serialize(&profile);
|
109
|
+
let serialized = CString::new(serialized).unwrap();
|
110
|
+
unsafe { rb_str_new_cstr(serialized.as_ptr()) }
|
111
|
+
} else {
|
112
|
+
panic!("stop() called before start()");
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
unsafe extern "C" fn postponed_job(ptr: *mut c_void) {
|
117
|
+
unsafe {
|
118
|
+
rb_gc_disable();
|
119
|
+
}
|
120
|
+
let args = unsafe { ManuallyDrop::new(Box::from_raw(ptr as *mut PostponedJobArgs)) };
|
121
|
+
|
122
|
+
let mut profile = match args.profile.try_write() {
|
123
|
+
Ok(profile) => profile,
|
124
|
+
Err(_) => {
|
125
|
+
// FIXME: Do we want to properly collect GC samples? I don't know yet.
|
126
|
+
log::trace!("Failed to acquire profile lock (garbage collection possibly in progress). Dropping sample.");
|
127
|
+
return;
|
128
|
+
}
|
129
|
+
};
|
130
|
+
|
131
|
+
// Collect stack information from specified Ruby Threads
|
132
|
+
let ruby_threads = args.ruby_threads.try_read().unwrap();
|
133
|
+
for ruby_thread in ruby_threads.iter() {
|
134
|
+
// Check if the thread is still alive
|
135
|
+
if unsafe { rb_funcall(*ruby_thread, rb_intern(cstr!("status")), 0) } == Qfalse as u64 {
|
136
|
+
continue;
|
137
|
+
}
|
138
|
+
|
139
|
+
let sample = Sample::capture(*ruby_thread);
|
140
|
+
if profile.temporary_sample_buffer.push(sample).is_err() {
|
141
|
+
log::debug!("Temporary sample buffer full. Dropping sample.");
|
142
|
+
}
|
143
|
+
}
|
144
|
+
unsafe {
|
145
|
+
rb_gc_enable();
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
fn start_profile_buffer_flusher_thread(&self, profile: &Arc<RwLock<Profile>>) {
|
150
|
+
let profile = Arc::clone(profile);
|
151
|
+
thread::spawn(move || loop {
|
152
|
+
log::trace!("Flushing temporary sample buffer");
|
153
|
+
match profile.try_write() {
|
154
|
+
Ok(mut profile) => {
|
155
|
+
profile.flush_temporary_sample_buffer();
|
156
|
+
}
|
157
|
+
Err(_) => {
|
158
|
+
log::debug!("flusher: Failed to acquire profile lock");
|
159
|
+
}
|
160
|
+
}
|
161
|
+
thread::sleep(Duration::from_millis(500));
|
162
|
+
});
|
163
|
+
}
|
164
|
+
|
165
|
+
// Ruby Methods
|
166
|
+
|
167
|
+
// SampleCollector.start
|
168
|
+
pub unsafe extern "C" fn rb_start(rbself: VALUE, ruby_threads: VALUE, _: VALUE) -> VALUE {
|
169
|
+
let mut collector = Self::get_struct_from(rbself);
|
170
|
+
collector.start(rbself, ruby_threads)
|
171
|
+
}
|
172
|
+
|
173
|
+
// SampleCollector.stop
|
174
|
+
pub unsafe extern "C" fn rb_stop(rbself: VALUE) -> VALUE {
|
175
|
+
let collector = Self::get_struct_from(rbself);
|
176
|
+
collector.stop(rbself)
|
177
|
+
}
|
178
|
+
|
179
|
+
// Functions for TypedData
|
180
|
+
|
181
|
+
fn get_struct_from(obj: VALUE) -> ManuallyDrop<Box<Self>> {
|
182
|
+
unsafe {
|
183
|
+
let ptr = rb_check_typeddata(obj, &RBDATA);
|
184
|
+
ManuallyDrop::new(Box::from_raw(ptr as *mut TimerThreadScheduler))
|
185
|
+
}
|
186
|
+
}
|
187
|
+
|
188
|
+
#[allow(non_snake_case)]
|
189
|
+
pub unsafe extern "C" fn rb_alloc(_rbself: VALUE) -> VALUE {
|
190
|
+
let collector = TimerThreadScheduler::new();
|
191
|
+
|
192
|
+
unsafe {
|
193
|
+
let rb_mPf2: VALUE = rb_define_module(cstr!("Pf2"));
|
194
|
+
let rb_cTimerThreadScheduler =
|
195
|
+
rb_define_class_under(rb_mPf2, cstr!("TimerThreadScheduler"), rb_cObject);
|
196
|
+
|
197
|
+
rb_data_typed_object_wrap(
|
198
|
+
rb_cTimerThreadScheduler,
|
199
|
+
Box::into_raw(Box::new(collector)) as *mut _ as *mut c_void,
|
200
|
+
&RBDATA,
|
201
|
+
)
|
202
|
+
}
|
203
|
+
}
|
204
|
+
|
205
|
+
unsafe extern "C" fn dmark(ptr: *mut c_void) {
|
206
|
+
unsafe {
|
207
|
+
let collector = ManuallyDrop::new(Box::from_raw(ptr as *mut TimerThreadScheduler));
|
208
|
+
if let Some(profile) = &collector.profile {
|
209
|
+
match profile.read() {
|
210
|
+
Ok(profile) => {
|
211
|
+
profile.dmark();
|
212
|
+
}
|
213
|
+
Err(_) => {
|
214
|
+
panic!("[pf2 FATAL] dmark: Failed to acquire profile lock.");
|
215
|
+
}
|
216
|
+
}
|
217
|
+
}
|
218
|
+
}
|
219
|
+
}
|
220
|
+
unsafe extern "C" fn dfree(ptr: *mut c_void) {
|
221
|
+
unsafe {
|
222
|
+
drop(Box::from_raw(ptr as *mut TimerThreadScheduler));
|
223
|
+
}
|
224
|
+
}
|
225
|
+
unsafe extern "C" fn dsize(_: *const c_void) -> size_t {
|
226
|
+
// FIXME: Report something better
|
227
|
+
std::mem::size_of::<TimerThreadScheduler>() as size_t
|
228
|
+
}
|
229
|
+
}
|
230
|
+
|
231
|
+
static mut RBDATA: rb_data_type_t = rb_data_type_t {
|
232
|
+
wrap_struct_name: cstr!("TimerThreadScheduler"),
|
233
|
+
function: rb_data_type_struct__bindgen_ty_1 {
|
234
|
+
dmark: Some(TimerThreadScheduler::dmark),
|
235
|
+
dfree: Some(TimerThreadScheduler::dfree),
|
236
|
+
dsize: Some(TimerThreadScheduler::dsize),
|
237
|
+
dcompact: None,
|
238
|
+
reserved: [null_mut(); 1],
|
239
|
+
},
|
240
|
+
parent: null_mut(),
|
241
|
+
data: null_mut(),
|
242
|
+
flags: 0,
|
243
|
+
};
|
data/ext/pf2/src/util.rs
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
use core::mem::transmute;
|
2
|
+
use rb_sys::*;
|
3
|
+
use std::ffi::c_void;
|
4
|
+
|
5
|
+
// Convert str literal to C string literal
|
6
|
+
macro_rules! cstr {
|
7
|
+
($s:expr) => {
|
8
|
+
concat!($s, "\0").as_ptr() as *const std::ffi::c_char
|
9
|
+
};
|
10
|
+
}
|
11
|
+
pub(crate) use cstr;
|
12
|
+
|
13
|
+
pub type RubyCFunc = unsafe extern "C" fn() -> VALUE;
|
14
|
+
|
15
|
+
// TODO: rewrite as macro
|
16
|
+
pub fn to_ruby_cfunc1<T>(f: unsafe extern "C" fn(T) -> VALUE) -> RubyCFunc {
|
17
|
+
unsafe { transmute::<unsafe extern "C" fn(T) -> VALUE, RubyCFunc>(f) }
|
18
|
+
}
|
19
|
+
pub fn to_ruby_cfunc3<T, U, V>(f: unsafe extern "C" fn(T, U, V) -> VALUE) -> RubyCFunc {
|
20
|
+
unsafe { transmute::<unsafe extern "C" fn(T, U, V) -> VALUE, RubyCFunc>(f) }
|
21
|
+
}
|
22
|
+
|
23
|
+
#[allow(non_snake_case)]
|
24
|
+
pub fn RTEST(v: VALUE) -> bool {
|
25
|
+
v != Qfalse as VALUE && v != Qnil as VALUE
|
26
|
+
}
|
27
|
+
|
28
|
+
extern "C" {
|
29
|
+
pub fn extract_si_value_sival_ptr(info: *mut libc::siginfo_t) -> *mut c_void;
|
30
|
+
}
|
data/lib/pf2/cli.rb
CHANGED
@@ -27,7 +27,7 @@ module Pf2
|
|
27
27
|
end
|
28
28
|
option_parser.parse!(argv)
|
29
29
|
|
30
|
-
profile =
|
30
|
+
profile = JSON.parse(File.read(ARGV[0]), symbolize_names: true, max_nesting: false)
|
31
31
|
report = JSON.generate(Pf2::Reporter.new(profile).emit)
|
32
32
|
|
33
33
|
if options[:output_file]
|
data/lib/pf2/reporter.rb
CHANGED
@@ -5,7 +5,7 @@ module Pf2
|
|
5
5
|
# https://github.com/firefox-devtools/profiler/blob/main/docs-developer/processed-profile-format.md
|
6
6
|
class Reporter
|
7
7
|
def initialize(profile)
|
8
|
-
@profile = profile
|
8
|
+
@profile = Reporter.deep_intize_keys(profile)
|
9
9
|
end
|
10
10
|
|
11
11
|
def inspect
|
@@ -13,15 +13,15 @@ module Pf2
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def emit
|
16
|
-
|
16
|
+
report = {
|
17
17
|
meta: {
|
18
18
|
interval: 10, # ms; TODO: replace with actual interval
|
19
19
|
start_time: 0,
|
20
20
|
process_type: 0,
|
21
21
|
product: 'ruby',
|
22
22
|
stackwalk: 0,
|
23
|
-
version:
|
24
|
-
preprocessed_profile_version:
|
23
|
+
version: 28,
|
24
|
+
preprocessed_profile_version: 47,
|
25
25
|
symbolicated: true,
|
26
26
|
categories: [
|
27
27
|
{
|
@@ -40,12 +40,13 @@ module Pf2
|
|
40
40
|
subcategories: ["Code"],
|
41
41
|
},
|
42
42
|
],
|
43
|
+
marker_schema: [],
|
43
44
|
},
|
44
45
|
libs: [],
|
45
46
|
counters: [],
|
46
47
|
threads: @profile[:threads].values.map {|th| ThreadReport.new(th).emit }
|
47
48
|
}
|
48
|
-
Reporter.deep_camelize_keys(
|
49
|
+
Reporter.deep_camelize_keys(report)
|
49
50
|
end
|
50
51
|
|
51
52
|
class ThreadReport
|
@@ -83,7 +84,9 @@ module Pf2
|
|
83
84
|
name: "Thread (tid: #{@thread[:thread_id]})",
|
84
85
|
is_main_thread: true,
|
85
86
|
is_js_tracer: true,
|
86
|
-
|
87
|
+
# FIXME: We can fill the correct PID only after we correctly fill is_main_thread
|
88
|
+
# (only one thread could be marked as is_main_thread in a single process)
|
89
|
+
pid: @thread[:thread_id],
|
87
90
|
tid: @thread[:thread_id],
|
88
91
|
samples: samples,
|
89
92
|
markers: markers,
|
@@ -114,7 +117,7 @@ module Pf2
|
|
114
117
|
|
115
118
|
@thread[:samples].each do |sample|
|
116
119
|
ret[:stack] << @stack_tree_id_map[sample[:stack_tree_id]]
|
117
|
-
ret[:time] << sample[:
|
120
|
+
ret[:time] << sample[:elapsed_ns] / 1000000 # ns -> ms
|
118
121
|
ret[:duration] << 1
|
119
122
|
ret[:event_delay] << 0
|
120
123
|
end
|
@@ -134,6 +137,8 @@ module Pf2
|
|
134
137
|
line: [],
|
135
138
|
column: [],
|
136
139
|
optimizations: [],
|
140
|
+
inline_depth: [],
|
141
|
+
native_symbol: [],
|
137
142
|
}
|
138
143
|
|
139
144
|
@thread[:frames].each.with_index do |(id, frame), i|
|
@@ -146,6 +151,8 @@ module Pf2
|
|
146
151
|
ret[:line] << nil
|
147
152
|
ret[:column] << nil
|
148
153
|
ret[:optimizations] << nil
|
154
|
+
ret[:inline_depth] << 0
|
155
|
+
ret[:native_symbol] << nil
|
149
156
|
|
150
157
|
@frame_id_map[id] = i
|
151
158
|
end
|
@@ -229,6 +236,9 @@ module Pf2
|
|
229
236
|
data: [],
|
230
237
|
name: [],
|
231
238
|
time: [],
|
239
|
+
start_time: [],
|
240
|
+
end_time: [],
|
241
|
+
phase: [],
|
232
242
|
category: [],
|
233
243
|
length: 0
|
234
244
|
}
|
@@ -244,17 +254,32 @@ module Pf2
|
|
244
254
|
s.split('_').inject([]) {|buffer, p| buffer.push(buffer.size == 0 ? p : p.capitalize) }.join
|
245
255
|
end
|
246
256
|
|
247
|
-
def
|
257
|
+
def deep_transform_keys(value, &block)
|
248
258
|
case value
|
249
259
|
when Array
|
250
|
-
value.map {|v|
|
260
|
+
value.map {|v| deep_transform_keys(v, &block) }
|
251
261
|
when Hash
|
252
|
-
Hash[value.map {|k, v| [
|
262
|
+
Hash[value.map {|k, v| [yield(k), deep_transform_keys(v, &block)] }]
|
253
263
|
else
|
254
264
|
value
|
255
265
|
end
|
256
266
|
end
|
267
|
+
|
268
|
+
def deep_camelize_keys(value)
|
269
|
+
deep_transform_keys(value) do |key|
|
270
|
+
snake_to_camel(key.to_s).to_sym
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def deep_intize_keys(value)
|
275
|
+
deep_transform_keys(value) do |key|
|
276
|
+
if key.to_s.to_i.to_s == key.to_s
|
277
|
+
key.to_s.to_i
|
278
|
+
else
|
279
|
+
key
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
257
283
|
end
|
258
284
|
end
|
259
285
|
end
|
260
|
-
|
data/lib/pf2/version.rb
CHANGED
data/lib/pf2.rb
CHANGED
@@ -4,13 +4,31 @@ require_relative 'pf2/version'
|
|
4
4
|
module Pf2
|
5
5
|
class Error < StandardError; end
|
6
6
|
|
7
|
-
|
7
|
+
def self.default_scheduler_class
|
8
|
+
# SignalScheduler is Linux-only. Use TimerThreadScheduler on other platforms.
|
9
|
+
if defined?(SignalScheduler)
|
10
|
+
SignalScheduler
|
11
|
+
else
|
12
|
+
TimerThreadScheduler
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.default_scheduler
|
17
|
+
@@default_scheduler ||= default_scheduler_class.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.start(...)
|
21
|
+
default_scheduler.start(...)
|
22
|
+
end
|
8
23
|
|
9
|
-
def self.
|
10
|
-
|
24
|
+
def self.stop(...)
|
25
|
+
default_scheduler.stop(...)
|
11
26
|
end
|
12
27
|
|
13
|
-
def self.
|
14
|
-
|
28
|
+
def self.profile(&block)
|
29
|
+
raise ArgumentError, "block required" unless block_given?
|
30
|
+
start([Thread.current], true)
|
31
|
+
yield
|
32
|
+
stop
|
15
33
|
end
|
16
34
|
end
|