Skip to main content

tsz_cli/
trace.rs

1//! Performance tracing support for the `--generateTrace` flag.
2//!
3//! Generates Chrome `DevTools` compatible trace files that can be loaded in
4//! <chrome://tracing> or the Perfetto UI (<https://ui.perfetto.dev>/).
5//!
6//! # Trace Format
7//!
8//! The trace file is a JSON array of trace events following the Chrome Trace
9//! Event Format specification.
10//!
11//! # Usage
12//!
13//! ```ignore
14//! use tsz::cli::trace::Tracer;
15//!
16//! let mut tracer = Tracer::new();
17//! tracer.begin("Parse", "file.ts");
18//! // ... do parsing ...
19//! tracer.end("Parse", "file.ts");
20//! tracer.write_to_file("trace/trace.json")?;
21//! ```
22
23use rustc_hash::FxHashMap;
24use serde::Serialize;
25use std::io::Write;
26use std::path::Path;
27use std::time::{Duration, Instant};
28
29/// Trace event phases (Chrome Trace Event Format)
30#[derive(Debug, Clone, Copy, Serialize)]
31pub enum Phase {
32    /// Duration event begin
33    #[serde(rename = "B")]
34    Begin,
35    /// Duration event end
36    #[serde(rename = "E")]
37    End,
38    /// Complete event (duration with explicit duration)
39    #[serde(rename = "X")]
40    Complete,
41    /// Instant event
42    #[serde(rename = "i")]
43    Instant,
44    /// Metadata event
45    #[serde(rename = "M")]
46    Metadata,
47}
48
49/// A single trace event
50#[derive(Debug, Clone, Serialize)]
51pub struct TraceEvent {
52    /// Event name
53    pub name: String,
54    /// Event category
55    pub cat: String,
56    /// Phase (B=begin, E=end, X=complete, i=instant, M=metadata)
57    pub ph: Phase,
58    /// Timestamp in microseconds
59    pub ts: u64,
60    /// Duration in microseconds (for complete events)
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub dur: Option<u64>,
63    /// Process ID
64    pub pid: u32,
65    /// Thread ID
66    pub tid: u32,
67    /// Additional arguments
68    #[serde(skip_serializing_if = "FxHashMap::is_empty")]
69    pub args: FxHashMap<String, serde_json::Value>,
70}
71
72/// Categories for trace events
73pub mod categories {
74    pub const PROGRAM: &str = "program";
75    pub const PARSE: &str = "parse";
76    pub const BIND: &str = "bind";
77    pub const CHECK: &str = "check";
78    pub const EMIT: &str = "emit";
79    pub const IO: &str = "io";
80    pub const MODULE_RESOLUTION: &str = "moduleResolution";
81}
82
83/// Performance tracer that collects timing information
84#[derive(Debug)]
85pub struct Tracer {
86    events: Vec<TraceEvent>,
87    start_time: Instant,
88    active_spans: FxHashMap<String, Instant>,
89    pid: u32,
90    tid: u32,
91}
92
93impl Tracer {
94    /// Create a new tracer
95    pub fn new() -> Self {
96        Self {
97            events: Vec::new(),
98            start_time: Instant::now(),
99            active_spans: FxHashMap::default(),
100            pid: std::process::id(),
101            tid: 1, // Main thread
102        }
103    }
104
105    /// Get timestamp in microseconds since tracer start
106    fn timestamp(&self) -> u64 {
107        self.start_time.elapsed().as_micros() as u64
108    }
109
110    /// Begin a duration event
111    pub fn begin(&mut self, name: &str, category: &str) {
112        let ts = self.timestamp();
113        let key = format!("{category}:{name}");
114        self.active_spans.insert(key, Instant::now());
115
116        self.events.push(TraceEvent {
117            name: name.to_string(),
118            cat: category.to_string(),
119            ph: Phase::Begin,
120            ts,
121            dur: None,
122            pid: self.pid,
123            tid: self.tid,
124            args: FxHashMap::default(),
125        });
126    }
127
128    /// Begin a duration event with arguments
129    pub fn begin_with_args(
130        &mut self,
131        name: &str,
132        category: &str,
133        args: FxHashMap<String, serde_json::Value>,
134    ) {
135        let ts = self.timestamp();
136        let key = format!("{category}:{name}");
137        self.active_spans.insert(key, Instant::now());
138
139        self.events.push(TraceEvent {
140            name: name.to_string(),
141            cat: category.to_string(),
142            ph: Phase::Begin,
143            ts,
144            dur: None,
145            pid: self.pid,
146            tid: self.tid,
147            args,
148        });
149    }
150
151    /// End a duration event
152    pub fn end(&mut self, name: &str, category: &str) {
153        let ts = self.timestamp();
154        let key = format!("{category}:{name}");
155        self.active_spans.remove(&key);
156
157        self.events.push(TraceEvent {
158            name: name.to_string(),
159            cat: category.to_string(),
160            ph: Phase::End,
161            ts,
162            dur: None,
163            pid: self.pid,
164            tid: self.tid,
165            args: FxHashMap::default(),
166        });
167    }
168
169    /// Record a complete event with known duration
170    pub fn complete(&mut self, name: &str, category: &str, start: Instant, duration: Duration) {
171        let ts = (start.duration_since(self.start_time)).as_micros() as u64;
172        let dur = duration.as_micros() as u64;
173
174        self.events.push(TraceEvent {
175            name: name.to_string(),
176            cat: category.to_string(),
177            ph: Phase::Complete,
178            ts,
179            dur: Some(dur),
180            pid: self.pid,
181            tid: self.tid,
182            args: FxHashMap::default(),
183        });
184    }
185
186    /// Record a complete event with arguments
187    pub fn complete_with_args(
188        &mut self,
189        name: &str,
190        category: &str,
191        start: Instant,
192        duration: Duration,
193        args: FxHashMap<String, serde_json::Value>,
194    ) {
195        let ts = (start.duration_since(self.start_time)).as_micros() as u64;
196        let dur = duration.as_micros() as u64;
197
198        self.events.push(TraceEvent {
199            name: name.to_string(),
200            cat: category.to_string(),
201            ph: Phase::Complete,
202            ts,
203            dur: Some(dur),
204            pid: self.pid,
205            tid: self.tid,
206            args,
207        });
208    }
209
210    /// Record an instant event
211    pub fn instant(&mut self, name: &str, category: &str) {
212        let ts = self.timestamp();
213
214        self.events.push(TraceEvent {
215            name: name.to_string(),
216            cat: category.to_string(),
217            ph: Phase::Instant,
218            ts,
219            dur: None,
220            pid: self.pid,
221            tid: self.tid,
222            args: FxHashMap::default(),
223        });
224    }
225
226    /// Record an instant event with arguments
227    pub fn instant_with_args(
228        &mut self,
229        name: &str,
230        category: &str,
231        args: FxHashMap<String, serde_json::Value>,
232    ) {
233        let ts = self.timestamp();
234
235        self.events.push(TraceEvent {
236            name: name.to_string(),
237            cat: category.to_string(),
238            ph: Phase::Instant,
239            ts,
240            dur: None,
241            pid: self.pid,
242            tid: self.tid,
243            args,
244        });
245    }
246
247    /// Add metadata event (e.g., process/thread names)
248    pub fn metadata(&mut self, name: &str, args: FxHashMap<String, serde_json::Value>) {
249        self.events.push(TraceEvent {
250            name: name.to_string(),
251            cat: "__metadata".to_string(),
252            ph: Phase::Metadata,
253            ts: 0,
254            dur: None,
255            pid: self.pid,
256            tid: self.tid,
257            args,
258        });
259    }
260
261    /// Write the trace to a file
262    pub fn write_to_file(&self, path: &Path) -> std::io::Result<()> {
263        // Ensure parent directory exists
264        if let Some(parent) = path.parent() {
265            std::fs::create_dir_all(parent)?;
266        }
267
268        let file = std::fs::File::create(path)?;
269        let mut writer = std::io::BufWriter::new(file);
270
271        // Write as JSON array (Chrome Trace Event Format)
272        serde_json::to_writer_pretty(&mut writer, &self.events)?;
273        writer.flush()?;
274
275        Ok(())
276    }
277
278    /// Get all recorded events
279    pub fn events(&self) -> &[TraceEvent] {
280        &self.events
281    }
282
283    /// Clear all events
284    pub fn clear(&mut self) {
285        self.events.clear();
286        self.active_spans.clear();
287    }
288}
289
290impl Default for Tracer {
291    fn default() -> Self {
292        Self::new()
293    }
294}
295
296/// RAII guard for tracing a span
297pub struct TraceSpan<'a> {
298    tracer: &'a mut Tracer,
299    name: String,
300    category: String,
301}
302
303impl<'a> TraceSpan<'a> {
304    /// Create a new trace span
305    pub fn new(tracer: &'a mut Tracer, name: &str, category: &str) -> Self {
306        tracer.begin(name, category);
307        TraceSpan {
308            tracer,
309            name: name.to_string(),
310            category: category.to_string(),
311        }
312    }
313}
314
315impl Drop for TraceSpan<'_> {
316    fn drop(&mut self) {
317        self.tracer.end(&self.name, &self.category);
318    }
319}
320
321/// Macro to trace a scope
322#[macro_export]
323macro_rules! trace_span {
324    ($tracer:expr, $name:expr, $category:expr) => {
325        let _span = $crate::trace::TraceSpan::new($tracer, $name, $category);
326    };
327}
328
329#[cfg(test)]
330#[path = "trace_tests.rs"]
331mod tests;