use std::fs::File;
use std::io::Write;
use std::sync::{Mutex, OnceLock};
use crate::Tick;
struct PerfettoState {
file: Mutex<File>,
t0: Tick,
needs_comma: Mutex<bool>,
}
static STATE: OnceLock<Option<PerfettoState>> = OnceLock::new();
fn state() -> Option<&'static PerfettoState> {
STATE
.get_or_init(|| {
let path = crate::env::var("RLX_TRACE_PERFETTO")?;
let mut file = File::create(&path).ok()?;
file.write_all(b"[\n").ok()?;
Some(PerfettoState {
file: Mutex::new(file),
t0: Tick::now(),
needs_comma: Mutex::new(false),
})
})
.as_ref()
}
pub fn enabled() -> bool {
state().is_some()
}
pub fn emit_complete(name: &str, cat: &str, start_ns: u64, end_ns: u64) {
let Some(s) = state() else { return };
let dur_us = (end_ns.saturating_sub(start_ns)) as f64 / 1000.0;
let ts_us = start_ns as f64 / 1000.0;
let mut comma = s.needs_comma.lock().unwrap();
let prefix = if *comma { ",\n" } else { "" };
let line = format!(
"{prefix}{{\"name\":\"{name}\",\"cat\":\"{cat}\",\"ph\":\"X\",\
\"ts\":{ts_us},\"dur\":{dur_us},\"pid\":1,\"tid\":1}}",
);
let _ = s.file.lock().unwrap().write_all(line.as_bytes());
*comma = true;
}
pub fn flush_and_finalize() {
let Some(s) = state() else { return };
let mut f = s.file.lock().unwrap();
let _ = f.write_all(b"\n]\n");
let _ = f.flush();
}
pub struct TraceSpan {
name: &'static str,
cat: &'static str,
start_ns: u64,
}
impl TraceSpan {
pub fn new(name: &'static str, cat: &'static str) -> Option<Self> {
let s = state()?;
let start_ns = Tick::now().elapsed_ns(s.t0);
Some(Self {
name,
cat,
start_ns,
})
}
}
impl Drop for TraceSpan {
fn drop(&mut self) {
let Some(s) = state() else { return };
let end_ns = Tick::now().elapsed_ns(s.t0);
emit_complete(self.name, self.cat, self.start_ns, end_ns);
}
}
#[macro_export]
macro_rules! trace_span {
($name:expr, $cat:expr) => {
$crate::perfetto::TraceSpan::new($name, $cat)
};
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
#[test]
fn disabled_is_noop() {
let was_enabled = enabled();
emit_complete("test_op", "test", 100, 200);
let _span = TraceSpan::new("test_op", "test");
let _macro_span = trace_span!("test_op", "test");
assert_eq!(enabled(), was_enabled, "enabled state must be stable");
}
#[test]
fn complete_event_is_valid_json() {
let line = format!(
"{{\"name\":\"{name}\",\"cat\":\"{cat}\",\"ph\":\"X\",\
\"ts\":{ts},\"dur\":{dur},\"pid\":1,\"tid\":1}}",
name = "Matmul",
cat = "cpu",
ts = 0.5,
dur = 1.25,
);
assert!(line.starts_with("{") && line.ends_with("}"));
assert!(line.contains("\"name\":\"Matmul\""));
assert!(line.contains("\"cat\":\"cpu\""));
assert!(line.contains("\"ph\":\"X\""));
assert!(line.contains("\"ts\":0.5"));
assert!(line.contains("\"dur\":1.25"));
}
#[test]
fn end_to_end_temp_file_check() {
use std::env;
use std::fs;
let dir = env::temp_dir();
let path = dir.join(format!("rlx-trace-{}.json", std::process::id()));
if path.exists() {
let _ = fs::remove_file(&path);
}
let mut f = File::create(&path).unwrap();
f.write_all(b"[\n").unwrap();
f.write_all(
b"{\"name\":\"matmul\",\"cat\":\"cpu\",\"ph\":\"X\",\
\"ts\":0,\"dur\":1.5,\"pid\":1,\"tid\":1}",
)
.unwrap();
f.write_all(b",\n").unwrap();
f.write_all(
b"{\"name\":\"layernorm\",\"cat\":\"cpu\",\"ph\":\"X\",\
\"ts\":2,\"dur\":0.8,\"pid\":1,\"tid\":1}",
)
.unwrap();
f.write_all(b"\n]\n").unwrap();
f.flush().unwrap();
drop(f);
let mut got = String::new();
File::open(&path).unwrap().read_to_string(&mut got).unwrap();
assert!(got.starts_with("[\n"));
assert!(got.contains("\"matmul\""));
assert!(got.contains("\"layernorm\""));
assert!(got.trim_end().ends_with("]"));
let _ = fs::remove_file(&path);
}
}