1pub 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#[derive(Debug, Serialize, Deserialize)]
22pub struct ExportTask {
23 pub id: u64,
25 pub name: String,
27 pub state: String,
29 pub created_at_ms: u128,
31 pub duration_ms: f64,
33 pub poll_count: u64,
35 pub run_time_ms: f64,
37 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#[derive(Debug, Serialize, Deserialize)]
58pub struct ExportEvent {
59 pub event_id: u64,
61 pub task_id: u64,
63 pub timestamp_ms: u128,
65 pub kind: String,
67 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, task_id: event.task_id.as_u64(),
131 timestamp_ms: event.timestamp.elapsed().as_millis(),
132 kind,
133 details,
134 }
135 }
136}
137
138#[derive(Debug, Serialize, Deserialize)]
140pub struct ExportData {
141 pub tasks: Vec<ExportTask>,
143 pub events: Vec<ExportEvent>,
145 pub metadata: ExportMetadata,
147}
148
149#[derive(Debug, Serialize, Deserialize)]
151pub struct ExportMetadata {
152 pub version: String,
154 pub timestamp: String,
156 pub total_tasks: usize,
158 pub total_events: usize,
160 pub duration_ms: f64,
162}
163
164pub struct JsonExporter;
166
167impl JsonExporter {
168 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 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
211pub struct CsvExporter;
213
214impl CsvExporter {
215 pub fn export_tasks_to_file<P: AsRef<Path>>(inspector: &Inspector, path: P) -> io::Result<()> {
217 let mut file = File::create(path)?;
218
219 writeln!(
221 file,
222 "id,name,state,created_at_ms,duration_ms,poll_count,run_time_ms,parent_id"
223 )?;
224
225 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 pub fn export_events_to_file<P: AsRef<Path>>(inspector: &Inspector, path: P) -> io::Result<()> {
249 let mut file = File::create(path)?;
250
251 writeln!(file, "event_id,task_id,timestamp_ms,kind,details")?;
253
254 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}