pf2 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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