Skip to main content

sapient_telemetry/
profiler.rs

1//! Chrome trace format profiler.
2//!
3//! Generates a `trace.json` compatible with `chrome://tracing` for visual
4//! flame-graph profiling of execution.
5
6use std::fs;
7use std::path::Path;
8use std::sync::Mutex;
9use std::time::{Duration, Instant};
10
11use serde::Serialize;
12
13// ── Span ─────────────────────────────────────────────────────────────────────
14
15#[derive(Debug, Clone)]
16pub struct Span {
17    pub name: String,
18    pub category: String,
19    pub start_us: u64,
20    pub dur_us: u64,
21    pub pid: u32,
22    pub tid: u64,
23}
24
25impl Span {
26    pub fn new(
27        name: impl Into<String>,
28        category: impl Into<String>,
29        start: Instant,
30        dur: Duration,
31    ) -> Self {
32        Self {
33            name: name.into(),
34            category: category.into(),
35            start_us: start.elapsed().as_micros() as u64,
36            dur_us: dur.as_micros() as u64,
37            pid: std::process::id(),
38            tid: thread_id(),
39        }
40    }
41}
42
43fn thread_id() -> u64 {
44    // Stable numeric thread ID via hash of OS thread ID string.
45    use std::collections::hash_map::DefaultHasher;
46    use std::hash::{Hash, Hasher};
47    let mut h = DefaultHasher::new();
48    format!("{:?}", std::thread::current().id()).hash(&mut h);
49    h.finish()
50}
51
52// ── ChromeTracer ──────────────────────────────────────────────────────────────
53
54/// Accumulates spans and writes them in Chrome trace format.
55#[derive(Debug, Default)]
56pub struct ChromeTracer {
57    spans: Mutex<Vec<Span>>,
58}
59
60impl ChromeTracer {
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    pub fn record(&self, span: Span) {
66        self.spans.lock().unwrap().push(span);
67    }
68
69    pub fn record_now(&self, name: impl Into<String>, category: impl Into<String>, dur: Duration) {
70        self.record(Span {
71            name: name.into(),
72            category: category.into(),
73            start_us: 0, // simplified: real implementation uses a monotonic base
74            dur_us: dur.as_micros() as u64,
75            pid: std::process::id(),
76            tid: thread_id(),
77        });
78    }
79
80    /// Write the collected spans to a `trace.json` file.
81    pub fn save(&self, path: &Path) -> std::io::Result<()> {
82        #[derive(Serialize)]
83        struct TraceEvent {
84            name: String,
85            cat: String,
86            ph: char,
87            ts: u64,
88            dur: u64,
89            pid: u32,
90            tid: u64,
91        }
92
93        let spans = self.spans.lock().unwrap();
94        let events: Vec<TraceEvent> = spans
95            .iter()
96            .map(|s| TraceEvent {
97                name: s.name.clone(),
98                cat: s.category.clone(),
99                ph: 'X', // complete events
100                ts: s.start_us,
101                dur: s.dur_us,
102                pid: s.pid,
103                tid: s.tid,
104            })
105            .collect();
106
107        #[derive(Serialize)]
108        struct TraceFile {
109            #[serde(rename = "traceEvents")]
110            trace_events: Vec<TraceEvent>,
111        }
112        let json = serde_json::to_string_pretty(&TraceFile {
113            trace_events: events,
114        })
115        .map_err(std::io::Error::other)?;
116
117        fs::write(path, json)
118    }
119}