pf2 0.3.0 → 0.5.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.
@@ -1,124 +1,39 @@
1
1
  #![deny(unsafe_op_in_unsafe_fn)]
2
2
 
3
- use std::ffi::{c_int, c_void, CStr, CString};
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
- ruby_threads: Arc<RwLock<Vec<VALUE>>>,
21
- interval: Option<Arc<Duration>>,
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
- ruby_threads: Arc<RwLock<Vec<VALUE>>>,
27
+ configuration: Arc<Configuration>,
29
28
  profile: Arc<RwLock<Profile>>,
30
29
  }
31
30
 
32
- impl TimerThreadScheduler {
33
- fn new() -> Self {
34
- TimerThreadScheduler {
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
- ruby_threads: Arc::clone(&self.ruby_threads),
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(stop_requested, interval, postponed_job_handle)
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(*interval);
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
- let ruby_threads = args.ruby_threads.try_read().unwrap();
200
- for ruby_thread in ruby_threads.iter() {
201
- // Check if the thread is still alive
202
- if unsafe { rb_funcall(*ruby_thread, rb_intern(cstr!("status")), 0) } == Qfalse as u64 {
203
- continue;
204
- }
205
-
206
- let sample = Sample::capture(*ruby_thread, &profile.backtrace_state);
207
- if profile.temporary_sample_buffer.push(sample).is_err() {
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
- Err(_) => {
290
- panic!("[pf2 FATAL] dmark: Failed to acquire profile lock.");
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
- drop(Box::from_raw(ptr as *mut TimerThreadScheduler));
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.on('-v', '--version', 'Prints version') do
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(ARGV[0]), symbolize_names: true, max_nesting: false)
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] << nil
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] << nil
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] << nil
188
- ret[:line_number] << nil
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