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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +1 -1
- data/ext/pf2/src/lib.rs +1 -0
- data/ext/pf2/src/profile.rs +7 -3
- data/ext/pf2/src/profile_serializer.rs +1 -1
- data/ext/pf2/src/serialization/profile.rs +46 -0
- data/ext/pf2/src/serialization/serializer.rs +146 -0
- data/ext/pf2/src/serialization.rs +2 -0
- data/ext/pf2/src/session/configuration.rs +2 -1
- data/ext/pf2/src/session.rs +15 -4
- data/ext/pf2/src/signal_scheduler.rs +7 -1
- data/ext/pf2/src/timer_thread_scheduler.rs +7 -1
- data/lib/pf2/cli.rb +1 -1
- data/lib/pf2/reporter/firefox_profiler.rb +395 -0
- data/lib/pf2/reporter.rb +2 -390
- data/lib/pf2/serve.rb +1 -1
- data/lib/pf2/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1aa1dd1d3caa1dd9327382e7de0a44b2c0ff4555a9dff8fdc7244f61c3a13405
|
4
|
+
data.tar.gz: c50895ea4d8285cc9bb223d575cbec7042aacd119a1390b836b818232ef7e214
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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:
|
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
data/ext/pf2/src/profile.rs
CHANGED
@@ -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:
|
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:
|
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.
|
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
|
+
}
|
@@ -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(
|
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)]
|
data/ext/pf2/src/session.rs
CHANGED
@@ -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;
|
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
|
-
|
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 (
|
134
|
+
"[Pf2] Warning: Specified interval ({}) is not valid. Using default value (9ms).",
|
131
135
|
interval_ms
|
132
136
|
);
|
133
|
-
|
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 =
|
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 =
|
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
|
-
|
1
|
+
require_relative './reporter/firefox_profiler'
|
2
2
|
|
3
3
|
module Pf2
|
4
|
-
|
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
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.
|
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-
|
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
|