pf2 0.4.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/Cargo.lock +2 -2
- data/README.md +15 -3
- data/ext/pf2/src/lib.rs +4 -0
- data/ext/pf2/src/profile_serializer.rs +77 -33
- data/ext/pf2/src/ruby_init.rs +9 -40
- data/ext/pf2/src/ruby_internal_apis.rs +32 -2
- data/ext/pf2/src/scheduler.rs +10 -0
- data/ext/pf2/src/session/configuration.rs +106 -0
- data/ext/pf2/src/session/new_thread_watcher.rs +80 -0
- data/ext/pf2/src/session/ruby_object.rs +90 -0
- data/ext/pf2/src/session.rs +242 -0
- data/ext/pf2/src/signal_scheduler.rs +105 -221
- data/ext/pf2/src/signal_scheduler_unsupported_platform.rs +39 -0
- data/ext/pf2/src/timer_thread_scheduler.rs +92 -240
- data/lib/pf2/cli.rb +69 -7
- data/lib/pf2/reporter.rb +105 -4
- data/lib/pf2/serve.rb +60 -0
- data/lib/pf2/session.rb +7 -0
- data/lib/pf2/version.rb +1 -1
- data/lib/pf2.rb +7 -14
- metadata +24 -4
- data/ext/pf2/src/signal_scheduler/configuration.rs +0 -31
- data/ext/pf2/src/signal_scheduler/timer_installer.rs +0 -145
@@ -1,124 +1,39 @@
|
|
1
1
|
#![deny(unsafe_op_in_unsafe_fn)]
|
2
2
|
|
3
|
-
use std::ffi::{
|
3
|
+
use std::ffi::{c_void, CString};
|
4
4
|
use std::mem::ManuallyDrop;
|
5
|
-
use std::ptr::null_mut;
|
6
5
|
use std::sync::atomic::{AtomicBool, Ordering};
|
7
6
|
use std::sync::{Arc, RwLock};
|
8
7
|
use std::thread;
|
9
|
-
use std::time::Duration;
|
10
8
|
|
11
9
|
use rb_sys::*;
|
12
10
|
|
13
11
|
use crate::profile::Profile;
|
14
12
|
use crate::profile_serializer::ProfileSerializer;
|
15
13
|
use crate::sample::Sample;
|
14
|
+
use crate::scheduler::Scheduler;
|
15
|
+
use crate::session::configuration::{self, Configuration};
|
16
16
|
use crate::util::*;
|
17
17
|
|
18
18
|
#[derive(Clone, Debug)]
|
19
19
|
pub struct TimerThreadScheduler {
|
20
|
-
|
21
|
-
|
22
|
-
profile: Option<Arc<RwLock<Profile>>>,
|
20
|
+
configuration: Arc<Configuration>,
|
21
|
+
profile: Arc<RwLock<Profile>>,
|
23
22
|
stop_requested: Arc<AtomicBool>,
|
24
23
|
}
|
25
24
|
|
26
25
|
#[derive(Debug)]
|
27
26
|
struct PostponedJobArgs {
|
28
|
-
|
27
|
+
configuration: Arc<Configuration>,
|
29
28
|
profile: Arc<RwLock<Profile>>,
|
30
29
|
}
|
31
30
|
|
32
|
-
impl TimerThreadScheduler {
|
33
|
-
fn
|
34
|
-
|
35
|
-
ruby_threads: Arc::new(RwLock::new(vec![])),
|
36
|
-
interval: None,
|
37
|
-
profile: None,
|
38
|
-
stop_requested: Arc::new(AtomicBool::new(false)),
|
39
|
-
}
|
40
|
-
}
|
41
|
-
|
42
|
-
fn initialize(&mut self, argc: c_int, argv: *const VALUE, _rbself: VALUE) -> VALUE {
|
43
|
-
// Parse arguments
|
44
|
-
let kwargs: VALUE = Qnil.into();
|
45
|
-
unsafe {
|
46
|
-
rb_scan_args(argc, argv, cstr!(":"), &kwargs);
|
47
|
-
};
|
48
|
-
let mut kwargs_values: [VALUE; 3] = [Qnil.into(); 3];
|
49
|
-
unsafe {
|
50
|
-
rb_get_kwargs(
|
51
|
-
kwargs,
|
52
|
-
[
|
53
|
-
rb_intern(cstr!("interval_ms")),
|
54
|
-
rb_intern(cstr!("threads")),
|
55
|
-
rb_intern(cstr!("time_mode")),
|
56
|
-
]
|
57
|
-
.as_mut_ptr(),
|
58
|
-
0,
|
59
|
-
3,
|
60
|
-
kwargs_values.as_mut_ptr(),
|
61
|
-
);
|
62
|
-
};
|
63
|
-
let interval: Duration = if kwargs_values[0] != Qundef as VALUE {
|
64
|
-
let interval_ms = unsafe { rb_num2long(kwargs_values[0]) };
|
65
|
-
Duration::from_millis(interval_ms.try_into().unwrap_or_else(|_| {
|
66
|
-
eprintln!(
|
67
|
-
"[Pf2] Warning: Specified interval ({}) is not valid. Using default value (49ms).",
|
68
|
-
interval_ms
|
69
|
-
);
|
70
|
-
49
|
71
|
-
}))
|
72
|
-
} else {
|
73
|
-
Duration::from_millis(49)
|
74
|
-
};
|
75
|
-
let threads: VALUE = if kwargs_values[1] != Qundef as VALUE {
|
76
|
-
kwargs_values[1]
|
77
|
-
} else {
|
78
|
-
unsafe { rb_funcall(rb_cThread, rb_intern(cstr!("list")), 0) }
|
79
|
-
};
|
80
|
-
if kwargs_values[2] != Qundef as VALUE {
|
81
|
-
let specified_mode = unsafe {
|
82
|
-
let mut str = rb_funcall(kwargs_values[2], rb_intern(cstr!("to_s")), 0);
|
83
|
-
let ptr = rb_string_value_ptr(&mut str);
|
84
|
-
CStr::from_ptr(ptr).to_str().unwrap()
|
85
|
-
};
|
86
|
-
if specified_mode != "wall" {
|
87
|
-
// Raise an ArgumentError
|
88
|
-
unsafe {
|
89
|
-
rb_raise(
|
90
|
-
rb_eArgError,
|
91
|
-
cstr!("TimerThreadScheduler only supports :wall mode."),
|
92
|
-
)
|
93
|
-
}
|
94
|
-
}
|
95
|
-
}
|
96
|
-
|
97
|
-
let mut target_ruby_threads = Vec::new();
|
98
|
-
unsafe {
|
99
|
-
for i in 0..RARRAY_LEN(threads) {
|
100
|
-
let ruby_thread: VALUE = rb_ary_entry(threads, i);
|
101
|
-
target_ruby_threads.push(ruby_thread);
|
102
|
-
}
|
103
|
-
}
|
104
|
-
|
105
|
-
self.interval = Some(Arc::new(interval));
|
106
|
-
self.ruby_threads = Arc::new(RwLock::new(target_ruby_threads.into_iter().collect()));
|
107
|
-
|
108
|
-
Qnil.into()
|
109
|
-
}
|
110
|
-
|
111
|
-
fn start(&mut self, _rbself: VALUE) -> VALUE {
|
112
|
-
// Create Profile
|
113
|
-
let profile = Arc::new(RwLock::new(Profile::new()));
|
114
|
-
self.start_profile_buffer_flusher_thread(&profile);
|
115
|
-
|
116
|
-
// Start monitoring thread
|
117
|
-
let stop_requested = Arc::clone(&self.stop_requested);
|
118
|
-
let interval = Arc::clone(self.interval.as_ref().unwrap());
|
31
|
+
impl Scheduler for TimerThreadScheduler {
|
32
|
+
fn start(&self) -> VALUE {
|
33
|
+
// Register the Postponed Job which does the actual work of collecting samples
|
119
34
|
let postponed_job_args: Box<PostponedJobArgs> = Box::new(PostponedJobArgs {
|
120
|
-
|
121
|
-
profile: Arc::clone(&profile),
|
35
|
+
configuration: Arc::clone(&self.configuration),
|
36
|
+
profile: Arc::clone(&self.profile),
|
122
37
|
});
|
123
38
|
let postponed_job_handle: rb_postponed_job_handle_t = unsafe {
|
124
39
|
rb_postponed_job_preregister(
|
@@ -127,18 +42,79 @@ impl TimerThreadScheduler {
|
|
127
42
|
Box::into_raw(postponed_job_args) as *mut c_void, // FIXME: leak
|
128
43
|
)
|
129
44
|
};
|
45
|
+
|
46
|
+
// Start a timer thread that periodically triggers postponed jobs based on configuration
|
47
|
+
let configuration = Arc::clone(&self.configuration);
|
48
|
+
let stop_requested = Arc::clone(&self.stop_requested);
|
130
49
|
thread::spawn(move || {
|
131
|
-
Self::thread_main_loop(
|
50
|
+
Self::thread_main_loop(configuration, stop_requested, postponed_job_handle)
|
132
51
|
});
|
133
52
|
|
134
|
-
self.profile = Some(profile);
|
135
|
-
|
136
53
|
Qtrue.into()
|
137
54
|
}
|
138
55
|
|
56
|
+
fn stop(&self) -> VALUE {
|
57
|
+
// Stop the collector thread
|
58
|
+
self.stop_requested.store(true, Ordering::Relaxed);
|
59
|
+
|
60
|
+
// Finalize
|
61
|
+
match self.profile.try_write() {
|
62
|
+
Ok(mut profile) => {
|
63
|
+
profile.flush_temporary_sample_buffer();
|
64
|
+
}
|
65
|
+
Err(_) => {
|
66
|
+
println!("[pf2 ERROR] stop: Failed to acquire profile lock.");
|
67
|
+
return Qfalse.into();
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
let profile = self.profile.try_read().unwrap();
|
72
|
+
log::debug!("Number of samples: {}", profile.samples.len());
|
73
|
+
|
74
|
+
let serialized = ProfileSerializer::serialize(&profile);
|
75
|
+
let serialized = CString::new(serialized).unwrap();
|
76
|
+
unsafe { rb_str_new_cstr(serialized.as_ptr()) }
|
77
|
+
}
|
78
|
+
|
79
|
+
fn on_new_thread(&self, _thread: VALUE) {
|
80
|
+
todo!();
|
81
|
+
}
|
82
|
+
|
83
|
+
fn dmark(&self) {
|
84
|
+
match self.profile.read() {
|
85
|
+
Ok(profile) => unsafe {
|
86
|
+
profile.dmark();
|
87
|
+
},
|
88
|
+
Err(_) => {
|
89
|
+
panic!("[pf2 FATAL] dmark: Failed to acquire profile lock.");
|
90
|
+
}
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
fn dfree(&self) {
|
95
|
+
// No-op
|
96
|
+
}
|
97
|
+
|
98
|
+
fn dsize(&self) -> size_t {
|
99
|
+
// FIXME: Report something better
|
100
|
+
std::mem::size_of::<TimerThreadScheduler>() as size_t
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
impl TimerThreadScheduler {
|
105
|
+
pub fn new(configuration: &Configuration, profile: Arc<RwLock<Profile>>) -> Self {
|
106
|
+
Self {
|
107
|
+
configuration: Arc::new(configuration.clone()),
|
108
|
+
profile,
|
109
|
+
stop_requested: Arc::new(AtomicBool::new(false)),
|
110
|
+
}
|
111
|
+
|
112
|
+
// cstr!("TimerThreadScheduler only supports :wall mode."),
|
113
|
+
}
|
114
|
+
|
139
115
|
fn thread_main_loop(
|
116
|
+
configuration: Arc<Configuration>,
|
140
117
|
stop_requested: Arc<AtomicBool>,
|
141
|
-
interval: Arc<Duration>,
|
142
118
|
postponed_job_handle: rb_postponed_job_handle_t,
|
143
119
|
) {
|
144
120
|
loop {
|
@@ -146,37 +122,11 @@ impl TimerThreadScheduler {
|
|
146
122
|
break;
|
147
123
|
}
|
148
124
|
unsafe {
|
125
|
+
log::trace!("Triggering postponed job");
|
149
126
|
rb_postponed_job_trigger(postponed_job_handle);
|
150
127
|
}
|
151
128
|
|
152
|
-
thread::sleep(
|
153
|
-
}
|
154
|
-
}
|
155
|
-
|
156
|
-
fn stop(&self, _rbself: VALUE) -> VALUE {
|
157
|
-
// Stop the collector thread
|
158
|
-
self.stop_requested.store(true, Ordering::Relaxed);
|
159
|
-
|
160
|
-
if let Some(profile) = &self.profile {
|
161
|
-
// Finalize
|
162
|
-
match profile.try_write() {
|
163
|
-
Ok(mut profile) => {
|
164
|
-
profile.flush_temporary_sample_buffer();
|
165
|
-
}
|
166
|
-
Err(_) => {
|
167
|
-
println!("[pf2 ERROR] stop: Failed to acquire profile lock.");
|
168
|
-
return Qfalse.into();
|
169
|
-
}
|
170
|
-
}
|
171
|
-
|
172
|
-
let profile = profile.try_read().unwrap();
|
173
|
-
log::debug!("Number of samples: {}", profile.samples.len());
|
174
|
-
|
175
|
-
let serialized = ProfileSerializer::serialize(&profile);
|
176
|
-
let serialized = CString::new(serialized).unwrap();
|
177
|
-
unsafe { rb_str_new_cstr(serialized.as_ptr()) }
|
178
|
-
} else {
|
179
|
-
panic!("stop() called before start()");
|
129
|
+
thread::sleep(configuration.interval);
|
180
130
|
}
|
181
131
|
}
|
182
132
|
|
@@ -196,124 +146,26 @@ impl TimerThreadScheduler {
|
|
196
146
|
};
|
197
147
|
|
198
148
|
// Collect stack information from specified Ruby Threads
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
log::debug!("Temporary sample buffer full. Dropping sample.");
|
209
|
-
}
|
210
|
-
}
|
211
|
-
unsafe {
|
212
|
-
rb_gc_enable();
|
213
|
-
}
|
214
|
-
}
|
215
|
-
|
216
|
-
fn start_profile_buffer_flusher_thread(&self, profile: &Arc<RwLock<Profile>>) {
|
217
|
-
let profile = Arc::clone(profile);
|
218
|
-
thread::spawn(move || loop {
|
219
|
-
log::trace!("Flushing temporary sample buffer");
|
220
|
-
match profile.try_write() {
|
221
|
-
Ok(mut profile) => {
|
222
|
-
profile.flush_temporary_sample_buffer();
|
223
|
-
}
|
224
|
-
Err(_) => {
|
225
|
-
log::debug!("flusher: Failed to acquire profile lock");
|
226
|
-
}
|
227
|
-
}
|
228
|
-
thread::sleep(Duration::from_millis(500));
|
229
|
-
});
|
230
|
-
}
|
231
|
-
|
232
|
-
// Ruby Methods
|
233
|
-
|
234
|
-
pub unsafe extern "C" fn rb_initialize(
|
235
|
-
argc: c_int,
|
236
|
-
argv: *const VALUE,
|
237
|
-
rbself: VALUE,
|
238
|
-
) -> VALUE {
|
239
|
-
let mut collector = Self::get_struct_from(rbself);
|
240
|
-
collector.initialize(argc, argv, rbself)
|
241
|
-
}
|
242
|
-
|
243
|
-
// SampleCollector.start
|
244
|
-
pub unsafe extern "C" fn rb_start(rbself: VALUE) -> VALUE {
|
245
|
-
let mut collector = Self::get_struct_from(rbself);
|
246
|
-
collector.start(rbself)
|
247
|
-
}
|
248
|
-
|
249
|
-
// SampleCollector.stop
|
250
|
-
pub unsafe extern "C" fn rb_stop(rbself: VALUE) -> VALUE {
|
251
|
-
let collector = Self::get_struct_from(rbself);
|
252
|
-
collector.stop(rbself)
|
253
|
-
}
|
254
|
-
|
255
|
-
// Functions for TypedData
|
256
|
-
|
257
|
-
fn get_struct_from(obj: VALUE) -> ManuallyDrop<Box<Self>> {
|
258
|
-
unsafe {
|
259
|
-
let ptr = rb_check_typeddata(obj, &RBDATA);
|
260
|
-
ManuallyDrop::new(Box::from_raw(ptr as *mut TimerThreadScheduler))
|
261
|
-
}
|
262
|
-
}
|
263
|
-
|
264
|
-
#[allow(non_snake_case)]
|
265
|
-
pub unsafe extern "C" fn rb_alloc(_rbself: VALUE) -> VALUE {
|
266
|
-
let collector = TimerThreadScheduler::new();
|
267
|
-
|
268
|
-
unsafe {
|
269
|
-
let rb_mPf2: VALUE = rb_define_module(cstr!("Pf2"));
|
270
|
-
let rb_cTimerThreadScheduler =
|
271
|
-
rb_define_class_under(rb_mPf2, cstr!("TimerThreadScheduler"), rb_cObject);
|
272
|
-
|
273
|
-
rb_data_typed_object_wrap(
|
274
|
-
rb_cTimerThreadScheduler,
|
275
|
-
Box::into_raw(Box::new(collector)) as *mut _ as *mut c_void,
|
276
|
-
&RBDATA,
|
277
|
-
)
|
278
|
-
}
|
279
|
-
}
|
280
|
-
|
281
|
-
unsafe extern "C" fn dmark(ptr: *mut c_void) {
|
282
|
-
unsafe {
|
283
|
-
let collector = ManuallyDrop::new(Box::from_raw(ptr as *mut TimerThreadScheduler));
|
284
|
-
if let Some(profile) = &collector.profile {
|
285
|
-
match profile.read() {
|
286
|
-
Ok(profile) => {
|
287
|
-
profile.dmark();
|
149
|
+
match &args.configuration.target_ruby_threads {
|
150
|
+
configuration::Threads::All => todo!(),
|
151
|
+
configuration::Threads::Targeted(threads) => {
|
152
|
+
for ruby_thread in threads.iter() {
|
153
|
+
// Check if the thread is still alive
|
154
|
+
if unsafe { rb_funcall(*ruby_thread, rb_intern(cstr!("status")), 0) }
|
155
|
+
== Qfalse as u64
|
156
|
+
{
|
157
|
+
continue;
|
288
158
|
}
|
289
|
-
|
290
|
-
|
159
|
+
|
160
|
+
let sample = Sample::capture(*ruby_thread, &profile.backtrace_state);
|
161
|
+
if profile.temporary_sample_buffer.push(sample).is_err() {
|
162
|
+
log::debug!("Temporary sample buffer full. Dropping sample.");
|
291
163
|
}
|
292
164
|
}
|
293
165
|
}
|
294
166
|
}
|
295
|
-
}
|
296
|
-
unsafe extern "C" fn dfree(ptr: *mut c_void) {
|
297
167
|
unsafe {
|
298
|
-
|
168
|
+
rb_gc_enable();
|
299
169
|
}
|
300
170
|
}
|
301
|
-
unsafe extern "C" fn dsize(_: *const c_void) -> size_t {
|
302
|
-
// FIXME: Report something better
|
303
|
-
std::mem::size_of::<TimerThreadScheduler>() as size_t
|
304
|
-
}
|
305
171
|
}
|
306
|
-
|
307
|
-
static mut RBDATA: rb_data_type_t = rb_data_type_t {
|
308
|
-
wrap_struct_name: cstr!("TimerThreadScheduler"),
|
309
|
-
function: rb_data_type_struct__bindgen_ty_1 {
|
310
|
-
dmark: Some(TimerThreadScheduler::dmark),
|
311
|
-
dfree: Some(TimerThreadScheduler::dfree),
|
312
|
-
dsize: Some(TimerThreadScheduler::dsize),
|
313
|
-
dcompact: None,
|
314
|
-
reserved: [null_mut(); 1],
|
315
|
-
},
|
316
|
-
parent: null_mut(),
|
317
|
-
data: null_mut(),
|
318
|
-
flags: 0,
|
319
|
-
};
|
data/lib/pf2/cli.rb
CHANGED
@@ -10,24 +10,51 @@ module Pf2
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def run(argv)
|
13
|
+
argv = argv.dup
|
14
|
+
program_name = File.basename($PROGRAM_NAME)
|
15
|
+
|
16
|
+
subcommand = argv.shift
|
17
|
+
case subcommand
|
18
|
+
when 'report'
|
19
|
+
subcommand_report(argv)
|
20
|
+
when 'serve'
|
21
|
+
subcommand_serve(argv)
|
22
|
+
when 'version'
|
23
|
+
puts VERSION
|
24
|
+
return 0
|
25
|
+
when '--help'
|
26
|
+
STDERR.puts <<~__EOS__
|
27
|
+
Usage: #{program_name} COMMAND [options]
|
28
|
+
|
29
|
+
Commands:
|
30
|
+
report Generate a report from a profile
|
31
|
+
serve Start an HTTP server alongside a target process
|
32
|
+
version Show version information
|
33
|
+
__EOS__
|
34
|
+
|
35
|
+
return 1
|
36
|
+
else
|
37
|
+
STDERR.puts "#{program_name}: Unknown subcommand '#{subcommand}'."
|
38
|
+
STDERR.puts "See '#{program_name} --help'"
|
39
|
+
return 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def subcommand_report(argv)
|
13
44
|
options = {}
|
14
45
|
option_parser = OptionParser.new do |opts|
|
15
|
-
opts.
|
16
|
-
puts Pf2::VERSION
|
17
|
-
exit
|
18
|
-
end
|
19
|
-
|
46
|
+
opts.banner = "Usage: pf2 report [options] COMMAND"
|
20
47
|
opts.on('-h', '--help', 'Prints this help') do
|
21
48
|
puts opts
|
49
|
+
return 0
|
22
50
|
end
|
23
|
-
|
24
51
|
opts.on('-o', '--output FILE', 'Output file') do |path|
|
25
52
|
options[:output_file] = path
|
26
53
|
end
|
27
54
|
end
|
28
55
|
option_parser.parse!(argv)
|
29
56
|
|
30
|
-
profile = JSON.parse(File.read(
|
57
|
+
profile = JSON.parse(File.read(argv[0]), symbolize_names: true, max_nesting: false)
|
31
58
|
report = JSON.generate(Pf2::Reporter.new(profile).emit)
|
32
59
|
|
33
60
|
if options[:output_file]
|
@@ -38,5 +65,40 @@ module Pf2
|
|
38
65
|
|
39
66
|
return 0
|
40
67
|
end
|
68
|
+
|
69
|
+
def subcommand_serve(argv)
|
70
|
+
options = {}
|
71
|
+
option_parser = OptionParser.new do |opts|
|
72
|
+
opts.banner = "Usage: pf2 serve [options] COMMAND"
|
73
|
+
opts.on('-h', '--help', 'Prints this help') do
|
74
|
+
puts opts
|
75
|
+
return 0
|
76
|
+
end
|
77
|
+
opts.on('-b', '--bind ADDRESS', 'Address to bind') do |host|
|
78
|
+
options[:serve_host] = host
|
79
|
+
end
|
80
|
+
opts.on('-p', '--port PORT', '') do |port|
|
81
|
+
options[:serve_port] = port
|
82
|
+
end
|
83
|
+
end
|
84
|
+
option_parser.parse!(argv)
|
85
|
+
|
86
|
+
if argv.size == 0
|
87
|
+
# No subcommand was specified
|
88
|
+
STDERR.puts option_parser.help
|
89
|
+
return 1
|
90
|
+
end
|
91
|
+
|
92
|
+
# Inject the profiler (pf2/serve) into the target process via RUBYOPT (-r).
|
93
|
+
# This will have no effect if the target process is not Ruby.
|
94
|
+
env = {
|
95
|
+
'RUBYOPT' => '-rpf2/serve'
|
96
|
+
}
|
97
|
+
env['PF2_SERVE_HOST'] = options[:serve_host] if options[:serve_host]
|
98
|
+
env['PF2_SERVE_PORT'] = options[:serve_port] if options[:serve_port]
|
99
|
+
exec(env, *argv) # never returns if succesful
|
100
|
+
|
101
|
+
return 1
|
102
|
+
end
|
41
103
|
end
|
42
104
|
end
|
data/lib/pf2/reporter.rb
CHANGED
@@ -71,6 +71,8 @@ module Pf2
|
|
71
71
|
end
|
72
72
|
|
73
73
|
def emit
|
74
|
+
x = weave_native_stack(@thread[:stack_tree])
|
75
|
+
@thread[:stack_tree] = x
|
74
76
|
func_table = build_func_table
|
75
77
|
frame_table = build_frame_table
|
76
78
|
stack_table = build_stack_table(func_table, frame_table)
|
@@ -147,13 +149,13 @@ module Pf2
|
|
147
149
|
}
|
148
150
|
|
149
151
|
@thread[:frames].each.with_index do |(id, frame), i|
|
150
|
-
ret[:address] <<
|
152
|
+
ret[:address] << frame[:address].to_s
|
151
153
|
ret[:category] << 1
|
152
154
|
ret[:subcategory] << 1
|
153
155
|
ret[:func] << i # TODO
|
154
156
|
ret[:inner_window_id] << nil
|
155
157
|
ret[:implementation] << nil
|
156
|
-
ret[:line] <<
|
158
|
+
ret[:line] << frame[:callsite_lineno]
|
157
159
|
ret[:column] << nil
|
158
160
|
ret[:optimizations] << nil
|
159
161
|
ret[:inline_depth] << 0
|
@@ -184,8 +186,8 @@ module Pf2
|
|
184
186
|
ret[:is_js] << !native
|
185
187
|
ret[:relevant_for_js] << false
|
186
188
|
ret[:resource] << -1
|
187
|
-
ret[:file_name] <<
|
188
|
-
ret[:line_number] <<
|
189
|
+
ret[:file_name] << string_id(frame[:file_name])
|
190
|
+
ret[:line_number] << frame[:function_first_lineno]
|
189
191
|
ret[:column_number] << nil
|
190
192
|
|
191
193
|
@func_id_map[id] = i
|
@@ -195,6 +197,105 @@ module Pf2
|
|
195
197
|
ret
|
196
198
|
end
|
197
199
|
|
200
|
+
# "Weave" the native stack into the Ruby stack.
|
201
|
+
#
|
202
|
+
# Strategy:
|
203
|
+
# - Split the stack into Ruby and Native parts
|
204
|
+
# - Start from the root of the Native stack
|
205
|
+
# - Dig in to the native stack until we hit a rb_vm_exec(), which marks a call into Ruby code
|
206
|
+
# - Switch to Ruby stack. Keep digging until we hit a Cfunc call, then switch back to Native stack
|
207
|
+
# - Repeat until we consume the entire stack
|
208
|
+
def weave_native_stack(stack_tree)
|
209
|
+
collected_paths = []
|
210
|
+
tree_to_array_of_paths(stack_tree, @thread[:frames], [], collected_paths)
|
211
|
+
collected_paths = collected_paths.map do |path|
|
212
|
+
next if path.size == 0
|
213
|
+
|
214
|
+
new_path = []
|
215
|
+
new_path << path.shift # root
|
216
|
+
|
217
|
+
# Split the stack into Ruby and Native parts
|
218
|
+
native_path, ruby_path = path.partition do |frame|
|
219
|
+
frame_id = frame[:frame_id]
|
220
|
+
@thread[:frames][frame_id][:entry_type] == 'Native'
|
221
|
+
end
|
222
|
+
|
223
|
+
mode = :native
|
224
|
+
|
225
|
+
loop do
|
226
|
+
break if ruby_path.size == 0 && native_path.size == 0
|
227
|
+
|
228
|
+
case mode
|
229
|
+
when :ruby
|
230
|
+
if ruby_path.size == 0
|
231
|
+
mode = :native
|
232
|
+
next
|
233
|
+
end
|
234
|
+
|
235
|
+
next_node = ruby_path[0]
|
236
|
+
new_path << ruby_path.shift
|
237
|
+
next_node_frame = @thread[:frames][next_node[:frame_id]]
|
238
|
+
if native_path.size > 0
|
239
|
+
# Search the remainder of the native stack for the same address
|
240
|
+
# Note: This isn't a very efficient way for the job... but it still works
|
241
|
+
ruby_addr = next_node_frame[:address]
|
242
|
+
native_path[0..].each do |native_node|
|
243
|
+
native_addr = @thread[:frames][native_node[:frame_id]][:address]
|
244
|
+
if ruby_addr && native_addr && ruby_addr == native_addr
|
245
|
+
# A match has been found. Switch to native mode
|
246
|
+
mode = :native
|
247
|
+
break
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
when :native
|
252
|
+
if native_path.size == 0
|
253
|
+
mode = :ruby
|
254
|
+
next
|
255
|
+
end
|
256
|
+
|
257
|
+
# Dig until we meet a rb_vm_exec
|
258
|
+
next_node = native_path[0]
|
259
|
+
new_path << native_path.shift
|
260
|
+
if @thread[:frames][next_node[:frame_id]][:full_label] =~ /vm_exec_core/ # VM_EXEC in vm_exec.h
|
261
|
+
mode = :ruby
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
new_path
|
267
|
+
end
|
268
|
+
|
269
|
+
# reconstruct stack_tree
|
270
|
+
new_stack_tree = array_of_paths_to_tree(collected_paths)
|
271
|
+
new_stack_tree
|
272
|
+
end
|
273
|
+
|
274
|
+
def tree_to_array_of_paths(stack_tree, frames, path, collected_paths)
|
275
|
+
new_path = path + [{ frame_id: stack_tree[:frame_id], node_id: stack_tree[:node_id] }]
|
276
|
+
if stack_tree[:children].empty?
|
277
|
+
collected_paths << new_path
|
278
|
+
else
|
279
|
+
stack_tree[:children].each do |frame_id, child|
|
280
|
+
tree_to_array_of_paths(child, frames, new_path, collected_paths)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def array_of_paths_to_tree(paths)
|
286
|
+
new_stack_tree = { children: {}, node_id: 0, frame_id: 0 }
|
287
|
+
paths.each do |path|
|
288
|
+
current = new_stack_tree
|
289
|
+
path[1..].each do |frame|
|
290
|
+
frame_id = frame[:frame_id]
|
291
|
+
node_id = frame[:node_id]
|
292
|
+
current[:children][frame_id] ||= { children: {}, node_id: node_id, frame_id: frame_id }
|
293
|
+
current = current[:children][frame_id]
|
294
|
+
end
|
295
|
+
end
|
296
|
+
new_stack_tree
|
297
|
+
end
|
298
|
+
|
198
299
|
def build_stack_table(func_table, frame_table)
|
199
300
|
ret = {
|
200
301
|
frame: [],
|
data/lib/pf2/serve.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'logger'
|
3
|
+
require 'uri'
|
4
|
+
require 'webrick'
|
5
|
+
|
6
|
+
require_relative '../pf2'
|
7
|
+
require_relative './reporter'
|
8
|
+
|
9
|
+
module Pf2
|
10
|
+
class Serve
|
11
|
+
CONFIG = {
|
12
|
+
Host: ENV.fetch('PF2_SERVE_HOST', 'localhost'),
|
13
|
+
Port: ENV.fetch('PF2_SERVE_PORT', '51502').to_i, # 1502 = 0xF2 (as in "Pf2")
|
14
|
+
Logger: Logger.new(nil),
|
15
|
+
AccessLog: [],
|
16
|
+
}
|
17
|
+
|
18
|
+
def self.start
|
19
|
+
|
20
|
+
# Ignore Bundler as in `bundle exec`.
|
21
|
+
if File.basename($PROGRAM_NAME) == 'bundle' && ARGV.first == 'exec'
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
25
|
+
server = WEBrick::HTTPServer.new(CONFIG)
|
26
|
+
server.mount_proc('/profile') do |req, res|
|
27
|
+
profile = Pf2.stop
|
28
|
+
profile = JSON.parse(profile, symbolize_names: true, max_nesting: false)
|
29
|
+
res.header['Content-Type'] = 'application/json'
|
30
|
+
res.header['Access-Control-Allow-Origin'] = '*'
|
31
|
+
res.body = JSON.generate(Pf2::Reporter.new((profile)).emit)
|
32
|
+
Pf2.start
|
33
|
+
end
|
34
|
+
|
35
|
+
Pf2.start
|
36
|
+
|
37
|
+
Thread.new do
|
38
|
+
hostport = "#{server.config[:Host]}:#{server.config[:Port]}"
|
39
|
+
# Print host:port to trigger VS Code's auto port-forwarding feature
|
40
|
+
STDERR.puts "[Pf2] Listening on #{hostport}."
|
41
|
+
STDERR.puts "[Pf2] Open https://profiler.firefox.com/from-url/#{URI.encode_www_form_component("http://#{hostport}/profile")} for visualization."
|
42
|
+
STDERR.puts ""
|
43
|
+
server.start
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.at_exit
|
48
|
+
STDERR.puts ""
|
49
|
+
STDERR.puts "[Pf2] Script execution complete (Pf2 server is still listening). Hit Ctrl-C to quit."
|
50
|
+
|
51
|
+
# Allow the user to download the profile after the target program exits
|
52
|
+
sleep
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
Pf2::Serve.start
|
58
|
+
at_exit do
|
59
|
+
Pf2::Serve.at_exit
|
60
|
+
end
|