1use 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#[derive(Debug, Serialize, Deserialize)]
18pub struct TraceEvent {
19 pub name: String,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub cat: Option<String>,
25
26 pub ph: String,
28
29 pub ts: u64,
31
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub dur: Option<u64>,
35
36 pub pid: u32,
38
39 pub tid: u64,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub args: Option<serde_json::Value>,
45}
46
47impl TraceEvent {
48 #[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 #[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 #[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 #[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#[derive(Debug, Serialize, Deserialize)]
124pub struct TraceDocument {
125 #[serde(rename = "displayTimeUnit")]
127 pub display_time_unit: String,
128
129 #[serde(rename = "traceEvents")]
131 pub trace_events: Vec<TraceEvent>,
132
133 #[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
148pub struct ChromeTraceExporter;
150
151impl ChromeTraceExporter {
152 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 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 document
171 .trace_events
172 .push(TraceEvent::process_name("async-inspect".to_string()));
173
174 let events = inspector.get_events();
176 let baseline = events.first().map_or_else(Instant::now, |e| e.timestamp);
177
178 let mut task_names = std::collections::HashMap::new();
180
181 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 task_names.insert(task_id, name.clone());
197
198 document
200 .trace_events
201 .push(TraceEvent::thread_name(task_id, name.clone()));
202
203 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 }
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 }
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 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}