pf2 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|