fxprof-processed-profile-0.4.0/.cargo_vcs_info.json0000644000000001660000000000100157670ustar { "git": { "sha1": "3da7f0e59c735c8e83682bdcc197522bfad5794e" }, "path_in_vcs": "fxprof_processed_profile" }fxprof-processed-profile-0.4.0/.gitignore000064400000000000000000000000230072674642500165670ustar 00000000000000/target Cargo.lock fxprof-processed-profile-0.4.0/Cargo.toml0000644000000020060000000000100137600ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "fxprof-processed-profile" version = "0.4.0" authors = ["Markus Stange "] description = "Create profiles in the Firefox Profiler's processed profile JSON format." readme = "README.md" license = "MIT/Apache-2.0" repository = "https://github.com/mstange/perfrecord/" [dependencies.debugid] version = "0.8.0" [dependencies.fxhash] version = "0.2.1" [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.serde_json] version = "1.0" [dev-dependencies.assert-json-diff] version = "2.0.1" fxprof-processed-profile-0.4.0/Cargo.toml.orig000064400000000000000000000007520072674642500174770ustar 00000000000000[package] name = "fxprof-processed-profile" version = "0.4.0" edition = "2018" authors = ["Markus Stange "] license = "MIT/Apache-2.0" description = "Create profiles in the Firefox Profiler's processed profile JSON format." repository = "https://github.com/mstange/perfrecord/" readme = "README.md" [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } debugid = "0.8.0" fxhash = "0.2.1" [dev-dependencies] assert-json-diff = "2.0.1" fxprof-processed-profile-0.4.0/README.md000064400000000000000000000066600072674642500160730ustar 00000000000000# fxprof-processed-profile A crate that allows creating profiles in the [Firefox Profiler](https://github.com/firefox-devtools/profiler)'s ["Processed profile" format](https://github.com/firefox-devtools/profiler/blob/main/docs-developer/processed-profile-format.md). Still work in progress, under-documented, and will have breaking changes frequently. ## Description This crate is a sibling crate to the `gecko_profile` crate. Profiles produced with this crate can be more efficient because they allow the Firefox Profiler to skip a processing step during loading, and because this format supports a "weight" column in the sample table. The sample weight can be used to collapse duplicate consecutive samples into one sample, which means that the individual sample timestamps don't have to be serialized into the JSON. This can save a ton of space. ## About the format When the Firefox Profiler is used with Firefox, the Firefox Profiler receives profile data in the "Gecko profile" format. Then it converts it into the "processed profile" format. The "processed profile" format is the format in which the files are stored when you upload the profile for sharing, or when you download it as a file. It is different from the "Gecko profile" format in the following ways: - There is one flat list of threads across all processes. Each thread comes with some information about its process, which allows the Firefox Profiler to group threads which belong to the same process. - Because of the flat list, the timestamps in all threads (from all processes) are relative to the same reference timestamp. This is different to the "Gecko profile" format where each process has its own time base. - The various tables in each thread are stored in a "struct of arrays" form. For example, the sample table has one flat list of timestamps, one flat list of stack indexes, and so forth. This is different to the "Gecko profile" format which contains one JS object for every individual sample - that object being an array such as `[stack_index, time, eventDelay, cpuDelta]`. The struct-of-arrays form makes the data cheaper to access and is much easier on the browser's GC. - The sample table in the "processed profile" format supports a weight column. The "Gecko profile" format currently does not have support for sample weights. - Each thread has a `funcTable`, a `resourceTable` and a `nativeSymbols` table. These tables do not exist in the "Gecko profile" format. - The structure of the `frameTable` is different. For example, each frame from the native stack has an integer code address, relative to the library that contains this address. In the "Gecko profile" format, the code address is stored in absolute form (process virtual memory address) as a hex string. - Native stacks in the "processed profile" format use "nudged" return addresses, i.e. return address minus one byte, so that they point into the calling instruction. This is different from the "Gecko profile" format, which uses raw return addresses. The "processed profile" format is almost identical to the JavaScript object structure which the Firefox Profiler keeps in memory; [the only difference](https://github.com/firefox-devtools/profiler/blob/af469ed357890f816ab71fb4ba4c9fe125336d94/src/profile-logic/process-profile.js#L1539-L1556) being the use of `stringArray` (which is a plain JSON array of strings) instead of `stringTable` (which is an object containing both the array and a map for fast string-to-index lookup). fxprof-processed-profile-0.4.0/src/lib.rs000064400000000000000000002356300072674642500165200ustar 00000000000000pub use debugid; use debugid::{CodeId, DebugId}; use fxhash::FxHasher; use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer}; use serde_json::{json, Value}; use std::cmp::Ordering; use std::collections::HashMap; use std::hash::BuildHasherDefault; use std::ops::{Deref, Range}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; mod markers; mod timestamp; pub use markers::*; pub use timestamp::*; type FastHashMap = HashMap>; #[derive(Debug)] pub struct Profile { product: String, interval: Duration, libs: GlobalLibTable, categories: Vec, // append-only for stable CategoryHandles processes: Vec, // append-only for stable ProcessHandles threads: Vec, // append-only for stable ThreadHandles reference_timestamp: ReferenceTimestamp, string_table: GlobalStringTable, marker_schemas: FastHashMap<&'static str, MarkerSchema>, } #[derive(Debug, Clone, Copy, PartialOrd, PartialEq)] pub struct ReferenceTimestamp { ms_since_unix_epoch: f64, } impl ReferenceTimestamp { pub fn from_duration_since_unix_epoch(duration: Duration) -> Self { Self::from_millis_since_unix_epoch(duration.as_secs_f64() * 1000.0) } pub fn from_millis_since_unix_epoch(ms_since_unix_epoch: f64) -> Self { Self { ms_since_unix_epoch, } } pub fn from_system_time(system_time: SystemTime) -> Self { Self::from_duration_since_unix_epoch(system_time.duration_since(UNIX_EPOCH).unwrap()) } } impl Serialize for ReferenceTimestamp { fn serialize(&self, serializer: S) -> Result { self.ms_since_unix_epoch.serialize(serializer) } } #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct InstantTimestampMaker { reference_instant: Instant, } impl From for InstantTimestampMaker { fn from(instant: Instant) -> Self { Self { reference_instant: instant, } } } impl InstantTimestampMaker { pub fn make_ts(&self, instant: Instant) -> Timestamp { Timestamp::from_duration_since_reference( instant.saturating_duration_since(self.reference_instant), ) } } #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct CpuDelta { micros: u64, } impl From for CpuDelta { fn from(duration: Duration) -> Self { Self { micros: duration.as_micros() as u64, } } } impl CpuDelta { const ZERO: Self = Self { micros: 0 }; pub fn from_nanos(nanos: u64) -> Self { Self { micros: nanos / 1000, } } pub fn from_micros(micros: u64) -> Self { Self { micros } } pub fn from_millis(millis: f64) -> Self { Self { micros: (millis * 1_000.0) as u64, } } pub fn is_zero(&self) -> bool { self.micros == 0 } } impl Serialize for CpuDelta { fn serialize(&self, serializer: S) -> Result { // CPU deltas are serialized as float microseconds, because // we set profile.meta.sampleUnits.threadCPUDelta to "µs". self.micros.serialize(serializer) } } /// A library ("binary" / "module" / "DSO") which is loaded into a process. /// This can be the main executable file or a dynamic library, or any other /// mapping of executable memory. /// /// Library information makes after-the-fact symbolication possible: The /// profile JSON contains raw code addresses, and then the symbols for these /// addresses get resolved later. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct LibraryInfo { /// The "actual virtual memory address", in the address space of the process, /// where this library's base address is located. The base address is the /// address which "relative addresses" are relative to. /// /// For ELF binaries, the base address is equal to the "image bias", i.e. the /// offset that is added to the virtual memory addresses as stated in the /// library file (SVMAs, "stated virtual memory addresses"). In other words, /// the base AVMA corresponds to SVMA zero. /// /// For mach-O binaries, the base address is the start of the `__TEXT` segment. /// /// For Windows binaries, the base address is the image load address. pub base_avma: u64, /// The address range that this mapping occupies in the virtual memory /// address space of the process. AVMA = "actual virtual memory address" pub avma_range: Range, /// The name of this library that should be displayed in the profiler. /// Usually this is the filename of the binary, but it could also be any other /// name, such as "[kernel.kallsyms]" or "[vdso]". pub name: String, /// The debug name of this library which should be used when looking up symbols. /// On Windows this is the filename of the PDB file, on other platforms it's /// usually the same as the filename of the binary. pub debug_name: String, /// The absolute path to the binary file. pub path: String, /// The absolute path to the debug file. On Linux and macOS this is the same as /// the path to the binary file. On Windows this is the path to the PDB file. pub debug_path: String, /// The debug ID of the library. This lets symbolication confirm that it's /// getting symbols for the right file, and it can sometimes allow obtaining a /// symbol file from a symbol server. pub debug_id: DebugId, /// The code ID of the library. This lets symbolication confirm that it's /// getting symbols for the right file, and it can sometimes allow obtaining a /// symbol file from a symbol server. pub code_id: Option, /// An optional string with the CPU arch of this library, for example "x86_64", /// "arm64", or "arm64e". Historically, this was used on macOS to find the /// correct sub-binary in a fat binary. But we now use the debug_id for that /// purpose. But it could still be used to find the right dyld shared cache for /// system libraries on macOS. pub arch: Option, } #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct ProcessHandle(usize); #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct ThreadHandle(usize); #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] struct ProcessLibIndex(usize); #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct GlobalLibIndex(usize); #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] struct GlobalStringIndex(StringIndex); #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct StringHandle(GlobalStringIndex); #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] struct ThreadInternalStringIndex(StringIndex); #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] struct StringIndex(u32); #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct CategoryHandle(u16); impl CategoryHandle { /// The "Other" category. All profiles have this category. pub const OTHER: Self = CategoryHandle(0); } #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] struct SubcategoryIndex(u8); #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct CategoryPairHandle(CategoryHandle, Option); impl From for CategoryPairHandle { fn from(category: CategoryHandle) -> Self { CategoryPairHandle(category, None) } } #[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)] pub enum Frame { /// A code address taken from the instruction pointer InstructionPointer(u64), /// A code address taken from a return address ReturnAddress(u64), /// A string, containing an index returned by Profile::intern_string Label(StringHandle), } #[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)] pub enum CategoryColor { Transparent, Purple, Green, Orange, Yellow, LightBlue, Grey, Blue, Brown, LightGreen, Red, LightRed, DarkGray, } impl Profile { pub fn new(product: &str, reference_timestamp: ReferenceTimestamp, interval: Duration) -> Self { Profile { interval, product: product.to_string(), threads: Vec::new(), libs: GlobalLibTable::new(), reference_timestamp, processes: Vec::new(), string_table: GlobalStringTable::new(), marker_schemas: FastHashMap::default(), categories: vec![Category { name: "Other".to_string(), color: CategoryColor::Grey, subcategories: Vec::new(), }], } } pub fn set_interval(&mut self, interval: Duration) { self.interval = interval; } pub fn set_reference_timestamp(&mut self, reference_timestamp: ReferenceTimestamp) { self.reference_timestamp = reference_timestamp; } pub fn set_product(&mut self, product: &str) { self.product = product.to_string(); } pub fn add_category(&mut self, name: &str, color: CategoryColor) -> CategoryHandle { let handle = CategoryHandle(self.categories.len() as u16); self.categories.push(Category { name: name.to_string(), color, subcategories: Vec::new(), }); handle } pub fn add_subcategory(&mut self, category: CategoryHandle, name: &str) -> CategoryPairHandle { let subcategories = &mut self.categories[category.0 as usize].subcategories; use std::convert::TryFrom; let subcategory_index = SubcategoryIndex(u8::try_from(subcategories.len()).unwrap()); subcategories.push(name.to_string()); CategoryPairHandle(category, Some(subcategory_index)) } pub fn add_process(&mut self, name: &str, pid: u32, start_time: Timestamp) -> ProcessHandle { let handle = ProcessHandle(self.processes.len()); self.processes.push(Process { pid, threads: Vec::new(), sorted_lib_ranges: Vec::new(), used_lib_map: FastHashMap::default(), libs: Vec::new(), start_time, end_time: None, name: name.to_owned(), }); handle } pub fn set_process_start_time(&mut self, process: ProcessHandle, start_time: Timestamp) { self.processes[process.0].start_time = start_time; } pub fn set_process_end_time(&mut self, process: ProcessHandle, end_time: Timestamp) { self.processes[process.0].end_time = Some(end_time); } pub fn set_process_name(&mut self, process: ProcessHandle, name: &str) { self.processes[process.0].name = name.to_string(); } pub fn add_lib(&mut self, process: ProcessHandle, library: LibraryInfo) { self.processes[process.0].add_lib(library); } pub fn unload_lib(&mut self, process: ProcessHandle, base_address: u64) { self.processes[process.0].unload_lib(base_address); } pub fn add_thread( &mut self, process: ProcessHandle, tid: u32, start_time: Timestamp, is_main: bool, ) -> ThreadHandle { let handle = ThreadHandle(self.threads.len()); self.threads.push(Thread { process, tid, name: None, start_time, end_time: None, is_main, stack_table: StackTable::new(), frame_table_and_func_table: FrameTableAndFuncTable::new(), samples: SampleTable::new(), markers: MarkerTable::new(), resources: ResourceTable::new(), string_table: ThreadStringTable::new(), last_sample_stack: None, last_sample_was_zero_cpu: false, }); self.processes[process.0].threads.push(handle); handle } pub fn set_thread_name(&mut self, thread: ThreadHandle, name: &str) { self.threads[thread.0].name = Some(name.to_string()); } pub fn set_thread_start_time(&mut self, thread: ThreadHandle, start_time: Timestamp) { self.threads[thread.0].start_time = start_time; } pub fn set_thread_end_time(&mut self, thread: ThreadHandle, end_time: Timestamp) { self.threads[thread.0].end_time = Some(end_time); } pub fn intern_string(&mut self, s: &str) -> StringHandle { StringHandle(self.string_table.index_for_string(s)) } pub fn add_sample( &mut self, thread: ThreadHandle, timestamp: Timestamp, frames: impl Iterator, cpu_delta: CpuDelta, weight: i32, ) { let stack_index = self.stack_index_for_frames(thread, frames); self.threads[thread.0].add_sample(timestamp, stack_index, cpu_delta, weight); } pub fn add_sample_same_stack_zero_cpu( &mut self, thread: ThreadHandle, timestamp: Timestamp, weight: i32, ) { self.threads[thread.0].add_sample_same_stack_zero_cpu(timestamp, weight); } /// Main marker API to add a new marker to profiler buffer. pub fn add_marker( &mut self, thread: ThreadHandle, name: &str, marker: T, timing: MarkerTiming, ) { self.marker_schemas .entry(T::MARKER_TYPE_NAME) .or_insert_with(T::schema); let name_string_index = self.threads[thread.0].string_table.index_for_string(name); self.threads[thread.0].markers.add_marker( name_string_index, timing, marker.json_marker_data(), ); } // frames is ordered from caller to callee, i.e. root function first, pc last fn stack_index_for_frames( &mut self, thread: ThreadHandle, frames: impl Iterator, ) -> Option { let thread = &mut self.threads[thread.0]; let process = &mut self.processes[thread.process.0]; let mut prefix = None; for (frame, category_pair) in frames { let location = match frame { Frame::InstructionPointer(ip) => process.convert_address(&mut self.libs, ip), Frame::ReturnAddress(ra) => { process.convert_address(&mut self.libs, ra.saturating_sub(1)) } Frame::Label(string_index) => { let thread_string_index = thread.convert_string_index(&self.string_table, string_index.0); InternalFrameLocation::Label(thread_string_index) } }; let internal_frame = InternalFrame { location, category_pair, }; let frame_index = thread.frame_index_for_frame(internal_frame, &self.libs); prefix = Some( thread .stack_table .index_for_stack(prefix, frame_index, category_pair), ); } prefix } } impl Serialize for Profile { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(None)?; map.serialize_entry("meta", &SerializableProfileMeta(self))?; map.serialize_entry("libs", &self.libs)?; map.serialize_entry("threads", &SerializableProfileThreadsProperty(self))?; map.serialize_entry("pages", &[] as &[()])?; map.serialize_entry("profilerOverhead", &[] as &[()])?; map.serialize_entry("counters", &[] as &[()])?; map.end() } } struct SerializableProfileMeta<'a>(&'a Profile); impl<'a> Serialize for SerializableProfileMeta<'a> { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(None)?; map.serialize_entry("categories", &self.0.categories)?; map.serialize_entry("debug", &false)?; map.serialize_entry( "extensions", &json!({ "length": 0, "baseURL": [], "id": [], "name": [], }), )?; map.serialize_entry("interval", &(self.0.interval.as_secs_f64() * 1000.0))?; map.serialize_entry("preprocessedProfileVersion", &41)?; map.serialize_entry("processType", &0)?; map.serialize_entry("product", &self.0.product)?; map.serialize_entry( "sampleUnits", &json!({ "time": "ms", "eventDelay": "ms", "threadCPUDelta": "µs", }), )?; map.serialize_entry("startTime", &self.0.reference_timestamp)?; map.serialize_entry("symbolicated", &false)?; map.serialize_entry("pausedRanges", &[] as &[()])?; map.serialize_entry("version", &24)?; let mut marker_schemas: Vec = self.0.marker_schemas.values().cloned().collect(); marker_schemas.sort_by_key(|schema| schema.type_name); map.serialize_entry("markerSchema", &marker_schemas)?; map.end() } } impl Serialize for Category { fn serialize(&self, serializer: S) -> Result { let mut subcategories = self.subcategories.clone(); subcategories.push("Other".to_string()); let mut map = serializer.serialize_map(None)?; map.serialize_entry("name", &self.name)?; map.serialize_entry("color", &self.color)?; map.serialize_entry("subcategories", &subcategories)?; map.end() } } impl Serialize for CategoryColor { fn serialize(&self, serializer: S) -> Result { match self { CategoryColor::Transparent => "transparent".serialize(serializer), CategoryColor::Purple => "purple".serialize(serializer), CategoryColor::Green => "green".serialize(serializer), CategoryColor::Orange => "orange".serialize(serializer), CategoryColor::Yellow => "yellow".serialize(serializer), CategoryColor::LightBlue => "lightblue".serialize(serializer), CategoryColor::Grey => "grey".serialize(serializer), CategoryColor::Blue => "blue".serialize(serializer), CategoryColor::Brown => "brown".serialize(serializer), CategoryColor::LightGreen => "lightgreen".serialize(serializer), CategoryColor::Red => "red".serialize(serializer), CategoryColor::LightRed => "lightred".serialize(serializer), CategoryColor::DarkGray => "darkgray".serialize(serializer), } } } struct SerializableProfileThreadsProperty<'a>(&'a Profile); impl<'a> Serialize for SerializableProfileThreadsProperty<'a> { fn serialize(&self, serializer: S) -> Result { // The processed profile format has all threads from all processes in a flattened threads list. // Each thread duplicates some information about its process, which allows the Firefox Profiler // UI to group threads from the same process. let mut seq = serializer.serialize_seq(Some(self.0.threads.len()))?; let mut sorted_processes: Vec<_> = (0..self.0.processes.len()).map(ProcessHandle).collect(); sorted_processes.sort_by(|a_handle, b_handle| { let a = &self.0.processes[a_handle.0]; let b = &self.0.processes[b_handle.0]; if let Some(ordering) = a.start_time.partial_cmp(&b.start_time) { if ordering != Ordering::Equal { return ordering; } } a.pid.cmp(&b.pid) }); for process in sorted_processes { let mut sorted_threads = self.0.processes[process.0].threads.clone(); sorted_threads.sort_by(|a_handle, b_handle| { let a = &self.0.threads[a_handle.0]; let b = &self.0.threads[b_handle.0]; if let Some(ordering) = a.get_start_time().partial_cmp(&b.get_start_time()) { if ordering != Ordering::Equal { return ordering; } } let ordering = a.get_name().cmp(&b.get_name()); if ordering != Ordering::Equal { return ordering; } a.get_tid().cmp(&b.get_tid()) }); for thread in sorted_threads { seq.serialize_element(&SerializableProfileThread(self.0, thread))?; } } seq.end() } } #[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] struct InternalFrame { location: InternalFrameLocation, category_pair: CategoryPairHandle, } #[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] enum InternalFrameLocation { UnknownAddress(u64), AddressInLib(u32, GlobalLibIndex), Label(ThreadInternalStringIndex), } #[derive(Debug)] struct GlobalLibTable { libs: Vec, // append-only for stable GlobalLibIndexes lib_map: FastHashMap, } impl GlobalLibTable { pub fn new() -> Self { Self { libs: Vec::new(), lib_map: FastHashMap::default(), } } pub fn index_for_lib(&mut self, lib: Lib) -> GlobalLibIndex { let libs = &mut self.libs; *self.lib_map.entry(lib.clone()).or_insert_with(|| { let index = GlobalLibIndex(libs.len()); libs.push(lib); index }) } pub fn lib_name(&self, index: GlobalLibIndex) -> &str { &self.libs[index.0].name } } impl Serialize for GlobalLibTable { fn serialize(&self, serializer: S) -> Result { self.libs.serialize(serializer) } } #[derive(Debug)] struct Category { name: String, color: CategoryColor, subcategories: Vec, } #[derive(Debug)] struct Process { pid: u32, name: String, threads: Vec, start_time: Timestamp, end_time: Option, libs: Vec, sorted_lib_ranges: Vec, used_lib_map: FastHashMap, } impl Process { pub fn convert_address( &mut self, global_libs: &mut GlobalLibTable, address: u64, ) -> InternalFrameLocation { let ranges = &self.sorted_lib_ranges[..]; let index = match ranges.binary_search_by_key(&address, |r| r.start) { Err(0) => return InternalFrameLocation::UnknownAddress(address), Ok(exact_match) => exact_match, Err(insertion_index) => { let range_index = insertion_index - 1; if address < ranges[range_index].end { range_index } else { return InternalFrameLocation::UnknownAddress(address); } } }; let range = &ranges[index]; let process_lib = range.lib_index; let relative_address = (address - range.base) as u32; let lib_index = self.convert_lib_index(process_lib, global_libs); InternalFrameLocation::AddressInLib(relative_address, lib_index) } pub fn convert_lib_index( &mut self, process_lib: ProcessLibIndex, global_libs: &mut GlobalLibTable, ) -> GlobalLibIndex { let libs = &self.libs; *self .used_lib_map .entry(process_lib) .or_insert_with(|| global_libs.index_for_lib(libs[process_lib.0].clone())) } #[allow(clippy::too_many_arguments)] pub fn add_lib(&mut self, lib: LibraryInfo) { let lib_index = ProcessLibIndex(self.libs.len()); self.libs.push(Lib { name: lib.name, debug_name: lib.debug_name, path: lib.path, debug_path: lib.debug_path, arch: lib.arch, debug_id: lib.debug_id, code_id: lib.code_id, }); let insertion_index = match self .sorted_lib_ranges .binary_search_by_key(&lib.avma_range.start, |r| r.start) { Ok(i) => { // We already have a library mapping at this address. // Not sure how to best deal with it. Ideally it wouldn't happen. Let's just remove this mapping. self.sorted_lib_ranges.remove(i); i } Err(i) => i, }; self.sorted_lib_ranges.insert( insertion_index, ProcessLibRange { lib_index, base: lib.base_avma, start: lib.avma_range.start, end: lib.avma_range.end, }, ); } pub fn unload_lib(&mut self, base_address: u64) { self.sorted_lib_ranges.retain(|r| r.base != base_address); } } #[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)] struct ProcessLibRange { start: u64, end: u64, lib_index: ProcessLibIndex, base: u64, } #[derive(Debug)] struct Thread { process: ProcessHandle, tid: u32, name: Option, start_time: Timestamp, end_time: Option, is_main: bool, stack_table: StackTable, frame_table_and_func_table: FrameTableAndFuncTable, samples: SampleTable, markers: MarkerTable, resources: ResourceTable, string_table: ThreadStringTable, last_sample_stack: Option, last_sample_was_zero_cpu: bool, } impl Thread { fn convert_string_index( &mut self, global_table: &GlobalStringTable, index: GlobalStringIndex, ) -> ThreadInternalStringIndex { self.string_table .index_for_global_string(index, global_table) } fn frame_index_for_frame( &mut self, frame: InternalFrame, global_libs: &GlobalLibTable, ) -> usize { self.frame_table_and_func_table.index_for_frame( &mut self.string_table, &mut self.resources, global_libs, frame, ) } pub fn add_sample( &mut self, timestamp: Timestamp, stack_index: Option, cpu_delta: CpuDelta, weight: i32, ) { self.samples.sample_weights.push(weight); self.samples.sample_timestamps.push(timestamp); self.samples.sample_stack_indexes.push(stack_index); self.samples.sample_cpu_deltas.push(cpu_delta); self.last_sample_stack = stack_index; self.last_sample_was_zero_cpu = cpu_delta == CpuDelta::ZERO; } pub fn add_sample_same_stack_zero_cpu(&mut self, timestamp: Timestamp, weight: i32) { if self.last_sample_was_zero_cpu { *self.samples.sample_weights.last_mut().unwrap() += weight; *self.samples.sample_timestamps.last_mut().unwrap() = timestamp; } else { let stack_index = self.last_sample_stack; self.samples.sample_weights.push(weight); self.samples.sample_timestamps.push(timestamp); self.samples.sample_stack_indexes.push(stack_index); self.samples.sample_cpu_deltas.push(CpuDelta::ZERO); self.last_sample_was_zero_cpu = true; } } pub fn get_start_time(&self) -> Timestamp { self.start_time } pub fn get_name(&self) -> Option<&str> { self.name.as_deref() } pub fn get_tid(&self) -> u32 { self.tid } } struct SerializableProfileThread<'a>(&'a Profile, ThreadHandle); impl<'a> Serialize for SerializableProfileThread<'a> { fn serialize(&self, serializer: S) -> Result { let thread_handle = self.1; let thread = &self.0.threads[thread_handle.0]; let process_handle = thread.process; let process = &self.0.processes[process_handle.0]; let process_start_time = process.start_time; let process_end_time = process.end_time; let process_name = &process.name; let pid = process.pid; let tid = thread.tid; let thread_name = if thread.is_main { // https://github.com/firefox-devtools/profiler/issues/2508 "GeckoMain".to_string() } else if let Some(name) = &thread.name { name.clone() } else { format!("Thread <{}>", tid) }; let thread_register_time = thread.start_time; let thread_unregister_time = thread.end_time; let native_symbols = json!({ "length": 0, "address": [], "libIndex": [], "name": [], }); let mut map = serializer.serialize_map(None)?; map.serialize_entry( "frameTable", &thread .frame_table_and_func_table .as_frame_table(&self.0.categories), )?; map.serialize_entry( "funcTable", &thread.frame_table_and_func_table.as_func_table(), )?; map.serialize_entry("markers", &thread.markers)?; map.serialize_entry("name", &thread_name)?; map.serialize_entry("nativeSymbols", &native_symbols)?; map.serialize_entry("pausedRanges", &[] as &[()])?; map.serialize_entry("pid", &pid)?; map.serialize_entry("processName", process_name)?; map.serialize_entry("processShutdownTime", &process_end_time)?; map.serialize_entry("processStartupTime", &process_start_time)?; map.serialize_entry("processType", &"default")?; map.serialize_entry("registerTime", &thread_register_time)?; map.serialize_entry("resourceTable", &thread.resources)?; map.serialize_entry("samples", &thread.samples)?; map.serialize_entry( "stackTable", &SerializableStackTable { table: &thread.stack_table, categories: &self.0.categories, }, )?; map.serialize_entry("stringArray", &thread.string_table)?; map.serialize_entry("tid", &thread.tid)?; map.serialize_entry("unregisterTime", &thread_unregister_time)?; map.end() } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct Lib { name: String, debug_name: String, path: String, debug_path: String, arch: Option, debug_id: DebugId, code_id: Option, } impl<'a> Serialize for Lib { fn serialize(&self, serializer: S) -> Result { let breakpad_id = self.debug_id.breakpad().to_string(); let code_id = self.code_id.as_ref().map(|cid| cid.to_string()); let mut map = serializer.serialize_map(None)?; map.serialize_entry("name", &self.name)?; map.serialize_entry("path", &self.path)?; map.serialize_entry("debugName", &self.debug_name)?; map.serialize_entry("debugPath", &self.debug_path)?; map.serialize_entry("breakpadId", &breakpad_id)?; map.serialize_entry("codeId", &code_id)?; map.serialize_entry("arch", &self.arch)?; map.end() } } #[derive(Debug, Clone, Default)] struct StackTable { stack_prefixes: Vec>, stack_frames: Vec, stack_categories: Vec, stack_subcategories: Vec, // (parent stack, frame_index) -> stack index index: FastHashMap<(Option, usize), usize>, } impl StackTable { pub fn new() -> Self { Default::default() } pub fn index_for_stack( &mut self, prefix: Option, frame: usize, category_pair: CategoryPairHandle, ) -> usize { match self.index.get(&(prefix, frame)) { Some(stack) => *stack, None => { let CategoryPairHandle(category, subcategory_index) = category_pair; let subcategory = match subcategory_index { Some(index) => Subcategory::Normal(index), None => Subcategory::Other(category), }; let stack = self.stack_prefixes.len(); self.stack_prefixes.push(prefix); self.stack_frames.push(frame); self.stack_categories.push(category); self.stack_subcategories.push(subcategory); self.index.insert((prefix, frame), stack); stack } } } } struct SerializableStackTable<'a> { table: &'a StackTable, categories: &'a [Category], } impl<'a> Serialize for SerializableStackTable<'a> { fn serialize(&self, serializer: S) -> Result { let len = self.table.stack_prefixes.len(); let mut map = serializer.serialize_map(Some(3))?; map.serialize_entry("length", &len)?; map.serialize_entry("prefix", &self.table.stack_prefixes)?; map.serialize_entry("frame", &self.table.stack_frames)?; map.serialize_entry("category", &self.table.stack_categories)?; map.serialize_entry( "subcategory", &SerializableSubcategoryColumn(&self.table.stack_subcategories, self.categories), )?; map.end() } } #[derive(Debug, Clone, Default)] struct FrameTableAndFuncTable { // We create one func for every frame. frame_addresses: Vec>, categories: Vec, subcategories: Vec, func_names: Vec, func_resources: Vec>, // address -> frame index index: FastHashMap, } impl FrameTableAndFuncTable { pub fn new() -> Self { Default::default() } pub fn index_for_frame( &mut self, string_table: &mut ThreadStringTable, resource_table: &mut ResourceTable, global_libs: &GlobalLibTable, frame: InternalFrame, ) -> usize { let frame_addresses = &mut self.frame_addresses; let categories = &mut self.categories; let subcategories = &mut self.subcategories; let func_names = &mut self.func_names; let func_resources = &mut self.func_resources; *self.index.entry(frame.clone()).or_insert_with(|| { let frame_index = frame_addresses.len(); let (address, location_string_index, resource) = match frame.location { InternalFrameLocation::UnknownAddress(address) => { let location_string = format!("0x{:x}", address); let s = string_table.index_for_string(&location_string); (None, s, None) } InternalFrameLocation::AddressInLib(address, lib_index) => { let location_string = format!("0x{:x}", address); let s = string_table.index_for_string(&location_string); let res = resource_table.resource_for_lib(lib_index, global_libs, string_table); (Some(address), s, Some(res)) } InternalFrameLocation::Label(string_index) => (None, string_index, None), }; let CategoryPairHandle(category, subcategory_index) = frame.category_pair; let subcategory = match subcategory_index { Some(index) => Subcategory::Normal(index), None => Subcategory::Other(category), }; frame_addresses.push(address); categories.push(category); subcategories.push(subcategory); func_names.push(location_string_index); func_resources.push(resource); frame_index }) } pub fn as_frame_table<'a>(&'a self, categories: &'a [Category]) -> SerializableFrameTable<'a> { SerializableFrameTable { table: self, categories, } } pub fn as_func_table(&self) -> SerializableFuncTable<'_> { SerializableFuncTable(self) } } struct SerializableFrameTable<'a> { table: &'a FrameTableAndFuncTable, categories: &'a [Category], } impl<'a> Serialize for SerializableFrameTable<'a> { fn serialize(&self, serializer: S) -> Result { let len = self.table.frame_addresses.len(); let mut map = serializer.serialize_map(None)?; map.serialize_entry("length", &len)?; map.serialize_entry("address", &SerializableFrameTableAddressColumn(self.table))?; map.serialize_entry("inlineDepth", &SerializableSingleValueColumn(0u32, len))?; map.serialize_entry("category", &self.table.categories)?; map.serialize_entry( "subcategory", &SerializableSubcategoryColumn(&self.table.subcategories, self.categories), )?; map.serialize_entry("func", &SerializableRange(len))?; map.serialize_entry("nativeSymbol", &SerializableSingleValueColumn((), len))?; map.serialize_entry("innerWindowID", &SerializableSingleValueColumn((), len))?; map.serialize_entry("implementation", &SerializableSingleValueColumn((), len))?; map.serialize_entry("line", &SerializableSingleValueColumn((), len))?; map.serialize_entry("column", &SerializableSingleValueColumn((), len))?; map.serialize_entry("optimizations", &SerializableSingleValueColumn((), len))?; map.end() } } impl Serialize for CategoryHandle { fn serialize(&self, serializer: S) -> Result { self.0.serialize(serializer) } } #[derive(Debug, Clone)] enum Subcategory { Normal(SubcategoryIndex), Other(CategoryHandle), } struct SerializableSubcategoryColumn<'a>(&'a [Subcategory], &'a [Category]); impl<'a> Serialize for SerializableSubcategoryColumn<'a> { fn serialize(&self, serializer: S) -> Result { let mut seq = serializer.serialize_seq(Some(self.0.len()))?; for subcategory in self.0 { match subcategory { Subcategory::Normal(index) => seq.serialize_element(&index.0)?, Subcategory::Other(category) => { // There is an implicit "Other" subcategory at the end of each category's // subcategory list. let subcategory_count = self.1[category.0 as usize].subcategories.len(); seq.serialize_element(&subcategory_count)? } } } seq.end() } } struct SerializableFuncTable<'a>(&'a FrameTableAndFuncTable); impl<'a> Serialize for SerializableFuncTable<'a> { fn serialize(&self, serializer: S) -> Result { let len = self.0.frame_addresses.len(); let mut map = serializer.serialize_map(None)?; map.serialize_entry("length", &len)?; map.serialize_entry("name", &self.0.func_names)?; map.serialize_entry("isJS", &SerializableSingleValueColumn(false, len))?; map.serialize_entry("relevantForJS", &SerializableSingleValueColumn(false, len))?; map.serialize_entry("resource", &SerializableFuncTableResourceColumn(self.0))?; map.serialize_entry("fileName", &SerializableSingleValueColumn((), len))?; map.serialize_entry("lineNumber", &SerializableSingleValueColumn((), len))?; map.serialize_entry("columnNumber", &SerializableSingleValueColumn((), len))?; map.end() } } impl Serialize for ThreadInternalStringIndex { fn serialize(&self, serializer: S) -> Result { serializer.serialize_u32(self.0 .0) } } impl Serialize for GlobalLibIndex { fn serialize(&self, serializer: S) -> Result { serializer.serialize_u32(self.0 as u32) } } impl Serialize for ResourceIndex { fn serialize(&self, serializer: S) -> Result { serializer.serialize_u32(self.0 as u32) } } struct SerializableRange(usize); impl Serialize for SerializableRange { fn serialize(&self, serializer: S) -> Result { serializer.collect_seq(0..self.0) } } struct SerializableSingleValueColumn(T, usize); impl Serialize for SerializableSingleValueColumn { fn serialize(&self, serializer: S) -> Result { let mut seq = serializer.serialize_seq(Some(self.1))?; for _ in 0..self.1 { seq.serialize_element(&self.0)?; } seq.end() } } struct SerializableFrameTableAddressColumn<'a>(&'a FrameTableAndFuncTable); impl<'a> Serialize for SerializableFrameTableAddressColumn<'a> { fn serialize(&self, serializer: S) -> Result { let mut seq = serializer.serialize_seq(Some(self.0.frame_addresses.len()))?; for address in &self.0.frame_addresses { match address { Some(address) => seq.serialize_element(&address)?, None => seq.serialize_element(&-1)?, } } seq.end() } } struct SerializableFuncTableResourceColumn<'a>(&'a FrameTableAndFuncTable); impl<'a> Serialize for SerializableFuncTableResourceColumn<'a> { fn serialize(&self, serializer: S) -> Result { let mut seq = serializer.serialize_seq(Some(self.0.func_resources.len()))?; for resource in &self.0.func_resources { match resource { Some(resource) => seq.serialize_element(&resource)?, None => seq.serialize_element(&-1)?, } } seq.end() } } #[derive(Debug, Clone, Default)] struct ResourceTable { resource_libs: Vec, resource_names: Vec, lib_to_resource: FastHashMap, } #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] struct ResourceIndex(u32); impl ResourceTable { pub fn new() -> Self { Default::default() } pub fn resource_for_lib( &mut self, lib_index: GlobalLibIndex, global_libs: &GlobalLibTable, thread_string_table: &mut ThreadStringTable, ) -> ResourceIndex { let resource_libs = &mut self.resource_libs; let resource_names = &mut self.resource_names; *self.lib_to_resource.entry(lib_index).or_insert_with(|| { let resource = ResourceIndex(resource_libs.len() as u32); let lib_name = global_libs.lib_name(lib_index); resource_libs.push(lib_index); resource_names.push(thread_string_table.index_for_string(lib_name)); resource }) } } impl Serialize for ResourceTable { fn serialize(&self, serializer: S) -> Result { const RESOURCE_TYPE_LIB: u32 = 1; let len = self.resource_libs.len(); let mut map = serializer.serialize_map(None)?; map.serialize_entry("length", &len)?; map.serialize_entry("lib", &self.resource_libs)?; map.serialize_entry("name", &self.resource_names)?; map.serialize_entry("host", &SerializableSingleValueColumn((), len))?; map.serialize_entry( "type", &SerializableSingleValueColumn(RESOURCE_TYPE_LIB, len), )?; map.end() } } #[derive(Debug, Clone, Default)] struct SampleTable { sample_weights: Vec, sample_timestamps: Vec, sample_stack_indexes: Vec>, sample_cpu_deltas: Vec, } impl SampleTable { pub fn new() -> Self { Default::default() } } impl Serialize for SampleTable { fn serialize(&self, serializer: S) -> Result { let len = self.sample_timestamps.len(); let mut map = serializer.serialize_map(None)?; map.serialize_entry("length", &len)?; map.serialize_entry("stack", &self.sample_stack_indexes)?; map.serialize_entry("time", &self.sample_timestamps)?; map.serialize_entry("weight", &self.sample_weights)?; map.serialize_entry("weightType", &"samples")?; map.serialize_entry("threadCPUDelta", &self.sample_cpu_deltas)?; map.end() } } #[derive(Debug, Clone, Default)] struct MarkerTable { marker_name_string_indexes: Vec, marker_starts: Vec>, marker_ends: Vec>, marker_phases: Vec, marker_datas: Vec, } impl MarkerTable { pub fn new() -> Self { Default::default() } pub fn add_marker( &mut self, name: ThreadInternalStringIndex, timing: MarkerTiming, data: Value, ) { let (s, e, phase) = match timing { MarkerTiming::Instant(s) => (Some(s), None, Phase::Instant), MarkerTiming::Interval(s, e) => (Some(s), Some(e), Phase::Interval), MarkerTiming::IntervalStart(s) => (Some(s), None, Phase::IntervalStart), MarkerTiming::IntervalEnd(e) => (None, Some(e), Phase::IntervalEnd), }; self.marker_name_string_indexes.push(name); self.marker_starts.push(s); self.marker_ends.push(e); self.marker_phases.push(phase); self.marker_datas.push(data); } } impl Serialize for MarkerTable { fn serialize(&self, serializer: S) -> Result { let len = self.marker_name_string_indexes.len(); let mut map = serializer.serialize_map(None)?; map.serialize_entry("length", &len)?; map.serialize_entry("category", &SerializableSingleValueColumn(0, len))?; map.serialize_entry("data", &self.marker_datas)?; map.serialize_entry( "endTime", &SerializableOptionalTimestampColumn(&self.marker_ends), )?; map.serialize_entry("name", &self.marker_name_string_indexes)?; map.serialize_entry("phase", &self.marker_phases)?; map.serialize_entry( "startTime", &SerializableOptionalTimestampColumn(&self.marker_starts), )?; map.end() } } struct SerializableOptionalTimestampColumn<'a>(&'a [Option]); impl<'a> Serialize for SerializableOptionalTimestampColumn<'a> { fn serialize(&self, serializer: S) -> Result { let mut seq = serializer.serialize_seq(Some(self.0.len()))?; for timestamp in self.0 { match timestamp { Some(timestamp) => seq.serialize_element(×tamp)?, None => seq.serialize_element(&0.0)?, } } seq.end() } } #[derive(Debug, Clone, Copy)] #[repr(u8)] enum Phase { Instant = 0, Interval = 1, IntervalStart = 2, IntervalEnd = 3, } impl Serialize for Phase { fn serialize(&self, serializer: S) -> Result { serializer.serialize_u8(*self as u8) } } #[derive(Debug, Clone, Default)] struct StringTable { strings: Vec, index: FastHashMap, } impl StringTable { pub fn index_for_string(&mut self, s: &str) -> StringIndex { match self.index.get(s) { Some(string_index) => *string_index, None => { let string_index = StringIndex(self.strings.len() as u32); self.strings.push(s.to_string()); self.index.insert(s.to_string(), string_index); string_index } } } pub fn get_string(&self, index: StringIndex) -> Option<&str> { self.strings.get(index.0 as usize).map(Deref::deref) } } #[derive(Debug, Clone, Default)] struct ThreadStringTable { table: StringTable, global_to_local_string: FastHashMap, } impl ThreadStringTable { pub fn new() -> Self { Default::default() } pub fn index_for_string(&mut self, s: &str) -> ThreadInternalStringIndex { ThreadInternalStringIndex(self.table.index_for_string(s)) } pub fn index_for_global_string( &mut self, global_index: GlobalStringIndex, global_table: &GlobalStringTable, ) -> ThreadInternalStringIndex { let table = &mut self.table; *self .global_to_local_string .entry(global_index) .or_insert_with(|| { let s = global_table.get_string(global_index).unwrap(); ThreadInternalStringIndex(table.index_for_string(s)) }) } } impl Serialize for ThreadStringTable { fn serialize(&self, serializer: S) -> Result { self.table.strings.serialize(serializer) } } #[derive(Debug, Clone, Default)] struct GlobalStringTable { table: StringTable, } impl GlobalStringTable { pub fn new() -> Self { Default::default() } pub fn index_for_string(&mut self, s: &str) -> GlobalStringIndex { GlobalStringIndex(self.table.index_for_string(s)) } pub fn get_string(&self, index: GlobalStringIndex) -> Option<&str> { self.table.get_string(index.0) } } #[cfg(test)] mod test { use assert_json_diff::assert_json_eq; use debugid::{CodeId, DebugId}; use serde_json::json; use std::{str::FromStr, time::Duration}; use crate::{ CategoryColor, CpuDelta, Frame, LibraryInfo, MarkerDynamicField, MarkerFieldFormat, MarkerLocation, MarkerSchema, MarkerSchemaField, MarkerStaticField, MarkerTiming, Profile, ProfilerMarker, ReferenceTimestamp, TextMarker, Timestamp, }; #[test] fn it_works() { struct CustomMarker { event_name: String, allocation_size: u32, url: String, latency: Duration, } impl ProfilerMarker for CustomMarker { const MARKER_TYPE_NAME: &'static str = "custom"; fn schema() -> MarkerSchema { MarkerSchema { type_name: Self::MARKER_TYPE_NAME, locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable], chart_label: None, tooltip_label: Some("Custom tooltip label"), table_label: None, fields: vec![ MarkerSchemaField::Dynamic(MarkerDynamicField { key: "eventName", label: "Event name", format: MarkerFieldFormat::String, searchable: None, }), MarkerSchemaField::Dynamic(MarkerDynamicField { key: "allocationSize", label: "Allocation size", format: MarkerFieldFormat::Bytes, searchable: None, }), MarkerSchemaField::Dynamic(MarkerDynamicField { key: "url", label: "URL", format: MarkerFieldFormat::Url, searchable: None, }), MarkerSchemaField::Dynamic(MarkerDynamicField { key: "latency", label: "Latency", format: MarkerFieldFormat::Duration, searchable: None, }), MarkerSchemaField::Static(MarkerStaticField { label: "Description", value: "This is a test marker with a custom schema.", }), ], } } fn json_marker_data(&self) -> serde_json::Value { json!({ "type": Self::MARKER_TYPE_NAME, "eventName": self.event_name, "allocationSize": self.allocation_size, "url": self.url, "latency": self.latency.as_secs_f64() * 1000.0, }) } } let mut profile = Profile::new( "test", ReferenceTimestamp::from_millis_since_unix_epoch(1636162232627.0), Duration::from_millis(1), ); let process = profile.add_process("test", 123, Timestamp::from_millis_since_reference(0.0)); let thread = profile.add_thread( process, 12345, Timestamp::from_millis_since_reference(0.0), true, ); profile.add_sample( thread, Timestamp::from_millis_since_reference(0.0), vec![].into_iter(), CpuDelta::ZERO, 1, ); profile.add_lib( process, LibraryInfo { name: "libc.so.6".to_string(), debug_name: "libc.so.6".to_string(), path: "/usr/lib/x86_64-linux-gnu/libc.so.6".to_string(), code_id: Some( CodeId::from_str("f0fc29165cbe6088c0e1adf03b0048fbecbc003a").unwrap(), ), debug_path: "/usr/lib/x86_64-linux-gnu/libc.so.6".to_string(), debug_id: DebugId::from_breakpad("1629FCF0BE5C8860C0E1ADF03B0048FB0").unwrap(), arch: None, base_avma: 0x00007f76b7e5d000, avma_range: 0x00007f76b7e85000..0x00007f76b8019000, }, ); profile.add_lib( process, LibraryInfo { name: "dump_syms".to_string(), debug_name: "dump_syms".to_string(), path: "/home/mstange/code/dump_syms/target/release/dump_syms".to_string(), code_id: Some( CodeId::from_str("510d0a5c19eadf8043f203b4525be9be3dcb9554").unwrap(), ), debug_path: "/home/mstange/code/dump_syms/target/release/dump_syms".to_string(), debug_id: DebugId::from_breakpad("5C0A0D51EA1980DF43F203B4525BE9BE0").unwrap(), arch: None, base_avma: 0x000055ba9eb4d000, avma_range: 0x000055ba9ebf6000..0x000055ba9f07e000, }, ); let category = profile.add_category("Regular", CategoryColor::Blue); profile.add_sample( thread, Timestamp::from_millis_since_reference(1.0), vec![ 0x7f76b7ffc0e7, 0x55ba9eda3d7f, 0x55ba9ed8bb62, 0x55ba9ec92419, 0x55ba9ec2b778, 0x55ba9ec0f705, 0x7ffdb4824838, ] .into_iter() .enumerate() .rev() .map(|(i, addr)| { if i == 0 { Frame::InstructionPointer(addr) } else { Frame::ReturnAddress(addr) } }) .map(|frame| (frame, category.into())), CpuDelta::ZERO, 1, ); profile.add_sample( thread, Timestamp::from_millis_since_reference(2.0), vec![ 0x55ba9eda018e, 0x55ba9ec3c3cf, 0x55ba9ec2a2d7, 0x55ba9ec53993, 0x7f76b7e8707d, 0x55ba9ec0f705, 0x7ffdb4824838, ] .into_iter() .enumerate() .rev() .map(|(i, addr)| { if i == 0 { Frame::InstructionPointer(addr) } else { Frame::ReturnAddress(addr) } }) .map(|frame| (frame, category.into())), CpuDelta::ZERO, 1, ); profile.add_sample( thread, Timestamp::from_millis_since_reference(3.0), vec![ 0x7f76b7f019c6, 0x55ba9edc48f5, 0x55ba9ec010e3, 0x55ba9eca41b9, 0x7f76b7e8707d, 0x55ba9ec0f705, 0x7ffdb4824838, ] .into_iter() .enumerate() .rev() .map(|(i, addr)| { if i == 0 { Frame::InstructionPointer(addr) } else { Frame::ReturnAddress(addr) } }) .map(|frame| (frame, category.into())), CpuDelta::ZERO, 1, ); profile.add_marker( thread, "Experimental", TextMarker("Hello world!".to_string()), MarkerTiming::Instant(Timestamp::from_millis_since_reference(0.0)), ); profile.add_marker( thread, "CustomName", CustomMarker { event_name: "My event".to_string(), allocation_size: 512000, url: "https://mozilla.org/".to_string(), latency: Duration::from_millis(123), }, MarkerTiming::Interval( Timestamp::from_millis_since_reference(0.0), Timestamp::from_millis_since_reference(2.0), ), ); // eprintln!("{}", serde_json::to_string_pretty(&profile).unwrap()); assert_json_eq!( profile, json!( { "meta": { "categories": [ { "color": "grey", "name": "Other", "subcategories": [ "Other" ] }, { "color": "blue", "name": "Regular", "subcategories": [ "Other" ] } ], "debug": false, "extensions": { "baseURL": [], "id": [], "length": 0, "name": [] }, "interval": 1.0, "markerSchema": [ { "chartLabel": "{marker.data.name}", "data": [ { "format": "string", "key": "name", "label": "Details" } ], "display": [ "marker-chart", "marker-table" ], "name": "Text", "tableLabel": "{marker.name} - {marker.data.name}" }, { "data": [ { "format": "string", "key": "eventName", "label": "Event name" }, { "format": "bytes", "key": "allocationSize", "label": "Allocation size" }, { "format": "url", "key": "url", "label": "URL" }, { "format": "duration", "key": "latency", "label": "Latency" }, { "label": "Description", "value": "This is a test marker with a custom schema." } ], "display": [ "marker-chart", "marker-table" ], "name": "custom", "tooltipLabel": "Custom tooltip label" } ], "pausedRanges": [], "preprocessedProfileVersion": 41, "processType": 0, "product": "test", "sampleUnits": { "eventDelay": "ms", "threadCPUDelta": "µs", "time": "ms" }, "startTime": 1636162232627.0, "symbolicated": false, "version": 24 }, "libs": [ { "name": "dump_syms", "path": "/home/mstange/code/dump_syms/target/release/dump_syms", "debugName": "dump_syms", "debugPath": "/home/mstange/code/dump_syms/target/release/dump_syms", "breakpadId": "5C0A0D51EA1980DF43F203B4525BE9BE0", "codeId": "510d0a5c19eadf8043f203b4525be9be3dcb9554", "arch": null }, { "name": "libc.so.6", "path": "/usr/lib/x86_64-linux-gnu/libc.so.6", "debugName": "libc.so.6", "debugPath": "/usr/lib/x86_64-linux-gnu/libc.so.6", "breakpadId": "1629FCF0BE5C8860C0E1ADF03B0048FB0", "codeId": "f0fc29165cbe6088c0e1adf03b0048fbecbc003a", "arch": null } ], "threads": [ { "frameTable": { "length": 16, "address": [ -1, 796420, 911223, 1332248, 2354017, 2452862, 1700071, 172156, 1075602, 905942, 979918, 2437518, 1405368, 737506, 2586868, 674246 ], "inlineDepth": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], "category": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ], "subcategory": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], "func": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ], "nativeSymbol": [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ], "innerWindowID": [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ], "implementation": [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ], "line": [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ], "column": [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ], "optimizations": [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ] }, "funcTable": { "length": 16, "name": [ 0, 1, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17 ], "isJS": [ false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false ], "relevantForJS": [ false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false ], "resource": [ -1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1 ], "fileName": [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ], "lineNumber": [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ], "columnNumber": [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ] }, "markers": { "length": 2, "category": [ 0, 0 ], "data": [ { "name": "Hello world!", "type": "Text" }, { "allocationSize": 512000, "eventName": "My event", "latency": 123.0, "type": "custom", "url": "https://mozilla.org/" } ], "endTime": [ 0.0, 2.0 ], "name": [ 18, 19 ], "phase": [ 0, 1 ], "startTime": [ 0.0, 0.0 ] }, "name": "GeckoMain", "nativeSymbols": { "address": [], "length": 0, "libIndex": [], "name": [] }, "pausedRanges": [], "pid": 123, "processName": "test", "processShutdownTime": null, "processStartupTime": 0.0, "processType": "default", "registerTime": 0.0, "resourceTable": { "length": 2, "lib": [ 0, 1 ], "name": [ 2, 8 ], "host": [ null, null ], "type": [ 1, 1 ] }, "samples": { "length": 4, "stack": [ null, 6, 11, 15 ], "time": [ 0.0, 1.0, 2.0, 3.0 ], "weight": [ 1, 1, 1, 1 ], "weightType": "samples", "threadCPUDelta": [ 0, 0, 0, 0 ] }, "stackTable": { "length": 16, "prefix": [ null, 0, 1, 2, 3, 4, 5, 1, 7, 8, 9, 10, 7, 12, 13, 14 ], "frame": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ], "category": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], "subcategory": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] }, "stringArray": [ "0x7ffdb4824837", "0xc2704", "dump_syms", "0xde777", "0x145418", "0x23eb61", "0x256d7e", "0x19f0e7", "libc.so.6", "0x2a07c", "0x106992", "0xdd2d6", "0xef3ce", "0x25318e", "0x1571b8", "0xb40e2", "0x2778f4", "0xa49c6", "Experimental", "CustomName" ], "tid": 12345, "unregisterTime": null } ], "pages": [], "profilerOverhead": [], "counters": [] } ) ) } } fxprof-processed-profile-0.4.0/src/markers.rs000064400000000000000000000147260072674642500174170ustar 00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ use serde::Serialize; use serde_json::{json, Value}; use super::timestamp::Timestamp; #[derive(Debug, Clone)] pub enum MarkerTiming { Instant(Timestamp), Interval(Timestamp, Timestamp), IntervalStart(Timestamp), IntervalEnd(Timestamp), } pub trait ProfilerMarker { /// The name of the marker type. const MARKER_TYPE_NAME: &'static str; /// A static method that returns a `MarkerSchema`, which contains all the /// information needed to stream the display schema associated with a /// marker type. fn schema() -> MarkerSchema; /// A method that streams the marker payload data as a serde_json object. fn json_marker_data(&self) -> Value; } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct MarkerSchema { #[serde(rename = "name")] pub type_name: &'static str, /// List of marker display locations. Empty for SpecialFrontendLocation. #[serde(rename = "display")] pub locations: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub chart_label: Option<&'static str>, #[serde(skip_serializing_if = "Option::is_none")] pub tooltip_label: Option<&'static str>, #[serde(skip_serializing_if = "Option::is_none")] pub table_label: Option<&'static str>, #[serde(rename = "data")] pub fields: Vec, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "kebab-case")] pub enum MarkerLocation { MarkerChart, MarkerTable, /// This adds markers to the main marker timeline in the header. TimelineOverview, /// In the timeline, this is a section that breaks out markers that are /// related to memory. When memory counters are enabled, this is its own /// track, otherwise it is displayed with the main thread. TimelineMemory, /// This adds markers to the IPC timeline area in the header. TimelineIPC, /// This adds markers to the FileIO timeline area in the header. #[serde(rename = "timeline-fileio")] TimelineFileIO, /// TODO - This is not supported yet. StackChart, } #[derive(Debug, Clone, Serialize)] #[serde(untagged)] pub enum MarkerSchemaField { /// Static fields have the same value on all markers. This is used for /// a "Description" field in the tooltip, for example. Static(MarkerStaticField), /// Dynamic fields have a per-marker value. The ProfilerMarker implementation /// on the marker type needs to serialize a field on the data JSON object with /// the matching key. Dynamic(MarkerDynamicField), } #[derive(Debug, Clone, Serialize)] pub struct MarkerStaticField { pub label: &'static str, pub value: &'static str, } #[derive(Debug, Clone, Serialize)] pub struct MarkerDynamicField { pub key: &'static str, #[serde(skip_serializing_if = "str::is_empty")] pub label: &'static str, pub format: MarkerFieldFormat, #[serde(skip_serializing_if = "Option::is_none")] pub searchable: Option, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "kebab-case")] pub enum MarkerFieldFormat { // ---------------------------------------------------- // String types. /// A URL, supports PII sanitization Url, /// A file path, supports PII sanitization. FilePath, /// A plain String, never sanitized for PII. /// Important: Do not put URL or file path information here, as it will not /// be sanitized during profile upload. Please be careful with including /// other types of PII here as well. String, // ---------------------------------------------------- // Numeric types /// For time data that represents a duration of time. /// e.g. "Label: 5s, 5ms, 5μs" Duration, /// Data that happened at a specific time, relative to the start of the /// profile. e.g. "Label: 15.5s, 20.5ms, 30.5μs" Time, /// The following are alternatives to display a time only in a specific unit /// of time. Seconds, // "Label: 5s" Milliseconds, // "Label: 5ms" Microseconds, // "Label: 5μs" Nanoseconds, // "Label: 5ns" /// e.g. "Label: 5.55mb, 5 bytes, 312.5kb" Bytes, /// This should be a value between 0 and 1. /// "Label: 50%" Percentage, // The integer should be used for generic representations of numbers. // Do not use it for time information. // "Label: 52, 5,323, 1,234,567" Integer, // The decimal should be used for generic representations of numbers. // Do not use it for time information. // "Label: 52.23, 0.0054, 123,456.78" Decimal, } /// The simplest possible example marker type. It contains no information /// beyond the name string that's passed to ThreadBuilder::add_marker. #[derive(Debug, Clone)] pub struct TracingMarker(); impl ProfilerMarker for TracingMarker { const MARKER_TYPE_NAME: &'static str = "tracing"; fn json_marker_data(&self) -> Value { json!({ "type": Self::MARKER_TYPE_NAME, }) } fn schema() -> MarkerSchema { MarkerSchema { type_name: Self::MARKER_TYPE_NAME, locations: vec![ MarkerLocation::MarkerChart, MarkerLocation::MarkerTable, MarkerLocation::TimelineOverview, ], chart_label: None, tooltip_label: None, table_label: None, fields: vec![], } } } /// An example marker type with some text content. #[derive(Debug, Clone)] pub struct TextMarker(pub String); impl ProfilerMarker for TextMarker { const MARKER_TYPE_NAME: &'static str = "Text"; fn json_marker_data(&self) -> Value { json!({ "type": Self::MARKER_TYPE_NAME, "name": self.0 }) } fn schema() -> MarkerSchema { MarkerSchema { type_name: Self::MARKER_TYPE_NAME, locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable], chart_label: Some("{marker.data.name}"), tooltip_label: None, table_label: Some("{marker.name} - {marker.data.name}"), fields: vec![MarkerSchemaField::Dynamic(MarkerDynamicField { key: "name", label: "Details", format: MarkerFieldFormat::String, searchable: None, })], } } } fxprof-processed-profile-0.4.0/src/timestamp.rs000064400000000000000000000016040072674642500177450ustar 00000000000000use serde::ser::{Serialize, Serializer}; use std::time::Duration; #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct Timestamp { nanos: u64, } impl Timestamp { pub fn from_nanos_since_reference(nanos: u64) -> Self { Self { nanos } } pub fn from_millis_since_reference(millis: f64) -> Self { Self { nanos: (millis * 1_000_000.0) as u64, } } pub fn from_duration_since_reference(duration: Duration) -> Self { Self { nanos: duration.as_nanos() as u64, } } } impl Serialize for Timestamp { fn serialize(&self, serializer: S) -> Result { // In the profile JSON, timestamps are currently expressed as float milliseconds // since profile.meta.startTime. serializer.serialize_f64((self.nanos as f64) / 1_000_000.0) } }