pf2 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66bf1eb8d6457621a04819a8d100bdb109bf1fd7b4820eb1423a85dcfb91799c
4
- data.tar.gz: b1046c04ae8a2a0630d3bd2ed86c4dc645a9ca42c8038bfaaee0be4c71dbec78
3
+ metadata.gz: 1aa1dd1d3caa1dd9327382e7de0a44b2c0ff4555a9dff8fdc7244f61c3a13405
4
+ data.tar.gz: c50895ea4d8285cc9bb223d575cbec7042aacd119a1390b836b818232ef7e214
5
5
  SHA512:
6
- metadata.gz: 5e8854a44c477c42dc9baa0a5b2cd95e19cd09a3c41fad0e950e7de6bd88ff90bf6a3d8e7b61c43c966976211047d5ecb6da11fbe5cba6460ab456186856e2b8
7
- data.tar.gz: 8d577d2502f38dab61d3ab130f2555b314667c414ec13b2829f2e7fdf0ddf1f114310d560806685145e5ce65f7f24cc50f834f9fc6eb2b1e0d7763b8ebe69d5f
6
+ metadata.gz: 1b5db496a58016e11203334b71e80e7a4c0d7d0dce6b2cb288884da4aecd8820d8915fd0b9f4ebd546802c314d621f69da23c474d254bff17ec3931cb8f7604b
7
+ data.tar.gz: 5e3a8fcf25be2dbf28cf8c36e03839cbeed13aa8d944e43991672a39f06fe16f6eae07aab5cd282c3237171bfff94b39a8abb176153a091452c3177266eb1364
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2024-07-15
4
+
5
+ ### Changed
6
+
7
+ - The default sampling interval is now 9 ms (was originally 49 ms).
8
+ - It is intentional that the default is not 10 (or 50) ms - this is to avoid lockstep sampling.
9
+ - BREAKING: `Pf2::Reporter` has been moved to `Pf2::Reporter::FirefoxProfiler`.
10
+ - This is to make space for other planned reporters.
11
+
12
+
3
13
  ## [0.5.2] - 2024-07-13
4
14
 
5
15
  ### Fixed
data/README.md CHANGED
@@ -67,7 +67,7 @@ Pf2 accepts the following configuration keys:
67
67
 
68
68
  ```rb
69
69
  Pf2.start(
70
- interval_ms: 49, # Integer: The sampling interval in milliseconds (default: 49)
70
+ interval_ms: 9, # Integer: The sampling interval in milliseconds (default: 9)
71
71
  time_mode: :cpu, # `:cpu` or `:wall`: The sampling timer's mode
72
72
  # (default: `:cpu` for SignalScheduler, `:wall` for TimerThreadScheduler)
73
73
  threads: [th1, th2], # `Array<Thread>` | `:all`: A list of Ruby Threads to be tracked.
data/ext/pf2/src/lib.rs CHANGED
@@ -10,6 +10,7 @@ mod profile_serializer;
10
10
  mod ringbuffer;
11
11
  mod sample;
12
12
  mod scheduler;
13
+ mod serialization;
13
14
  mod session;
14
15
  #[cfg(target_os = "linux")]
15
16
  mod signal_scheduler;
@@ -1,4 +1,4 @@
1
- use std::time::Instant;
1
+ use std::time::{Instant, SystemTime};
2
2
  use std::{collections::HashSet, ptr::null_mut};
3
3
 
4
4
  use rb_sys::*;
@@ -15,7 +15,9 @@ const DEFAULT_RINGBUFFER_CAPACITY: usize = 320;
15
15
 
16
16
  #[derive(Debug)]
17
17
  pub struct Profile {
18
- pub start_timestamp: Instant,
18
+ pub start_timestamp: SystemTime,
19
+ pub start_instant: Instant,
20
+ pub end_instant: Option<Instant>,
19
21
  pub samples: Vec<Sample>,
20
22
  pub temporary_sample_buffer: Ringbuffer,
21
23
  pub backtrace_state: BacktraceState,
@@ -35,7 +37,9 @@ impl Profile {
35
37
  };
36
38
 
37
39
  Self {
38
- start_timestamp: Instant::now(),
40
+ start_timestamp: SystemTime::now(),
41
+ start_instant: Instant::now(),
42
+ end_instant: None,
39
43
  samples: vec![],
40
44
  temporary_sample_buffer: Ringbuffer::new(DEFAULT_RINGBUFFER_CAPACITY),
41
45
  backtrace_state,
@@ -221,7 +221,7 @@ impl ProfileSerializer {
221
221
 
222
222
  if merged_stack.is_empty() {
223
223
  // This is the leaf node, record a Sample
224
- let elapsed_ns = (sample.timestamp - profile.start_timestamp).as_nanos();
224
+ let elapsed_ns = (sample.timestamp - profile.start_instant).as_nanos();
225
225
  thread_serializer.samples.push(ProfileSample {
226
226
  elapsed_ns,
227
227
  stack_tree_id: stack_tree.node_id,
@@ -0,0 +1,46 @@
1
+ #[derive(Clone, Deserialize, Serialize)]
2
+ pub struct Profile {
3
+ pub samples: Vec<Sample>,
4
+ pub locations: Vec<Location>,
5
+ pub functions: Vec<Function>,
6
+ pub start_timestamp_ns: u128,
7
+ pub duration_ns: u128,
8
+ }
9
+
10
+ pub type LocationIndex = usize;
11
+ pub type FunctionIndex = usize;
12
+
13
+ /// Sample
14
+ #[derive(Clone, Serialize, Deserialize)]
15
+ pub struct Sample {
16
+ /// The stack leading to this sample.
17
+ /// The leaf node will be stored at `stack[0]`.
18
+ pub stack: Vec<LocationIndex>,
19
+ pub ruby_thread_id: Option<u64>,
20
+ }
21
+
22
+ /// Location represents a location (line) in the source code when a sample was captured.
23
+ #[derive(Clone, PartialEq, Serialize, Deserialize)]
24
+ pub struct Location {
25
+ pub function_index: FunctionIndex,
26
+ pub lineno: i32,
27
+ pub address: Option<usize>,
28
+ }
29
+
30
+ /// Function represents a Ruby method or a C function in the profile.
31
+ #[derive(Clone, PartialEq, Serialize, Deserialize)]
32
+ pub struct Function {
33
+ pub implementation: FunctionImplementation,
34
+ pub name: Option<String>, // unique key
35
+ pub filename: Option<String>,
36
+ /// The first line number in the method/function definition.
37
+ /// For the actual location (line) which was hit during sample capture, refer to `Location.lineno`.
38
+ pub start_lineno: Option<i32>,
39
+ pub start_address: Option<usize>,
40
+ }
41
+
42
+ #[derive(Clone, PartialEq, Serialize, Deserialize)]
43
+ pub enum FunctionImplementation {
44
+ Ruby,
45
+ C,
46
+ }
@@ -0,0 +1,146 @@
1
+ use std::ffi::CStr;
2
+
3
+ use rb_sys::*;
4
+
5
+ use super::profile::{Function, FunctionImplementation, Location, LocationIndex, Profile, Sample};
6
+ use crate::util::RTEST;
7
+
8
+ pub struct ProfileSerializer2 {
9
+ profile: Profile,
10
+ }
11
+
12
+ impl ProfileSerializer2 {
13
+ pub fn new() -> ProfileSerializer2 {
14
+ ProfileSerializer2 {
15
+ profile: Profile {
16
+ start_timestamp_ns: 0,
17
+ duration_ns: 0,
18
+ samples: vec![],
19
+ locations: vec![],
20
+ functions: vec![],
21
+ },
22
+ }
23
+ }
24
+
25
+ pub fn serialize(&mut self, source: &crate::profile::Profile) -> String {
26
+ // Fill in meta fields
27
+ self.profile.start_timestamp_ns = source
28
+ .start_timestamp
29
+ .duration_since(std::time::UNIX_EPOCH)
30
+ .unwrap()
31
+ .as_nanos();
32
+ self.profile.duration_ns = source
33
+ .end_instant
34
+ .unwrap()
35
+ .duration_since(source.start_instant)
36
+ .as_nanos();
37
+
38
+ // Create a Sample for each sample collected
39
+ for sample in source.samples.iter() {
40
+ let mut stack: Vec<LocationIndex> = vec![];
41
+
42
+ // Iterate over the Ruby stack
43
+ let ruby_stack_depth = sample.line_count;
44
+ for i in 0..ruby_stack_depth {
45
+ let frame: VALUE = sample.frames[i as usize];
46
+ let lineno: i32 = sample.linenos[i as usize];
47
+
48
+ // Get the Location corresponding to the frame.
49
+ let location_index = self.process_ruby_frame(frame, lineno);
50
+
51
+ stack.push(location_index);
52
+ }
53
+
54
+ self.profile.samples.push(Sample {
55
+ stack,
56
+ ruby_thread_id: Some(sample.ruby_thread),
57
+ });
58
+ }
59
+
60
+ serde_json::to_string(&self.profile).unwrap()
61
+ }
62
+
63
+ /// Process a collected Ruby frame.
64
+ /// Calling this method will modify `self.profile` in place.
65
+ ///
66
+ /// Returns the index of the location in `locations`.
67
+ fn process_ruby_frame(&mut self, frame: VALUE, lineno: i32) -> LocationIndex {
68
+ // Build a Function corresponding to the frame, and get the index in `functions`
69
+ let function = Self::extract_function_from_frame(frame);
70
+ let function_index = match self
71
+ .profile
72
+ .functions
73
+ .iter_mut()
74
+ .position(|f| *f == function)
75
+ {
76
+ Some(index) => index,
77
+ None => {
78
+ self.profile.functions.push(function);
79
+ self.profile.functions.len() - 1
80
+ }
81
+ };
82
+
83
+ // Build a Location based on (1) the Function and (2) the actual line hit during sampling.
84
+ let location = Location {
85
+ function_index,
86
+ lineno,
87
+ address: None,
88
+ };
89
+ // Get the index of the location in `locations`
90
+ match self
91
+ .profile
92
+ .locations
93
+ .iter_mut()
94
+ .position(|l| *l == location)
95
+ {
96
+ Some(index) => index,
97
+ None => {
98
+ self.profile.locations.push(location);
99
+ self.profile.locations.len() - 1
100
+ }
101
+ }
102
+ }
103
+
104
+ fn extract_function_from_frame(frame: VALUE) -> Function {
105
+ unsafe {
106
+ let mut frame_full_label: VALUE = rb_profile_frame_full_label(frame);
107
+ let frame_full_label: Option<String> = if RTEST(frame_full_label) {
108
+ Some(
109
+ CStr::from_ptr(rb_string_value_cstr(&mut frame_full_label))
110
+ .to_str()
111
+ .unwrap()
112
+ .to_owned(),
113
+ )
114
+ } else {
115
+ None
116
+ };
117
+
118
+ let mut frame_path: VALUE = rb_profile_frame_path(frame);
119
+ let frame_path: Option<String> = if RTEST(frame_path) {
120
+ Some(
121
+ CStr::from_ptr(rb_string_value_cstr(&mut frame_path))
122
+ .to_str()
123
+ .unwrap()
124
+ .to_owned(),
125
+ )
126
+ } else {
127
+ None
128
+ };
129
+
130
+ let frame_first_lineno: VALUE = rb_profile_frame_first_lineno(frame);
131
+ let frame_first_lineno: Option<i32> = if RTEST(frame_first_lineno) {
132
+ Some(rb_num2int(frame_first_lineno).try_into().unwrap())
133
+ } else {
134
+ None
135
+ };
136
+
137
+ Function {
138
+ implementation: FunctionImplementation::Ruby,
139
+ name: frame_full_label,
140
+ filename: frame_path,
141
+ start_lineno: frame_first_lineno,
142
+ start_address: None,
143
+ }
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,2 @@
1
+ pub mod profile;
2
+ pub mod serializer;
@@ -15,7 +15,7 @@ pub const DEFAULT_SCHEDULER: Scheduler = Scheduler::TimerThread;
15
15
  #[cfg(not(target_os = "linux"))]
16
16
  pub const DEFAULT_TIME_MODE: TimeMode = TimeMode::WallTime;
17
17
 
18
- pub const DEFAULT_INTERVAL: Duration = Duration::from_millis(49);
18
+ pub const DEFAULT_INTERVAL: Duration = Duration::from_millis(9);
19
19
 
20
20
  #[derive(Clone, Debug)]
21
21
  pub struct Configuration {
@@ -23,6 +23,7 @@ pub struct Configuration {
23
23
  pub interval: Duration,
24
24
  pub time_mode: TimeMode,
25
25
  pub target_ruby_threads: Threads,
26
+ pub use_experimental_serializer: bool,
26
27
  }
27
28
 
28
29
  #[derive(Clone, Debug, PartialEq)]
@@ -38,7 +38,7 @@ impl Session {
38
38
  unsafe {
39
39
  rb_scan_args(argc, argv, cstr!(":"), &kwargs);
40
40
  };
41
- let mut kwargs_values: [VALUE; 4] = [Qnil.into(); 4];
41
+ let mut kwargs_values: [VALUE; 5] = [Qnil.into(); 5];
42
42
  unsafe {
43
43
  rb_get_kwargs(
44
44
  kwargs,
@@ -47,10 +47,11 @@ impl Session {
47
47
  rb_intern(cstr!("threads")),
48
48
  rb_intern(cstr!("time_mode")),
49
49
  rb_intern(cstr!("scheduler")),
50
+ rb_intern(cstr!("use_experimental_serializer")),
50
51
  ]
51
52
  .as_mut_ptr(),
52
53
  0,
53
- 4,
54
+ 5,
54
55
  kwargs_values.as_mut_ptr(),
55
56
  );
56
57
  };
@@ -59,12 +60,15 @@ impl Session {
59
60
  let threads = Self::parse_option_threads(kwargs_values[1]);
60
61
  let time_mode = Self::parse_option_time_mode(kwargs_values[2]);
61
62
  let scheduler = Self::parse_option_scheduler(kwargs_values[3]);
63
+ let use_experimental_serializer =
64
+ Self::parse_option_use_experimental_serializer(kwargs_values[4]);
62
65
 
63
66
  let configuration = Configuration {
64
67
  scheduler,
65
68
  interval,
66
69
  target_ruby_threads: threads.clone(),
67
70
  time_mode,
71
+ use_experimental_serializer,
68
72
  };
69
73
 
70
74
  match configuration.validate() {
@@ -127,10 +131,10 @@ impl Session {
127
131
  let interval_ms = unsafe { rb_num2long(value) };
128
132
  Duration::from_millis(interval_ms.try_into().unwrap_or_else(|_| {
129
133
  eprintln!(
130
- "[Pf2] Warning: Specified interval ({}) is not valid. Using default value (49ms).",
134
+ "[Pf2] Warning: Specified interval ({}) is not valid. Using default value (9ms).",
131
135
  interval_ms
132
136
  );
133
- 49
137
+ 9
134
138
  }))
135
139
  }
136
140
 
@@ -207,6 +211,13 @@ impl Session {
207
211
  scheduler
208
212
  }
209
213
 
214
+ fn parse_option_use_experimental_serializer(value: VALUE) -> bool {
215
+ if value == Qundef as VALUE {
216
+ return false;
217
+ }
218
+ RTEST(value)
219
+ }
220
+
210
221
  pub fn start(&mut self) -> VALUE {
211
222
  self.running.store(true, Ordering::Relaxed);
212
223
  self.start_profile_buffer_flusher_thread();
@@ -5,6 +5,7 @@ use crate::profile_serializer::ProfileSerializer;
5
5
  use crate::ruby_internal_apis::rb_thread_getcpuclockid;
6
6
  use crate::sample::Sample;
7
7
  use crate::scheduler::Scheduler;
8
+ use crate::serialization::serializer::ProfileSerializer2;
8
9
  use crate::session::configuration::{self, Configuration};
9
10
 
10
11
  use core::panic;
@@ -46,6 +47,7 @@ impl Scheduler for SignalScheduler {
46
47
  match self.profile.try_write() {
47
48
  Ok(mut profile) => {
48
49
  profile.flush_temporary_sample_buffer();
50
+ profile.end_instant = Some(std::time::Instant::now());
49
51
  }
50
52
  Err(_) => {
51
53
  println!("[pf2 ERROR] stop: Failed to acquire profile lock.");
@@ -56,7 +58,11 @@ impl Scheduler for SignalScheduler {
56
58
  let profile = self.profile.try_read().unwrap();
57
59
  log::debug!("Number of samples: {}", profile.samples.len());
58
60
 
59
- let serialized = ProfileSerializer::serialize(&profile);
61
+ let serialized = if self.configuration.use_experimental_serializer {
62
+ ProfileSerializer2::new().serialize(&profile)
63
+ } else {
64
+ ProfileSerializer::serialize(&profile)
65
+ };
60
66
  let serialized = CString::new(serialized).unwrap();
61
67
  unsafe { rb_str_new_cstr(serialized.as_ptr()) }
62
68
  }
@@ -12,6 +12,7 @@ use crate::profile::Profile;
12
12
  use crate::profile_serializer::ProfileSerializer;
13
13
  use crate::sample::Sample;
14
14
  use crate::scheduler::Scheduler;
15
+ use crate::serialization::serializer::ProfileSerializer2;
15
16
  use crate::session::configuration::{self, Configuration};
16
17
  use crate::util::*;
17
18
 
@@ -61,6 +62,7 @@ impl Scheduler for TimerThreadScheduler {
61
62
  match self.profile.try_write() {
62
63
  Ok(mut profile) => {
63
64
  profile.flush_temporary_sample_buffer();
65
+ profile.end_instant = Some(std::time::Instant::now());
64
66
  }
65
67
  Err(_) => {
66
68
  println!("[pf2 ERROR] stop: Failed to acquire profile lock.");
@@ -71,7 +73,11 @@ impl Scheduler for TimerThreadScheduler {
71
73
  let profile = self.profile.try_read().unwrap();
72
74
  log::debug!("Number of samples: {}", profile.samples.len());
73
75
 
74
- let serialized = ProfileSerializer::serialize(&profile);
76
+ let serialized = if self.configuration.use_experimental_serializer {
77
+ ProfileSerializer2::new().serialize(&profile)
78
+ } else {
79
+ ProfileSerializer::serialize(&profile)
80
+ };
75
81
  let serialized = CString::new(serialized).unwrap();
76
82
  unsafe { rb_str_new_cstr(serialized.as_ptr()) }
77
83
  }
data/lib/pf2/cli.rb CHANGED
@@ -55,7 +55,7 @@ module Pf2
55
55
  option_parser.parse!(argv)
56
56
 
57
57
  profile = JSON.parse(File.read(argv[0]), symbolize_names: true, max_nesting: false)
58
- report = JSON.generate(Pf2::Reporter.new(profile).emit)
58
+ report = JSON.generate(Pf2::Reporter::FirefoxProfiler.new(profile).emit)
59
59
 
60
60
  if options[:output_file]
61
61
  File.write(options[:output_file], report)
@@ -0,0 +1,395 @@
1
+ require 'json'
2
+
3
+ module Pf2
4
+ module Reporter
5
+ # Generates Firefox Profiler's "processed profile format"
6
+ # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/processed-profile-format.md
7
+ class FirefoxProfiler
8
+ def initialize(profile)
9
+ @profile = FirefoxProfiler.deep_intize_keys(profile)
10
+ end
11
+
12
+ def inspect
13
+ "" # TODO: provide something better
14
+ end
15
+
16
+ def emit
17
+ report = {
18
+ meta: {
19
+ interval: 10, # ms; TODO: replace with actual interval
20
+ start_time: 0,
21
+ process_type: 0,
22
+ product: 'ruby',
23
+ stackwalk: 0,
24
+ version: 28,
25
+ preprocessed_profile_version: 47,
26
+ symbolicated: true,
27
+ categories: [
28
+ {
29
+ name: "Logs",
30
+ color: "grey",
31
+ subcategories: ["Unused"],
32
+ },
33
+ {
34
+ name: "Ruby",
35
+ color: "red",
36
+ subcategories: ["Code"],
37
+ },
38
+ {
39
+ name: "Native",
40
+ color: "blue",
41
+ subcategories: ["Code"],
42
+ },
43
+ {
44
+ name: "Native",
45
+ color: "lightblue",
46
+ subcategories: ["Code"],
47
+ },
48
+ ],
49
+ marker_schema: [],
50
+ },
51
+ libs: [],
52
+ counters: [],
53
+ threads: @profile[:threads].values.map {|th| ThreadReport.new(th).emit }
54
+ }
55
+ FirefoxProfiler.deep_camelize_keys(report)
56
+ end
57
+
58
+ class ThreadReport
59
+ def initialize(thread)
60
+ @thread = thread
61
+
62
+ # Populated in other methods
63
+ @func_id_map = {}
64
+ @frame_id_map = {}
65
+ @stack_tree_id_map = {}
66
+
67
+ @string_table = {}
68
+ end
69
+
70
+ def inspect
71
+ "" # TODO: provide something better
72
+ end
73
+
74
+ def emit
75
+ x = weave_native_stack(@thread[:stack_tree])
76
+ @thread[:stack_tree] = x
77
+ func_table = build_func_table
78
+ frame_table = build_frame_table
79
+ stack_table = build_stack_table(func_table, frame_table)
80
+ samples = build_samples
81
+
82
+ string_table = build_string_table
83
+
84
+ {
85
+ process_type: 'default',
86
+ process_name: 'ruby',
87
+ process_startup_time: 0,
88
+ process_shutdown_time: nil,
89
+ register_time: 0,
90
+ unregister_time: nil,
91
+ paused_ranges: [],
92
+ name: "Thread (tid: #{@thread[:thread_id]})",
93
+ is_main_thread: true,
94
+ is_js_tracer: true,
95
+ # FIXME: We can fill the correct PID only after we correctly fill is_main_thread
96
+ # (only one thread could be marked as is_main_thread in a single process)
97
+ pid: @thread[:thread_id],
98
+ tid: @thread[:thread_id],
99
+ samples: samples,
100
+ markers: markers,
101
+ stack_table: stack_table,
102
+ frame_table: frame_table,
103
+ string_array: build_string_table,
104
+ func_table: func_table,
105
+ resource_table: {
106
+ lib: [],
107
+ name: [],
108
+ host: [],
109
+ type: [],
110
+ length: 0,
111
+ },
112
+ native_symbols: [],
113
+ }
114
+ end
115
+
116
+ def build_samples
117
+ ret = {
118
+ event_delay: [],
119
+ stack: [],
120
+ time: [],
121
+ duration: [],
122
+ # weight: nil,
123
+ # weight_type: 'samples',
124
+ }
125
+
126
+ @thread[:samples].each do |sample|
127
+ ret[:stack] << @stack_tree_id_map[sample[:stack_tree_id]]
128
+ ret[:time] << sample[:elapsed_ns] / 1000000 # ns -> ms
129
+ ret[:duration] << 1
130
+ ret[:event_delay] << 0
131
+ end
132
+
133
+ ret[:length] = ret[:stack].length
134
+ ret
135
+ end
136
+
137
+ def build_frame_table
138
+ ret = {
139
+ address: [],
140
+ category: [],
141
+ subcategory: [],
142
+ func: [],
143
+ inner_window_id: [],
144
+ implementation: [],
145
+ line: [],
146
+ column: [],
147
+ optimizations: [],
148
+ inline_depth: [],
149
+ native_symbol: [],
150
+ }
151
+
152
+ @thread[:frames].each.with_index do |(id, frame), i|
153
+ ret[:address] << frame[:address].to_s
154
+ ret[:category] << 1
155
+ ret[:subcategory] << 1
156
+ ret[:func] << i # TODO
157
+ ret[:inner_window_id] << nil
158
+ ret[:implementation] << nil
159
+ ret[:line] << frame[:callsite_lineno]
160
+ ret[:column] << nil
161
+ ret[:optimizations] << nil
162
+ ret[:inline_depth] << 0
163
+ ret[:native_symbol] << nil
164
+
165
+ @frame_id_map[id] = i
166
+ end
167
+
168
+ ret[:length] = ret[:address].length
169
+ ret
170
+ end
171
+
172
+ def build_func_table
173
+ ret = {
174
+ name: [],
175
+ is_js: [],
176
+ relevant_for_js: [],
177
+ resource: [],
178
+ file_name: [],
179
+ line_number: [],
180
+ column_number: [],
181
+ }
182
+
183
+ @thread[:frames].each.with_index do |(id, frame), i|
184
+ native = (frame[:entry_type] == 'Native')
185
+ label = "#{native ? "Native: " : ""}#{frame[:full_label]}"
186
+ ret[:name] << string_id(label)
187
+ ret[:is_js] << !native
188
+ ret[:relevant_for_js] << false
189
+ ret[:resource] << -1
190
+ ret[:file_name] << string_id(frame[:file_name])
191
+ ret[:line_number] << frame[:function_first_lineno]
192
+ ret[:column_number] << nil
193
+
194
+ @func_id_map[id] = i
195
+ end
196
+
197
+ ret[:length] = ret[:name].length
198
+ ret
199
+ end
200
+
201
+ # "Weave" the native stack into the Ruby stack.
202
+ #
203
+ # Strategy:
204
+ # - Split the stack into Ruby and Native parts
205
+ # - Start from the root of the Native stack
206
+ # - Dig in to the native stack until we hit a rb_vm_exec(), which marks a call into Ruby code
207
+ # - Switch to Ruby stack. Keep digging until we hit a Cfunc call, then switch back to Native stack
208
+ # - Repeat until we consume the entire stack
209
+ def weave_native_stack(stack_tree)
210
+ collected_paths = []
211
+ tree_to_array_of_paths(stack_tree, @thread[:frames], [], collected_paths)
212
+ collected_paths = collected_paths.map do |path|
213
+ next if path.size == 0
214
+
215
+ new_path = []
216
+ new_path << path.shift # root
217
+
218
+ # Split the stack into Ruby and Native parts
219
+ native_path, ruby_path = path.partition do |frame|
220
+ frame_id = frame[:frame_id]
221
+ @thread[:frames][frame_id][:entry_type] == 'Native'
222
+ end
223
+
224
+ mode = :native
225
+
226
+ loop do
227
+ break if ruby_path.size == 0 && native_path.size == 0
228
+
229
+ case mode
230
+ when :ruby
231
+ if ruby_path.size == 0
232
+ mode = :native
233
+ next
234
+ end
235
+
236
+ next_node = ruby_path[0]
237
+ new_path << ruby_path.shift
238
+ next_node_frame = @thread[:frames][next_node[:frame_id]]
239
+ if native_path.size > 0
240
+ # Search the remainder of the native stack for the same address
241
+ # Note: This isn't a very efficient way for the job... but it still works
242
+ ruby_addr = next_node_frame[:address]
243
+ native_path[0..].each do |native_node|
244
+ native_addr = @thread[:frames][native_node[:frame_id]][:address]
245
+ if ruby_addr && native_addr && ruby_addr == native_addr
246
+ # A match has been found. Switch to native mode
247
+ mode = :native
248
+ break
249
+ end
250
+ end
251
+ end
252
+ when :native
253
+ if native_path.size == 0
254
+ mode = :ruby
255
+ next
256
+ end
257
+
258
+ # Dig until we meet a rb_vm_exec
259
+ next_node = native_path[0]
260
+ new_path << native_path.shift
261
+ if @thread[:frames][next_node[:frame_id]][:full_label] =~ /vm_exec_core/ # VM_EXEC in vm_exec.h
262
+ mode = :ruby
263
+ end
264
+ end
265
+ end
266
+
267
+ new_path
268
+ end
269
+
270
+ # reconstruct stack_tree
271
+ new_stack_tree = array_of_paths_to_tree(collected_paths)
272
+ new_stack_tree
273
+ end
274
+
275
+ def tree_to_array_of_paths(stack_tree, frames, path, collected_paths)
276
+ new_path = path + [{ frame_id: stack_tree[:frame_id], node_id: stack_tree[:node_id] }]
277
+ if stack_tree[:children].empty?
278
+ collected_paths << new_path
279
+ else
280
+ stack_tree[:children].each do |frame_id, child|
281
+ tree_to_array_of_paths(child, frames, new_path, collected_paths)
282
+ end
283
+ end
284
+ end
285
+
286
+ def array_of_paths_to_tree(paths)
287
+ new_stack_tree = { children: {}, node_id: 0, frame_id: 0 }
288
+ paths.each do |path|
289
+ current = new_stack_tree
290
+ path[1..].each do |frame|
291
+ frame_id = frame[:frame_id]
292
+ node_id = frame[:node_id]
293
+ current[:children][frame_id] ||= { children: {}, node_id: node_id, frame_id: frame_id }
294
+ current = current[:children][frame_id]
295
+ end
296
+ end
297
+ new_stack_tree
298
+ end
299
+
300
+ def build_stack_table(func_table, frame_table)
301
+ ret = {
302
+ frame: [],
303
+ category: [],
304
+ subcategory: [],
305
+ prefix: [],
306
+ }
307
+
308
+ queue = []
309
+
310
+ @thread[:stack_tree][:children].each {|_, c| queue << [nil, c] }
311
+
312
+ loop do
313
+ break if queue.size == 0
314
+
315
+ prefix, node = queue.shift
316
+ ret[:frame] << @frame_id_map[node[:frame_id]]
317
+ ret[:category] << (build_string_table[func_table[:name][frame_table[:func][@frame_id_map[node[:frame_id]]]]].start_with?('Native:') ? 2 : 1)
318
+ ret[:subcategory] << nil
319
+ ret[:prefix] << prefix
320
+
321
+ # The index of this frame - children can refer to this frame using this index as prefix
322
+ frame_index = ret[:frame].length - 1
323
+ @stack_tree_id_map[node[:node_id]] = frame_index
324
+
325
+ # Enqueue children nodes
326
+ node[:children].each {|_, c| queue << [frame_index, c] }
327
+ end
328
+
329
+ ret[:length] = ret[:frame].length
330
+ ret
331
+ end
332
+
333
+ def build_string_table
334
+ @string_table.sort_by {|_, v| v}.map {|s| s[0] }
335
+ end
336
+
337
+ def string_id(str)
338
+ return @string_table[str] if @string_table.has_key?(str)
339
+ @string_table[str] = @string_table.length
340
+ @string_table[str]
341
+ end
342
+
343
+ def markers
344
+ {
345
+ data: [],
346
+ name: [],
347
+ time: [],
348
+ start_time: [],
349
+ end_time: [],
350
+ phase: [],
351
+ category: [],
352
+ length: 0
353
+ }
354
+ end
355
+ end
356
+
357
+ # Util functions
358
+ class << self
359
+ def snake_to_camel(s)
360
+ return "isJS" if s == "is_js"
361
+ return "relevantForJS" if s == "relevant_for_js"
362
+ return "innerWindowID" if s == "inner_window_id"
363
+ s.split('_').inject([]) {|buffer, p| buffer.push(buffer.size == 0 ? p : p.capitalize) }.join
364
+ end
365
+
366
+ def deep_transform_keys(value, &block)
367
+ case value
368
+ when Array
369
+ value.map {|v| deep_transform_keys(v, &block) }
370
+ when Hash
371
+ Hash[value.map {|k, v| [yield(k), deep_transform_keys(v, &block)] }]
372
+ else
373
+ value
374
+ end
375
+ end
376
+
377
+ def deep_camelize_keys(value)
378
+ deep_transform_keys(value) do |key|
379
+ snake_to_camel(key.to_s).to_sym
380
+ end
381
+ end
382
+
383
+ def deep_intize_keys(value)
384
+ deep_transform_keys(value) do |key|
385
+ if key.to_s.to_i.to_s == key.to_s
386
+ key.to_s.to_i
387
+ else
388
+ key
389
+ end
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
395
+ end
data/lib/pf2/reporter.rb CHANGED
@@ -1,393 +1,5 @@
1
- require 'json'
1
+ require_relative './reporter/firefox_profiler'
2
2
 
3
3
  module Pf2
4
- # Generates Firefox Profiler's "processed profile format"
5
- # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/processed-profile-format.md
6
- class Reporter
7
- def initialize(profile)
8
- @profile = Reporter.deep_intize_keys(profile)
9
- end
10
-
11
- def inspect
12
- "" # TODO: provide something better
13
- end
14
-
15
- def emit
16
- report = {
17
- meta: {
18
- interval: 10, # ms; TODO: replace with actual interval
19
- start_time: 0,
20
- process_type: 0,
21
- product: 'ruby',
22
- stackwalk: 0,
23
- version: 28,
24
- preprocessed_profile_version: 47,
25
- symbolicated: true,
26
- categories: [
27
- {
28
- name: "Logs",
29
- color: "grey",
30
- subcategories: ["Unused"],
31
- },
32
- {
33
- name: "Ruby",
34
- color: "red",
35
- subcategories: ["Code"],
36
- },
37
- {
38
- name: "Native",
39
- color: "blue",
40
- subcategories: ["Code"],
41
- },
42
- {
43
- name: "Native",
44
- color: "lightblue",
45
- subcategories: ["Code"],
46
- },
47
- ],
48
- marker_schema: [],
49
- },
50
- libs: [],
51
- counters: [],
52
- threads: @profile[:threads].values.map {|th| ThreadReport.new(th).emit }
53
- }
54
- Reporter.deep_camelize_keys(report)
55
- end
56
-
57
- class ThreadReport
58
- def initialize(thread)
59
- @thread = thread
60
-
61
- # Populated in other methods
62
- @func_id_map = {}
63
- @frame_id_map = {}
64
- @stack_tree_id_map = {}
65
-
66
- @string_table = {}
67
- end
68
-
69
- def inspect
70
- "" # TODO: provide something better
71
- end
72
-
73
- def emit
74
- x = weave_native_stack(@thread[:stack_tree])
75
- @thread[:stack_tree] = x
76
- func_table = build_func_table
77
- frame_table = build_frame_table
78
- stack_table = build_stack_table(func_table, frame_table)
79
- samples = build_samples
80
-
81
- string_table = build_string_table
82
-
83
- {
84
- process_type: 'default',
85
- process_name: 'ruby',
86
- process_startup_time: 0,
87
- process_shutdown_time: nil,
88
- register_time: 0,
89
- unregister_time: nil,
90
- paused_ranges: [],
91
- name: "Thread (tid: #{@thread[:thread_id]})",
92
- is_main_thread: true,
93
- is_js_tracer: true,
94
- # FIXME: We can fill the correct PID only after we correctly fill is_main_thread
95
- # (only one thread could be marked as is_main_thread in a single process)
96
- pid: @thread[:thread_id],
97
- tid: @thread[:thread_id],
98
- samples: samples,
99
- markers: markers,
100
- stack_table: stack_table,
101
- frame_table: frame_table,
102
- string_array: build_string_table,
103
- func_table: func_table,
104
- resource_table: {
105
- lib: [],
106
- name: [],
107
- host: [],
108
- type: [],
109
- length: 0,
110
- },
111
- native_symbols: [],
112
- }
113
- end
114
-
115
- def build_samples
116
- ret = {
117
- event_delay: [],
118
- stack: [],
119
- time: [],
120
- duration: [],
121
- # weight: nil,
122
- # weight_type: 'samples',
123
- }
124
-
125
- @thread[:samples].each do |sample|
126
- ret[:stack] << @stack_tree_id_map[sample[:stack_tree_id]]
127
- ret[:time] << sample[:elapsed_ns] / 1000000 # ns -> ms
128
- ret[:duration] << 1
129
- ret[:event_delay] << 0
130
- end
131
-
132
- ret[:length] = ret[:stack].length
133
- ret
134
- end
135
-
136
- def build_frame_table
137
- ret = {
138
- address: [],
139
- category: [],
140
- subcategory: [],
141
- func: [],
142
- inner_window_id: [],
143
- implementation: [],
144
- line: [],
145
- column: [],
146
- optimizations: [],
147
- inline_depth: [],
148
- native_symbol: [],
149
- }
150
-
151
- @thread[:frames].each.with_index do |(id, frame), i|
152
- ret[:address] << frame[:address].to_s
153
- ret[:category] << 1
154
- ret[:subcategory] << 1
155
- ret[:func] << i # TODO
156
- ret[:inner_window_id] << nil
157
- ret[:implementation] << nil
158
- ret[:line] << frame[:callsite_lineno]
159
- ret[:column] << nil
160
- ret[:optimizations] << nil
161
- ret[:inline_depth] << 0
162
- ret[:native_symbol] << nil
163
-
164
- @frame_id_map[id] = i
165
- end
166
-
167
- ret[:length] = ret[:address].length
168
- ret
169
- end
170
-
171
- def build_func_table
172
- ret = {
173
- name: [],
174
- is_js: [],
175
- relevant_for_js: [],
176
- resource: [],
177
- file_name: [],
178
- line_number: [],
179
- column_number: [],
180
- }
181
-
182
- @thread[:frames].each.with_index do |(id, frame), i|
183
- native = (frame[:entry_type] == 'Native')
184
- label = "#{native ? "Native: " : ""}#{frame[:full_label]}"
185
- ret[:name] << string_id(label)
186
- ret[:is_js] << !native
187
- ret[:relevant_for_js] << false
188
- ret[:resource] << -1
189
- ret[:file_name] << string_id(frame[:file_name])
190
- ret[:line_number] << frame[:function_first_lineno]
191
- ret[:column_number] << nil
192
-
193
- @func_id_map[id] = i
194
- end
195
-
196
- ret[:length] = ret[:name].length
197
- ret
198
- end
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
-
299
- def build_stack_table(func_table, frame_table)
300
- ret = {
301
- frame: [],
302
- category: [],
303
- subcategory: [],
304
- prefix: [],
305
- }
306
-
307
- queue = []
308
-
309
- @thread[:stack_tree][:children].each {|_, c| queue << [nil, c] }
310
-
311
- loop do
312
- break if queue.size == 0
313
-
314
- prefix, node = queue.shift
315
- ret[:frame] << @frame_id_map[node[:frame_id]]
316
- ret[:category] << (build_string_table[func_table[:name][frame_table[:func][@frame_id_map[node[:frame_id]]]]].start_with?('Native:') ? 2 : 1)
317
- ret[:subcategory] << nil
318
- ret[:prefix] << prefix
319
-
320
- # The index of this frame - children can refer to this frame using this index as prefix
321
- frame_index = ret[:frame].length - 1
322
- @stack_tree_id_map[node[:node_id]] = frame_index
323
-
324
- # Enqueue children nodes
325
- node[:children].each {|_, c| queue << [frame_index, c] }
326
- end
327
-
328
- ret[:length] = ret[:frame].length
329
- ret
330
- end
331
-
332
- def build_string_table
333
- @string_table.sort_by {|_, v| v}.map {|s| s[0] }
334
- end
335
-
336
- def string_id(str)
337
- return @string_table[str] if @string_table.has_key?(str)
338
- @string_table[str] = @string_table.length
339
- @string_table[str]
340
- end
341
-
342
- def markers
343
- {
344
- data: [],
345
- name: [],
346
- time: [],
347
- start_time: [],
348
- end_time: [],
349
- phase: [],
350
- category: [],
351
- length: 0
352
- }
353
- end
354
- end
355
-
356
- # Util functions
357
- class << self
358
- def snake_to_camel(s)
359
- return "isJS" if s == "is_js"
360
- return "relevantForJS" if s == "relevant_for_js"
361
- return "innerWindowID" if s == "inner_window_id"
362
- s.split('_').inject([]) {|buffer, p| buffer.push(buffer.size == 0 ? p : p.capitalize) }.join
363
- end
364
-
365
- def deep_transform_keys(value, &block)
366
- case value
367
- when Array
368
- value.map {|v| deep_transform_keys(v, &block) }
369
- when Hash
370
- Hash[value.map {|k, v| [yield(k), deep_transform_keys(v, &block)] }]
371
- else
372
- value
373
- end
374
- end
375
-
376
- def deep_camelize_keys(value)
377
- deep_transform_keys(value) do |key|
378
- snake_to_camel(key.to_s).to_sym
379
- end
380
- end
381
-
382
- def deep_intize_keys(value)
383
- deep_transform_keys(value) do |key|
384
- if key.to_s.to_i.to_s == key.to_s
385
- key.to_s.to_i
386
- else
387
- key
388
- end
389
- end
390
- end
391
- end
392
- end
4
+ module Reporter; end
393
5
  end
data/lib/pf2/serve.rb CHANGED
@@ -28,7 +28,7 @@ module Pf2
28
28
  profile = JSON.parse(profile, symbolize_names: true, max_nesting: false)
29
29
  res.header['Content-Type'] = 'application/json'
30
30
  res.header['Access-Control-Allow-Origin'] = '*'
31
- res.body = JSON.generate(Pf2::Reporter.new((profile)).emit)
31
+ res.body = JSON.generate(Pf2::Reporter::FirefoxProfiler.new((profile)).emit)
32
32
  Pf2.start
33
33
  end
34
34
 
data/lib/pf2/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pf2
2
- VERSION = '0.5.2'
2
+ VERSION = '0.6.0'
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pf2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daisuke Aritomo
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2024-07-12 00:00:00.000000000 Z
10
+ date: 2024-07-14 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rake-compiler
@@ -171,6 +171,9 @@ files:
171
171
  - ext/pf2/src/ruby_internal_apis.rs
172
172
  - ext/pf2/src/sample.rs
173
173
  - ext/pf2/src/scheduler.rs
174
+ - ext/pf2/src/serialization.rs
175
+ - ext/pf2/src/serialization/profile.rs
176
+ - ext/pf2/src/serialization/serializer.rs
174
177
  - ext/pf2/src/session.rs
175
178
  - ext/pf2/src/session/configuration.rs
176
179
  - ext/pf2/src/session/new_thread_watcher.rs
@@ -183,6 +186,7 @@ files:
183
186
  - lib/pf2.rb
184
187
  - lib/pf2/cli.rb
185
188
  - lib/pf2/reporter.rb
189
+ - lib/pf2/reporter/firefox_profiler.rb
186
190
  - lib/pf2/serve.rb
187
191
  - lib/pf2/session.rb
188
192
  - lib/pf2/version.rb