async_inspect/
export.rs

1//! Export functionality for various formats
2//!
3//! This module provides exporters for task data in industry-standard formats
4//! like JSON, CSV, Chrome Trace Event Format, flamegraphs, and others.
5
6pub mod chrome_trace;
7pub mod flamegraph;
8
9pub use chrome_trace::ChromeTraceExporter;
10pub use flamegraph::{FlamegraphBuilder, FlamegraphExporter};
11
12use crate::inspector::Inspector;
13use crate::task::TaskInfo;
14use crate::timeline::{Event, EventKind};
15use serde::{Deserialize, Serialize};
16use std::fs::File;
17use std::io::{self, Write};
18use std::path::Path;
19
20/// Serializable task data
21#[derive(Debug, Serialize, Deserialize)]
22pub struct ExportTask {
23    /// Unique task identifier
24    pub id: u64,
25    /// Task name (function name)
26    pub name: String,
27    /// Current task state (Running, Blocked, etc.)
28    pub state: String,
29    /// Task creation timestamp in milliseconds
30    pub created_at_ms: u128,
31    /// Total duration in milliseconds
32    pub duration_ms: f64,
33    /// Number of times the task was polled
34    pub poll_count: u64,
35    /// Total time spent running (not waiting) in milliseconds
36    pub run_time_ms: f64,
37    /// Parent task ID if this is a spawned task
38    pub parent_id: Option<u64>,
39}
40
41impl From<&TaskInfo> for ExportTask {
42    fn from(task: &TaskInfo) -> Self {
43        Self {
44            id: task.id.as_u64(),
45            name: task.name.clone(),
46            state: format!("{:?}", task.state),
47            created_at_ms: task.created_at.elapsed().as_millis(),
48            duration_ms: task.age().as_secs_f64() * 1000.0,
49            poll_count: task.poll_count,
50            run_time_ms: task.total_run_time.as_secs_f64() * 1000.0,
51            parent_id: task.parent.map(|id| id.as_u64()),
52        }
53    }
54}
55
56/// Serializable event data
57#[derive(Debug, Serialize, Deserialize)]
58pub struct ExportEvent {
59    /// Unique event identifier
60    pub event_id: u64,
61    /// Associated task identifier
62    pub task_id: u64,
63    /// Event timestamp in milliseconds
64    pub timestamp_ms: u128,
65    /// Event kind (`TaskSpawned`, Poll, Wake, etc.)
66    pub kind: String,
67    /// Additional event details
68    pub details: Option<String>,
69}
70
71impl From<&Event> for ExportEvent {
72    fn from(event: &Event) -> Self {
73        let (kind, details) = match &event.kind {
74            EventKind::TaskSpawned {
75                name,
76                parent,
77                location,
78            } => (
79                "TaskSpawned".to_string(),
80                Some(format!(
81                    "name={name}, parent={parent:?}, location={location:?}"
82                )),
83            ),
84            EventKind::PollStarted => ("PollStarted".to_string(), None),
85            EventKind::PollEnded { duration } => (
86                "PollEnded".to_string(),
87                Some(format!("duration={}ms", duration.as_secs_f64() * 1000.0)),
88            ),
89            EventKind::AwaitStarted {
90                await_point,
91                location,
92            } => (
93                "AwaitStarted".to_string(),
94                Some(format!("point={await_point}, location={location:?}")),
95            ),
96            EventKind::AwaitEnded {
97                await_point,
98                duration,
99            } => (
100                "AwaitEnded".to_string(),
101                Some(format!(
102                    "point={}, duration={}ms",
103                    await_point,
104                    duration.as_secs_f64() * 1000.0
105                )),
106            ),
107            EventKind::TaskCompleted { duration } => (
108                "TaskCompleted".to_string(),
109                Some(format!("duration={}ms", duration.as_secs_f64() * 1000.0)),
110            ),
111            EventKind::TaskFailed { error } => (
112                "TaskFailed".to_string(),
113                error.as_ref().map(|e| format!("error={e}")),
114            ),
115            EventKind::InspectionPoint { label, message } => (
116                "InspectionPoint".to_string(),
117                Some(format!("label={label}, message={message:?}")),
118            ),
119            EventKind::StateChanged {
120                old_state,
121                new_state,
122            } => (
123                "StateChanged".to_string(),
124                Some(format!("old={old_state:?}, new={new_state:?}")),
125            ),
126        };
127
128        Self {
129            event_id: 0, // Event IDs are internal, use 0 for export
130            task_id: event.task_id.as_u64(),
131            timestamp_ms: event.timestamp.elapsed().as_millis(),
132            kind,
133            details,
134        }
135    }
136}
137
138/// Complete export data
139#[derive(Debug, Serialize, Deserialize)]
140pub struct ExportData {
141    /// List of all tasks
142    pub tasks: Vec<ExportTask>,
143    /// List of all events
144    pub events: Vec<ExportEvent>,
145    /// Export metadata
146    pub metadata: ExportMetadata,
147}
148
149/// Export metadata
150#[derive(Debug, Serialize, Deserialize)]
151pub struct ExportMetadata {
152    /// async-inspect version
153    pub version: String,
154    /// Export timestamp
155    pub timestamp: String,
156    /// Total number of tasks
157    pub total_tasks: usize,
158    /// Total number of events
159    pub total_events: usize,
160    /// Total duration captured in milliseconds
161    pub duration_ms: f64,
162}
163
164/// JSON exporter
165pub struct JsonExporter;
166
167impl JsonExporter {
168    /// Export to JSON string
169    pub fn export_to_string(inspector: &Inspector) -> serde_json::Result<String> {
170        let data = Self::prepare_export_data(inspector);
171        serde_json::to_string_pretty(&data)
172    }
173
174    /// Export to JSON file
175    pub fn export_to_file<P: AsRef<Path>>(inspector: &Inspector, path: P) -> io::Result<()> {
176        let data = Self::prepare_export_data(inspector);
177        let file = File::create(path)?;
178        serde_json::to_writer_pretty(file, &data)?;
179        Ok(())
180    }
181
182    fn prepare_export_data(inspector: &Inspector) -> ExportData {
183        let tasks: Vec<ExportTask> = inspector
184            .get_all_tasks()
185            .iter()
186            .map(ExportTask::from)
187            .collect();
188
189        let events: Vec<ExportEvent> = inspector
190            .get_events()
191            .iter()
192            .map(ExportEvent::from)
193            .collect();
194
195        let stats = inspector.stats();
196
197        ExportData {
198            tasks,
199            events,
200            metadata: ExportMetadata {
201                version: env!("CARGO_PKG_VERSION").to_string(),
202                timestamp: chrono::Utc::now().to_rfc3339(),
203                total_tasks: stats.total_tasks,
204                total_events: stats.total_events,
205                duration_ms: stats.timeline_duration.as_secs_f64() * 1000.0,
206            },
207        }
208    }
209}
210
211/// CSV exporter
212pub struct CsvExporter;
213
214impl CsvExporter {
215    /// Export tasks to CSV file
216    pub fn export_tasks_to_file<P: AsRef<Path>>(inspector: &Inspector, path: P) -> io::Result<()> {
217        let mut file = File::create(path)?;
218
219        // Write header
220        writeln!(
221            file,
222            "id,name,state,created_at_ms,duration_ms,poll_count,run_time_ms,parent_id"
223        )?;
224
225        // Write tasks
226        for task in inspector.get_all_tasks() {
227            let export_task = ExportTask::from(&task);
228            writeln!(
229                file,
230                "{},{},{},{},{},{},{},{}",
231                export_task.id,
232                Self::escape_csv(&export_task.name),
233                export_task.state,
234                export_task.created_at_ms,
235                export_task.duration_ms,
236                export_task.poll_count,
237                export_task.run_time_ms,
238                export_task
239                    .parent_id
240                    .map_or(String::new(), |id| id.to_string())
241            )?;
242        }
243
244        Ok(())
245    }
246
247    /// Export events to CSV file
248    pub fn export_events_to_file<P: AsRef<Path>>(inspector: &Inspector, path: P) -> io::Result<()> {
249        let mut file = File::create(path)?;
250
251        // Write header
252        writeln!(file, "event_id,task_id,timestamp_ms,kind,details")?;
253
254        // Write events
255        for event in inspector.get_events() {
256            let export_event = ExportEvent::from(&event);
257            writeln!(
258                file,
259                "{},{},{},{},{}",
260                export_event.event_id,
261                export_event.task_id,
262                export_event.timestamp_ms,
263                export_event.kind,
264                export_event.details.as_deref().unwrap_or("")
265            )?;
266        }
267
268        Ok(())
269    }
270
271    fn escape_csv(s: &str) -> String {
272        if s.contains(',') || s.contains('"') || s.contains('\n') {
273            format!("\"{}\"", s.replace('"', "\"\""))
274        } else {
275            s.to_string()
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_csv_escape() {
286        assert_eq!(CsvExporter::escape_csv("simple"), "simple");
287        assert_eq!(CsvExporter::escape_csv("with,comma"), "\"with,comma\"");
288        assert_eq!(CsvExporter::escape_csv("with\"quote"), "\"with\"\"quote\"");
289    }
290}