use crate::inspector::Inspector;
use crate::timeline::EventKind;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io;
use std::path::Path;
use std::time::Instant;
#[derive(Debug, Serialize, Deserialize)]
pub struct TraceEvent {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cat: Option<String>,
pub ph: String,
pub ts: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub dur: Option<u64>,
pub pid: u32,
pub tid: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub args: Option<serde_json::Value>,
}
impl TraceEvent {
#[must_use]
pub fn complete(
name: String,
cat: &str,
ts_us: u64,
dur_us: u64,
tid: u64,
args: Option<serde_json::Value>,
) -> Self {
Self {
name,
cat: Some(cat.to_string()),
ph: "X".to_string(),
ts: ts_us,
dur: Some(dur_us),
pid: std::process::id(),
tid,
args,
}
}
#[must_use]
pub fn instant(
name: String,
cat: &str,
ts_us: u64,
tid: u64,
args: Option<serde_json::Value>,
) -> Self {
Self {
name,
cat: Some(cat.to_string()),
ph: "i".to_string(),
ts: ts_us,
dur: None,
pid: std::process::id(),
tid,
args,
}
}
#[must_use]
pub fn thread_name(tid: u64, name: String) -> Self {
Self {
name: "thread_name".to_string(),
cat: None,
ph: "M".to_string(),
ts: 0,
dur: None,
pid: std::process::id(),
tid,
args: Some(serde_json::json!({ "name": name })),
}
}
#[must_use]
pub fn process_name(name: String) -> Self {
Self {
name: "process_name".to_string(),
cat: None,
ph: "M".to_string(),
ts: 0,
dur: None,
pid: std::process::id(),
tid: 0,
args: Some(serde_json::json!({ "name": name })),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TraceDocument {
#[serde(rename = "displayTimeUnit")]
pub display_time_unit: String,
#[serde(rename = "traceEvents")]
pub trace_events: Vec<TraceEvent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
impl Default for TraceDocument {
fn default() -> Self {
Self {
display_time_unit: "ms".to_string(),
trace_events: Vec::new(),
metadata: None,
}
}
}
pub struct ChromeTraceExporter;
impl ChromeTraceExporter {
pub fn export_to_file<P: AsRef<Path>>(inspector: &Inspector, path: P) -> io::Result<()> {
let document = Self::prepare_trace_document(inspector);
let file = File::create(path)?;
serde_json::to_writer_pretty(file, &document)?;
Ok(())
}
pub fn export_to_string(inspector: &Inspector) -> serde_json::Result<String> {
let document = Self::prepare_trace_document(inspector);
serde_json::to_string_pretty(&document)
}
fn prepare_trace_document(inspector: &Inspector) -> TraceDocument {
let mut document = TraceDocument::default();
document
.trace_events
.push(TraceEvent::process_name("async-inspect".to_string()));
let events = inspector.get_events();
let baseline = events.first().map_or_else(Instant::now, |e| e.timestamp);
let mut task_names = std::collections::HashMap::new();
for event in events {
let task_id = event.task_id.as_u64();
let ts_us = event
.timestamp
.saturating_duration_since(baseline)
.as_micros() as u64;
match &event.kind {
EventKind::TaskSpawned {
name,
parent,
location,
} => {
task_names.insert(task_id, name.clone());
document
.trace_events
.push(TraceEvent::thread_name(task_id, name.clone()));
document.trace_events.push(TraceEvent::instant(
format!("spawn: {name}"),
"task",
ts_us,
task_id,
Some(serde_json::json!({
"parent": parent.map(|p| p.as_u64()),
"location": location,
})),
));
}
EventKind::PollStarted => {
}
EventKind::PollEnded { duration } => {
let dur_us = duration.as_micros() as u64;
let start_ts = ts_us.saturating_sub(dur_us);
document.trace_events.push(TraceEvent::complete(
"poll".to_string(),
"runtime",
start_ts,
dur_us,
task_id,
None,
));
}
EventKind::AwaitStarted {
await_point: _,
location: _,
} => {
}
EventKind::AwaitEnded {
await_point,
duration,
} => {
let dur_us = duration.as_micros() as u64;
let start_ts = ts_us.saturating_sub(dur_us);
document.trace_events.push(TraceEvent::complete(
await_point.clone(),
"await",
start_ts,
dur_us,
task_id,
Some(serde_json::json!({
"await_point": await_point,
})),
));
}
EventKind::TaskCompleted { duration } => {
let dur_us = duration.as_micros() as u64;
let start_ts = ts_us.saturating_sub(dur_us);
let task_name = task_names
.get(&task_id)
.cloned()
.unwrap_or_else(|| format!("task_{task_id}"));
document.trace_events.push(TraceEvent::complete(
task_name,
"task",
start_ts,
dur_us,
task_id,
Some(serde_json::json!({
"status": "completed",
})),
));
}
EventKind::TaskFailed { error } => {
document.trace_events.push(TraceEvent::instant(
"task_failed".to_string(),
"task",
ts_us,
task_id,
Some(serde_json::json!({
"error": error,
})),
));
}
EventKind::InspectionPoint { label, message } => {
document.trace_events.push(TraceEvent::instant(
label.clone(),
"inspection",
ts_us,
task_id,
Some(serde_json::json!({
"message": message,
})),
));
}
EventKind::StateChanged {
old_state,
new_state,
} => {
document.trace_events.push(TraceEvent::instant(
"state_change".to_string(),
"task",
ts_us,
task_id,
Some(serde_json::json!({
"old_state": format!("{:?}", old_state),
"new_state": format!("{:?}", new_state),
})),
));
}
}
}
let stats = inspector.stats();
document.metadata = Some(serde_json::json!({
"async-inspect-version": env!("CARGO_PKG_VERSION"),
"total_tasks": stats.total_tasks,
"total_events": stats.total_events,
"duration_ms": stats.timeline_duration.as_secs_f64() * 1000.0,
}));
document
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trace_event_complete() {
let event = TraceEvent::complete("test_task".to_string(), "task", 1000, 500, 42, None);
assert_eq!(event.name, "test_task");
assert_eq!(event.ph, "X");
assert_eq!(event.ts, 1000);
assert_eq!(event.dur, Some(500));
assert_eq!(event.tid, 42);
}
#[test]
fn test_trace_event_instant() {
let event = TraceEvent::instant(
"spawn".to_string(),
"task",
2000,
1,
Some(serde_json::json!({"parent": 0})),
);
assert_eq!(event.name, "spawn");
assert_eq!(event.ph, "i");
assert_eq!(event.ts, 2000);
assert_eq!(event.dur, None);
}
#[test]
fn test_trace_event_metadata() {
let event = TraceEvent::thread_name(42, "worker-1".to_string());
assert_eq!(event.name, "thread_name");
assert_eq!(event.ph, "M");
assert_eq!(event.tid, 42);
}
}