async_inspect/export/
chrome_trace.rs

1//! Chrome Trace Event Format exporter
2//!
3//! Exports async-inspect data to Chrome Trace Event Format for viewing in <chrome://tracing>
4//! or compatible tools like Perfetto UI.
5//!
6//! Format specification: <https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU>/
7
8use crate::inspector::Inspector;
9use crate::timeline::EventKind;
10use serde::{Deserialize, Serialize};
11use std::fs::File;
12use std::io;
13use std::path::Path;
14use std::time::Instant;
15
16/// Chrome Trace Event
17#[derive(Debug, Serialize, Deserialize)]
18pub struct TraceEvent {
19    /// Event name
20    pub name: String,
21
22    /// Event category (comma-separated)
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub cat: Option<String>,
25
26    /// Event type: B (begin), E (end), X (complete), i (instant), M (metadata)
27    pub ph: String,
28
29    /// Timestamp in microseconds
30    pub ts: u64,
31
32    /// Duration in microseconds (for 'X' events)
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub dur: Option<u64>,
35
36    /// Process ID
37    pub pid: u32,
38
39    /// Thread ID
40    pub tid: u64,
41
42    /// Additional arguments
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub args: Option<serde_json::Value>,
45}
46
47impl TraceEvent {
48    /// Create a complete event (X) with duration
49    #[must_use]
50    pub fn complete(
51        name: String,
52        cat: &str,
53        ts_us: u64,
54        dur_us: u64,
55        tid: u64,
56        args: Option<serde_json::Value>,
57    ) -> Self {
58        Self {
59            name,
60            cat: Some(cat.to_string()),
61            ph: "X".to_string(),
62            ts: ts_us,
63            dur: Some(dur_us),
64            pid: std::process::id(),
65            tid,
66            args,
67        }
68    }
69
70    /// Create an instant event (i)
71    #[must_use]
72    pub fn instant(
73        name: String,
74        cat: &str,
75        ts_us: u64,
76        tid: u64,
77        args: Option<serde_json::Value>,
78    ) -> Self {
79        Self {
80            name,
81            cat: Some(cat.to_string()),
82            ph: "i".to_string(),
83            ts: ts_us,
84            dur: None,
85            pid: std::process::id(),
86            tid,
87            args,
88        }
89    }
90
91    /// Create a metadata event (M) for thread name
92    #[must_use]
93    pub fn thread_name(tid: u64, name: String) -> Self {
94        Self {
95            name: "thread_name".to_string(),
96            cat: None,
97            ph: "M".to_string(),
98            ts: 0,
99            dur: None,
100            pid: std::process::id(),
101            tid,
102            args: Some(serde_json::json!({ "name": name })),
103        }
104    }
105
106    /// Create a metadata event (M) for process name
107    #[must_use]
108    pub fn process_name(name: String) -> Self {
109        Self {
110            name: "process_name".to_string(),
111            cat: None,
112            ph: "M".to_string(),
113            ts: 0,
114            dur: None,
115            pid: std::process::id(),
116            tid: 0,
117            args: Some(serde_json::json!({ "name": name })),
118        }
119    }
120}
121
122/// Chrome Trace Event Format document
123#[derive(Debug, Serialize, Deserialize)]
124pub struct TraceDocument {
125    /// Display time unit (default: "ms")
126    #[serde(rename = "displayTimeUnit")]
127    pub display_time_unit: String,
128
129    /// Array of trace events
130    #[serde(rename = "traceEvents")]
131    pub trace_events: Vec<TraceEvent>,
132
133    /// Metadata about the trace
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub metadata: Option<serde_json::Value>,
136}
137
138impl Default for TraceDocument {
139    fn default() -> Self {
140        Self {
141            display_time_unit: "ms".to_string(),
142            trace_events: Vec::new(),
143            metadata: None,
144        }
145    }
146}
147
148/// Chrome Trace Event Format exporter
149pub struct ChromeTraceExporter;
150
151impl ChromeTraceExporter {
152    /// Export to Chrome Trace Event Format JSON file
153    pub fn export_to_file<P: AsRef<Path>>(inspector: &Inspector, path: P) -> io::Result<()> {
154        let document = Self::prepare_trace_document(inspector);
155        let file = File::create(path)?;
156        serde_json::to_writer_pretty(file, &document)?;
157        Ok(())
158    }
159
160    /// Export to Chrome Trace Event Format JSON string
161    pub fn export_to_string(inspector: &Inspector) -> serde_json::Result<String> {
162        let document = Self::prepare_trace_document(inspector);
163        serde_json::to_string_pretty(&document)
164    }
165
166    fn prepare_trace_document(inspector: &Inspector) -> TraceDocument {
167        let mut document = TraceDocument::default();
168
169        // Add process name metadata
170        document
171            .trace_events
172            .push(TraceEvent::process_name("async-inspect".to_string()));
173
174        // Get timeline baseline (earliest event timestamp)
175        let events = inspector.get_events();
176        let baseline = events.first().map_or_else(Instant::now, |e| e.timestamp);
177
178        // Track task names for thread metadata
179        let mut task_names = std::collections::HashMap::new();
180
181        // Convert events to trace events
182        for event in events {
183            let task_id = event.task_id.as_u64();
184            let ts_us = event
185                .timestamp
186                .saturating_duration_since(baseline)
187                .as_micros() as u64;
188
189            match &event.kind {
190                EventKind::TaskSpawned {
191                    name,
192                    parent,
193                    location,
194                } => {
195                    // Store task name for metadata
196                    task_names.insert(task_id, name.clone());
197
198                    // Add thread name metadata
199                    document
200                        .trace_events
201                        .push(TraceEvent::thread_name(task_id, name.clone()));
202
203                    // Add instant event for task spawn
204                    document.trace_events.push(TraceEvent::instant(
205                        format!("spawn: {name}"),
206                        "task",
207                        ts_us,
208                        task_id,
209                        Some(serde_json::json!({
210                            "parent": parent.map(|p| p.as_u64()),
211                            "location": location,
212                        })),
213                    ));
214                }
215
216                EventKind::PollStarted => {
217                    // We'll use PollEnded to create complete events
218                }
219
220                EventKind::PollEnded { duration } => {
221                    let dur_us = duration.as_micros() as u64;
222                    let start_ts = ts_us.saturating_sub(dur_us);
223
224                    document.trace_events.push(TraceEvent::complete(
225                        "poll".to_string(),
226                        "runtime",
227                        start_ts,
228                        dur_us,
229                        task_id,
230                        None,
231                    ));
232                }
233
234                EventKind::AwaitStarted {
235                    await_point: _,
236                    location: _,
237                } => {
238                    // We'll use AwaitEnded to create complete events
239                    // Store await point name for later
240                }
241
242                EventKind::AwaitEnded {
243                    await_point,
244                    duration,
245                } => {
246                    let dur_us = duration.as_micros() as u64;
247                    let start_ts = ts_us.saturating_sub(dur_us);
248
249                    document.trace_events.push(TraceEvent::complete(
250                        await_point.clone(),
251                        "await",
252                        start_ts,
253                        dur_us,
254                        task_id,
255                        Some(serde_json::json!({
256                            "await_point": await_point,
257                        })),
258                    ));
259                }
260
261                EventKind::TaskCompleted { duration } => {
262                    let dur_us = duration.as_micros() as u64;
263                    let start_ts = ts_us.saturating_sub(dur_us);
264
265                    let task_name = task_names
266                        .get(&task_id)
267                        .cloned()
268                        .unwrap_or_else(|| format!("task_{task_id}"));
269
270                    document.trace_events.push(TraceEvent::complete(
271                        task_name,
272                        "task",
273                        start_ts,
274                        dur_us,
275                        task_id,
276                        Some(serde_json::json!({
277                            "status": "completed",
278                        })),
279                    ));
280                }
281
282                EventKind::TaskFailed { error } => {
283                    document.trace_events.push(TraceEvent::instant(
284                        "task_failed".to_string(),
285                        "task",
286                        ts_us,
287                        task_id,
288                        Some(serde_json::json!({
289                            "error": error,
290                        })),
291                    ));
292                }
293
294                EventKind::InspectionPoint { label, message } => {
295                    document.trace_events.push(TraceEvent::instant(
296                        label.clone(),
297                        "inspection",
298                        ts_us,
299                        task_id,
300                        Some(serde_json::json!({
301                            "message": message,
302                        })),
303                    ));
304                }
305
306                EventKind::StateChanged {
307                    old_state,
308                    new_state,
309                } => {
310                    document.trace_events.push(TraceEvent::instant(
311                        "state_change".to_string(),
312                        "task",
313                        ts_us,
314                        task_id,
315                        Some(serde_json::json!({
316                            "old_state": format!("{:?}", old_state),
317                            "new_state": format!("{:?}", new_state),
318                        })),
319                    ));
320                }
321            }
322        }
323
324        // Add metadata
325        let stats = inspector.stats();
326        document.metadata = Some(serde_json::json!({
327            "async-inspect-version": env!("CARGO_PKG_VERSION"),
328            "total_tasks": stats.total_tasks,
329            "total_events": stats.total_events,
330            "duration_ms": stats.timeline_duration.as_secs_f64() * 1000.0,
331        }));
332
333        document
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn test_trace_event_complete() {
343        let event = TraceEvent::complete("test_task".to_string(), "task", 1000, 500, 42, None);
344
345        assert_eq!(event.name, "test_task");
346        assert_eq!(event.ph, "X");
347        assert_eq!(event.ts, 1000);
348        assert_eq!(event.dur, Some(500));
349        assert_eq!(event.tid, 42);
350    }
351
352    #[test]
353    fn test_trace_event_instant() {
354        let event = TraceEvent::instant(
355            "spawn".to_string(),
356            "task",
357            2000,
358            1,
359            Some(serde_json::json!({"parent": 0})),
360        );
361
362        assert_eq!(event.name, "spawn");
363        assert_eq!(event.ph, "i");
364        assert_eq!(event.ts, 2000);
365        assert_eq!(event.dur, None);
366    }
367
368    #[test]
369    fn test_trace_event_metadata() {
370        let event = TraceEvent::thread_name(42, "worker-1".to_string());
371
372        assert_eq!(event.name, "thread_name");
373        assert_eq!(event.ph, "M");
374        assert_eq!(event.tid, 42);
375    }
376}