use crate::function_trace::*;
use ahash::{HashMap, HashSet, RandomState};
use bytesize::ByteSize;
use color_eyre::eyre::{Result, WrapErr};
use indexmap::set::IndexSet;
use owo_colors::OwoColorize;
use serde::Serialize;
use serde_repr::Serialize_repr;
use std::borrow::Cow;
use std::fs::File;
use std::io::BufWriter;
use std::num::NonZeroU32;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
use std::time::{Duration, SystemTime};
type HighResFirefoxTime = f64;
type FirefoxTime = f32;
#[derive(Serialize, Debug)]
enum Unused {}
type StringIndex = usize;
type ThreadId = u32;
pub struct FirefoxProfileThreadId {
pub tid: ThreadId,
start_duration: Duration,
}
#[derive(Serialize, Debug)]
pub struct FirefoxProfile {
meta: Metadata,
counters: Vec<Unused>,
threads: Vec<FirefoxThread>,
libs: Vec<Unused>,
#[serde(skip_serializing)]
thread_count: u32,
}
#[derive(Hash, Debug, PartialEq, Eq)]
enum FunctionCall<'a> {
Native {
function_name: Cow<'a, str>,
module: Cow<'a, str>,
},
Python {
function_name: Cow<'a, str>,
module: Cow<'a, str>,
line: NonZeroU32,
},
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "lowercase")]
#[allow(dead_code)]
enum Color {
Transparent,
Purple,
Green,
Orange,
Yellow,
LightBlue,
Grey,
Blue,
Brown,
}
#[derive(Serialize, Debug)]
struct Category {
name: &'static str,
color: Color,
subcategories: [&'static str; 1],
}
#[derive(Serialize_repr, Debug, Clone, Copy)]
#[repr(u32)]
enum CategoryIndex {
Logs,
Python,
Native,
Exceptions,
Imports,
GarbageCollection,
Length,
}
const CATEGORIES: [Category; CategoryIndex::Length as usize] = [
Category {
name: "Logs",
color: Color::Grey,
subcategories: ["Unused"],
},
Category {
name: "Python",
color: Color::Orange,
subcategories: ["Code"],
},
Category {
name: "Native",
color: Color::LightBlue,
subcategories: ["Code"],
},
Category {
name: "Exceptions",
color: Color::Brown,
subcategories: ["Unused"],
},
Category {
name: "Imports",
color: Color::Grey,
subcategories: ["Unused"],
},
Category {
name: "GarbageCollection",
color: Color::Purple,
subcategories: ["Unused"],
},
];
macro_rules! struct_table {
(struct $struct_name: ident {
$($element: ident: $ty: ty),+
} $($additional_field:ident: $additional_type: ty),*) => {
paste::item! {
#[derive(Serialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
struct [<$struct_name Index>] (u32);
#[derive(Serialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
struct [<$struct_name Table>] {
$($element: Vec<$ty>,)*
$($additional_field: $additional_type,)*
length: usize
}
struct $struct_name {
$($element: $ty),*,
}
impl [<$struct_name Table>] {
#[allow(dead_code)]
fn new($([<$additional_field _arg>]: $additional_type,)*) -> Self {
Self {
$($element: Vec::new()),*,
$($additional_field: [<$additional_field _arg>],)*
length: 0
}
}
#[allow(dead_code)]
fn add(&mut self, arg: $struct_name) -> [<$struct_name Index>] {
let length = self.length;
$(self.$element.push(arg.$element);)*
self.length += 1;
[<$struct_name Index>](length as u32)
}
#[allow(dead_code)]
fn is_empty(&self) -> bool {
self.length == 0
}
$(
#[allow(dead_code)]
fn [<get_ $element>](&self, index: [<$struct_name Index>]) -> &$ty {
&self.$element[index.0 as usize]
})*
}
}
}
}
struct_table! {
struct Sample {
stack: StackIndex,
time: FirefoxTime,
weight: Option<FirefoxTime>
}
weight_type: &'static str
}
struct_table! {
struct Stack {
frame: FrameIndex,
category: CategoryIndex,
subcategory: u8,
prefix: Option<StackIndex>
}
}
struct_table! {
struct Frame {
func: FunctionIndex,
inlineDepth: u8,
category: CategoryIndex,
subcategory: u8,
address: Option<Unused>,
nativeSymbol: Option<Unused>,
innerWindowID: Option<Unused>,
implementation: Option<Unused>,
line: Option<Unused>,
column: Option<Unused>
}
}
struct_table! {
struct Function {
isJS: bool,
name: StringIndex,
resource: i32,
relevantForJS: bool,
file_name: StringIndex, line_number: Option<NonZeroU32>,
column_number: Option<Unused>
}
}
struct_table! {
struct NativeAllocation {
time: FirefoxTime,
weight: isize, stack: Option<StackIndex>,
memory_address: usize,
thread_id: ThreadId
}
weight_type: &'static str
}
struct_table! {
struct NativeSymbol {
libIndex: Option<Unused>,
address: Option<Unused>,
name: StringIndex,
functionSize: Option<u32>
}
}
struct_table! {
struct Marker {
data: serde_json::Value,
name: StringIndex,
startTime: FirefoxTime,
endTime: Option<FirefoxTime>,
phase: u64, category: CategoryIndex
}
}
struct_table! {
struct Resource {
lib: Unused,
name: Unused,
host: Unused,
r#type: Unused
}
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct MarkerSchema {
name: &'static str,
tooltip_label: &'static str,
table_label: &'static str,
chart_label: &'static str,
display: &'static [&'static str],
data: &'static [MarkerDataSchema],
}
#[derive(Serialize, Debug)]
struct MarkerDataSchema {
key: &'static str,
label: &'static str,
format: &'static str,
searchable: bool,
}
const MARKER_DISPLAYS_ALL: &[&str] = &[
"marker-chart",
"marker-table",
"timeline-overview",
"stack-chart",
];
const MARKER_SCHEMAS_SUPPORTED: &[MarkerSchema] = &[
MarkerSchema {
name: "Log",
tooltip_label: "Log event from {marker.data.origin}",
chart_label: "{marker.data.origin}",
table_label: "{marker.data.origin}: {marker.data.value}",
display: MARKER_DISPLAYS_ALL,
data: &[
MarkerDataSchema {
key: "origin",
label: "Log Origin",
format: "string",
searchable: true,
},
MarkerDataSchema {
key: "value",
label: "Value",
format: "string",
searchable: true,
},
],
},
MarkerSchema {
name: "Import",
tooltip_label: "Imported {marker.data.module}",
chart_label: "{marker.data.module}",
table_label: "imported {marker.data.module}",
display: MARKER_DISPLAYS_ALL,
data: &[MarkerDataSchema {
key: "module",
label: "Imported Module",
format: "string",
searchable: true,
}],
},
MarkerSchema {
name: "Exception",
tooltip_label: "Exception {marker.data.exception} in {marker.data.module}",
chart_label: "{marker.data.exception}",
table_label: "Exception {marker.data.exception}: {marker.data.value}",
display: MARKER_DISPLAYS_ALL,
data: &[
MarkerDataSchema {
key: "exception",
label: "Exception Type",
format: "string",
searchable: true,
},
MarkerDataSchema {
key: "value",
label: "Exception Data",
format: "string",
searchable: true,
},
MarkerDataSchema {
key: "module",
label: "Module",
format: "string",
searchable: true,
},
],
},
];
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Metadata {
interval: f32,
start_time: f64,
process_type: u32,
categories: &'static [Category],
stackwalk: u32,
debug: bool,
version: u32,
preprocessed_profile_version: u32,
symbolicated: bool,
product: String,
#[serde(rename = "appBuildID")]
app_build_id: String,
abi: String,
platform: String,
misc: String,
#[serde(rename = "physicalCPUs")]
physical_cpus: usize,
#[serde(rename = "logicalCPUs")]
logical_cpus: usize,
marker_schema: &'static [MarkerSchema],
#[serde(skip_serializing)]
start_duration: Duration,
}
#[derive(Serialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct FirefoxThread {
process_type: &'static str,
process_startup_time: HighResFirefoxTime, process_shutdown_time: HighResFirefoxTime,
register_time: HighResFirefoxTime,
unregister_time: Option<Unused>, paused_ranges: Vec<Unused>,
name: &'static str,
is_main_thread: bool,
process_name: String,
is_js_tracer: bool,
pid: String,
tid: ThreadId,
samples: SampleTable,
markers: MarkerTable,
stack_table: StackTable,
frame_table: FrameTable,
func_table: FunctionTable,
string_array: IndexSet<String, RandomState>,
#[serde(skip_serializing_if = "NativeAllocationTable::is_empty")]
native_allocations: NativeAllocationTable,
resource_table: ResourceTable,
native_symbols: NativeSymbolTable,
#[serde(skip_serializing)]
calls: Vec<SampleIndex>,
#[serde(skip_serializing)]
functions: HashMap<FunctionCall<'static>, FunctionIndex>,
#[serde(skip_serializing)]
start_duration: Duration,
#[serde(skip_serializing)]
allocations: HashMap<usize, usize>,
#[serde(skip_serializing)]
existing_stacks: HashMap<(FunctionIndex, Option<StackIndex>), StackIndex>,
}
impl FirefoxProfile {
#[must_use]
pub fn new(info: TraceInitialization) -> FirefoxProfile {
FirefoxProfile {
meta: Metadata {
start_time: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0.0, |n| n.as_secs_f64() * 1000.0),
start_duration: info.time,
interval: 0.000_001, process_type: 0,
categories: &CATEGORIES,
stackwalk: 0,
version: 27,
preprocessed_profile_version: 47,
debug: false,
symbolicated: true,
physical_cpus: num_cpus::get_physical(),
logical_cpus: num_cpus::get(),
product: info.program_name,
app_build_id: info.program_version,
abi: info.lang_version,
platform: info.platform,
misc: "<git revision>".to_string(),
marker_schema: MARKER_SCHEMAS_SUPPORTED,
},
counters: Vec::new(),
threads: Vec::new(),
libs: Vec::new(),
thread_count: 0,
}
}
pub fn register_thread(&mut self) -> FirefoxProfileThreadId {
self.thread_count += 1;
FirefoxProfileThreadId {
tid: self.thread_count,
start_duration: self.meta.start_duration,
}
}
pub fn finalize_thread(&mut self, thread: FirefoxThread) {
self.threads.push(thread);
}
pub fn export(&mut self, output_dir: PathBuf) -> Result<()> {
self.threads.sort_by(|x, y| {
x.register_time
.partial_cmp(&y.register_time)
.expect("Registration time is reasonable")
});
let end_time = self
.threads
.iter()
.map(|x| x.process_shutdown_time)
.fold(0.0, HighResFirefoxTime::max);
let mut registered_pids = HashSet::default();
for thread in &mut self.threads {
thread.name = if registered_pids.contains(&thread.pid) {
"thread"
} else {
thread.is_main_thread = true;
"GeckoMain"
};
registered_pids.insert(thread.pid.clone());
if thread.functions.is_empty() {
let message = "[WARNING] FunctionTrace didn't log any data for this thread. This may be because the thread crashed or otherwise exited before flushing its trace buffer.";
thread.emit_call(message, thread.register_time, "FunctionTrace", None);
thread.emit_return(message, end_time);
thread.emit_call(message, end_time, "FunctionTrace", None);
thread.emit_return(message, end_time);
thread.process_shutdown_time = end_time;
}
}
let compressed = std::env::var("FUNCTIONTRACE_COMPRESSION")
.map(|x| !matches!(x.as_str(), "false" | "no" | "disable"))
.unwrap_or(true);
let output_path = {
let now = chrono::Local::now();
output_dir.join(format!(
"functiontrace.{}.json{}",
now.format("%F-%T.%3f"),
if compressed { ".gz" } else { "" }
))
};
{
use spinoff::spinners::{Arrow3, SpinnerFrames};
let bytes = std::sync::atomic::AtomicUsize::new(0);
let mut spinner = spinoff::Spinner::new(
Arrow3,
"Exporting FunctionTrace profile...",
spinoff::Color::Blue,
);
let gzipped_bytes = std::thread::scope(|s| {
s.spawn(|| {
let spin_interval = {
let spinner_frame = SpinnerFrames::from(Arrow3);
Duration::from_millis(
spinner_frame.frames.len() as u64 * spinner_frame.interval as u64,
)
};
loop {
std::thread::sleep(spin_interval);
let written = bytes.load(Ordering::Relaxed);
if written == usize::MAX {
break;
}
spinner.update_text(format!(
"Exporting FunctionTrace profile... {}",
ByteSize(written as u64).display().iec_short()
));
}
});
let progress_writer = BufWriter::new(progress_streams::ProgressWriter::new(
File::create(&output_path)?,
|progress: usize| {
bytes.fetch_add(progress, Ordering::Relaxed);
},
));
if compressed {
let gzip = BufWriter::new(flate2::write::GzEncoder::new(
progress_writer,
flate2::Compression::new(4),
));
serde_json::to_writer(gzip, &self)?;
} else {
serde_json::to_writer(progress_writer, &self)?;
}
Ok::<_, std::io::Error>(bytes.swap(usize::MAX, Ordering::SeqCst))
})?;
spinner.success(&format!(
"Exported FunctionTrace profile to {} ({})",
output_path.display().bold().italic(),
ByteSize(gzipped_bytes as u64).display().iec_short()
));
};
let symlink = output_dir.join(format!(
"functiontrace.latest.json{}",
if compressed { ".gz" } else { "" }
));
let _ = std::fs::remove_file(&symlink);
std::os::unix::fs::symlink(&output_path, &symlink)
.wrap_err("Symlinking functiontrace.latest.json.gz failed")?;
Ok(())
}
}
impl FirefoxThread {
#[must_use]
pub fn new(registration: ThreadRegistration, thread: &FirefoxProfileThreadId) -> FirefoxThread {
let startup_time = (registration.time - thread.start_duration).as_secs_f64() * 1000.0;
FirefoxThread {
process_type: "default",
process_startup_time: startup_time,
process_shutdown_time: 0.0, register_time: startup_time,
start_duration: thread.start_duration,
unregister_time: None,
name: "<filled in at export>",
process_name: registration.program_name,
is_js_tracer: true,
pid: registration.pid.to_string(),
tid: thread.tid,
samples: SampleTable::new("tracing-ms"),
native_allocations: NativeAllocationTable::new("bytes"),
..Default::default()
}
}
fn offset_time(&self, time: Duration) -> HighResFirefoxTime {
(time - self.start_duration).as_secs_f64() * 1000.0
}
fn unique_string(&mut self, string: &str) -> StringIndex {
match self.string_array.get_full(string) {
Some((index, _)) => index,
None => self.string_array.insert_full(string.to_string()).0,
}
}
fn emit_call(
&mut self,
name: &str,
time: HighResFirefoxTime,
file_or_module: &str,
line_number: Option<NonZeroU32>,
) -> HighResFirefoxTime {
let (func_uid, category) = match line_number {
None => (
FunctionCall::Native {
function_name: std::borrow::Cow::Borrowed(name),
module: std::borrow::Cow::Borrowed(file_or_module),
},
CategoryIndex::Native,
),
Some(line) => (
FunctionCall::Python {
function_name: std::borrow::Cow::Borrowed(name),
module: std::borrow::Cow::Borrowed(file_or_module),
line,
},
CategoryIndex::Python,
),
};
let function_id = match self.functions.get(&func_uid) {
Some(&x) => x,
None => {
let funcname_id = self.unique_string(name);
let filename_id = self.unique_string(file_or_module);
let function_id = self.func_table.add(Function {
isJS: false,
name: funcname_id,
resource: -1,
relevantForJS: false,
file_name: filename_id,
line_number,
column_number: None,
});
self.frame_table.add(Frame {
func: function_id,
category,
subcategory: 0,
address: None,
inlineDepth: 0,
nativeSymbol: None,
innerWindowID: None,
implementation: None,
line: None,
column: None,
});
let func_uid = match func_uid {
FunctionCall::Native { .. } => FunctionCall::Native {
function_name: std::borrow::Cow::Owned(name.to_string()),
module: std::borrow::Cow::Owned(file_or_module.to_string()),
},
FunctionCall::Python { line, .. } => FunctionCall::Python {
function_name: std::borrow::Cow::Owned(name.to_string()),
module: std::borrow::Cow::Owned(file_or_module.to_string()),
line,
},
};
self.functions.insert(func_uid, function_id);
function_id
}
};
let frame_id = FrameIndex(function_id.0);
let prefix = self.calls.last().map(|&x| *self.samples.get_stack(x));
let stack_id = *self
.existing_stacks
.entry((function_id, prefix))
.or_insert_with(|| {
self.stack_table.add(Stack {
frame: frame_id,
subcategory: 0,
category,
prefix,
})
});
let samples_id = self.samples.add(Sample {
stack: stack_id,
time: time as FirefoxTime,
weight: None, });
self.calls.push(samples_id);
time
}
fn emit_return(&mut self, func_name: &str, time: HighResFirefoxTime) -> HighResFirefoxTime {
while let Some(call_id) = self.calls.pop() {
let caller_id = *self.func_table.get_name(
*self
.frame_table
.get_func(*self.stack_table.get_frame(*self.samples.get_stack(call_id))),
);
let matches = self
.string_array
.get_full(func_name)
.is_some_and(|(id, _)| id == caller_id);
self.samples.weight[call_id.0 as usize] =
Some((time as FirefoxTime) - self.samples.get_time(call_id));
if matches {
break;
} else if let Some(returned_name) = self.string_array.get_index(caller_id) {
log::warn!(
"|{}| Returning from `{returned_name}` rather than `{func_name}` as expected",
self.tid
);
} else {
log::error!(
"|{}| Returning from from unknown function rather than `{func_name}` as expected",
self.tid
);
}
}
time
}
pub fn add_trace(&mut self, trace: FunctionTrace) {
let event_time = match trace {
FunctionTrace::Call {
time,
func_name,
filename,
linenumber,
} => self.emit_call(
&func_name,
self.offset_time(time),
&filename,
NonZeroU32::new(linenumber),
),
FunctionTrace::NativeCall {
time,
func_name,
module_name,
} => self.emit_call(&func_name, self.offset_time(time), &module_name, None),
FunctionTrace::Return { time, func_name }
| FunctionTrace::NativeReturn { time, func_name } => {
self.emit_return(&func_name, self.offset_time(time))
}
FunctionTrace::Exception {
time,
exception_type,
exception_value,
filename,
linenumber,
} => {
let unique_type = self.unique_string(&exception_type);
let time = self.offset_time(time);
self.markers.add(Marker {
data: serde_json::json!({
"type": "Exception",
"exeption": exception_type,
"value": exception_value,
"module": format!("{}:{}", filename, linenumber)
}),
name: unique_type,
startTime: time as FirefoxTime,
endTime: None,
phase: 0,
category: CategoryIndex::Exceptions,
});
time
}
FunctionTrace::Log {
time,
log_type,
log_value,
} => {
let time = self.offset_time(time);
let unique_type = self.unique_string(&log_type);
self.markers.add(Marker {
data: serde_json::json!({
"type": "Log",
"origin": log_type,
"value": log_value,
}),
name: unique_type,
startTime: time as FirefoxTime,
endTime: None,
phase: 0,
category: CategoryIndex::Logs,
});
time
}
FunctionTrace::Import { time, module_name } => {
let time = self.offset_time(time);
let unique_type = self.unique_string("Import");
self.markers.add(Marker {
data: serde_json::json!({
"type": "Import",
"module": module_name,
}),
name: unique_type,
startTime: time as FirefoxTime,
endTime: None,
phase: 0,
category: CategoryIndex::Imports,
});
time
}
FunctionTrace::Allocation { time, details } => {
let time = self.offset_time(time);
let stack = self.calls.last().map(|&x| *self.samples.get_stack(x));
match details {
AllocationDetails::Alloc { bytes, addr } => {
self.allocations.insert(addr, bytes);
self.native_allocations.add(NativeAllocation {
time: time as FirefoxTime,
weight: bytes as isize,
stack,
memory_address: addr,
thread_id: self.tid,
});
}
AllocationDetails::Realloc {
bytes,
old_addr,
new_addr,
} => {
let old_bytes = self.allocations.remove(&old_addr).unwrap_or(0);
self.native_allocations.add(NativeAllocation {
time: time as FirefoxTime,
weight: -(old_bytes as isize),
stack,
memory_address: old_addr,
thread_id: self.tid,
});
self.allocations.insert(new_addr, bytes);
self.native_allocations.add(NativeAllocation {
time: time as FirefoxTime,
weight: bytes as isize,
stack,
memory_address: new_addr,
thread_id: self.tid,
});
}
AllocationDetails::Free { old_addr } => {
let bytes = self.allocations.remove(&old_addr).unwrap_or(0);
self.native_allocations.add(NativeAllocation {
time: time as FirefoxTime,
weight: -(bytes as isize),
stack,
memory_address: old_addr,
thread_id: self.tid,
});
}
}
time
}
FunctionTrace::RegisterThread(_) => unreachable!(),
};
self.process_shutdown_time = self.process_shutdown_time.max(event_time);
}
}