use std::collections::HashMap;
use std::io::Write;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::ProfilerReport;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PerfettoPhase {
Begin,
End,
Complete,
Instant,
Counter,
}
impl PerfettoPhase {
pub fn as_code(&self) -> &'static str {
match self {
Self::Begin => "B",
Self::End => "E",
Self::Complete => "X",
Self::Instant => "i",
Self::Counter => "C",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerfettoEvent {
pub name: String,
pub phase: PerfettoPhase,
pub timestamp_us: u64,
pub duration_us: Option<u64>,
pub pid: u32,
pub tid: u32,
pub args: HashMap<String, Value>,
}
#[derive(Debug, Default)]
pub struct PerfettoTrace {
events: Vec<PerfettoEvent>,
}
impl PerfettoTrace {
pub fn new() -> Self {
Self::default()
}
pub fn add_event(&mut self, event: PerfettoEvent) {
self.events.push(event);
}
pub fn len(&self) -> usize {
self.events.len()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
pub fn export_to_string(&self) -> Result<String> {
let doc = self.build_doc();
Ok(serde_json::to_string_pretty(&doc)?)
}
pub fn export_to_file(&self, path: &std::path::Path) -> Result<()> {
let json = self.export_to_string()?;
let mut file = std::fs::File::create(path)?;
file.write_all(json.as_bytes())?;
tracing::debug!("Perfetto trace written to {}", path.display());
Ok(())
}
fn build_doc(&self) -> Value {
let events: Vec<Value> = self.events.iter().map(event_to_value).collect();
serde_json::json!({
"traceEvents": events,
"displayTimeUnit": "ms",
})
}
}
pub struct PerfettoExporter;
impl PerfettoExporter {
pub fn export_profiler_report(
report: &ProfilerReport,
path: &std::path::Path,
) -> Result<()> {
let mut trace = PerfettoTrace::new();
let mut cursor_us: u64 = 0;
for (layer_name, duration) in &report.slowest_layers {
let dur_us = duration.as_micros() as u64;
let mut args = HashMap::new();
args.insert("layer_name".to_string(), Value::String(layer_name.clone()));
trace.add_event(PerfettoEvent {
name: layer_name.clone(),
phase: PerfettoPhase::Complete,
timestamp_us: cursor_us,
duration_us: Some(dur_us),
pid: 1,
tid: 1,
args,
});
cursor_us += dur_us;
}
for bottleneck in &report.bottlenecks {
let mut args = HashMap::new();
args.insert(
"description".to_string(),
Value::String(bottleneck.description.clone()),
);
args.insert(
"suggestion".to_string(),
Value::String(bottleneck.suggestion.clone()),
);
trace.add_event(PerfettoEvent {
name: format!("bottleneck:{}", bottleneck.location),
phase: PerfettoPhase::Instant,
timestamp_us: cursor_us,
duration_us: None,
pid: 1,
tid: 1,
args,
});
}
trace.export_to_file(path)
}
}
fn event_to_value(e: &PerfettoEvent) -> Value {
let mut obj = serde_json::json!({
"name": e.name,
"ph": e.phase.as_code(),
"ts": e.timestamp_us,
"pid": e.pid,
"tid": e.tid,
});
if let Some(dur) = e.duration_us {
obj["dur"] = Value::Number(dur.into());
}
if !e.args.is_empty() {
obj["args"] = Value::Object(
e.args
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
);
}
obj
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn make_event(name: &str, phase: PerfettoPhase, ts: u64, dur: Option<u64>) -> PerfettoEvent {
PerfettoEvent {
name: name.to_string(),
phase,
timestamp_us: ts,
duration_us: dur,
pid: 1,
tid: 1,
args: HashMap::new(),
}
}
#[test]
fn test_empty_trace_roundtrip() {
let trace = PerfettoTrace::new();
let json = trace.export_to_string().unwrap();
let parsed: Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["traceEvents"].as_array().unwrap().len(), 0);
assert_eq!(parsed["displayTimeUnit"], "ms");
}
#[test]
fn test_add_complete_event() {
let mut trace = PerfettoTrace::new();
trace.add_event(make_event(
"forward",
PerfettoPhase::Complete,
1000,
Some(500),
));
assert_eq!(trace.len(), 1);
let json = trace.export_to_string().unwrap();
let parsed: Value = serde_json::from_str(&json).unwrap();
let ev = &parsed["traceEvents"][0];
assert_eq!(ev["ph"], "X");
assert_eq!(ev["ts"], 1000_u64);
assert_eq!(ev["dur"], 500_u64);
assert_eq!(ev["name"], "forward");
}
#[test]
fn test_begin_end_phases() {
let mut trace = PerfettoTrace::new();
trace.add_event(make_event("op", PerfettoPhase::Begin, 0, None));
trace.add_event(make_event("op", PerfettoPhase::End, 200, None));
let json = trace.export_to_string().unwrap();
let parsed: Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["traceEvents"][0]["ph"], "B");
assert_eq!(parsed["traceEvents"][1]["ph"], "E");
}
#[test]
fn test_export_to_file() {
let mut dir = std::env::temp_dir();
dir.push("perfetto_test_trace.json");
let mut trace = PerfettoTrace::new();
let mut args = HashMap::new();
args.insert("batch_size".to_string(), serde_json::json!(32));
trace.add_event(PerfettoEvent {
name: "attention_forward".to_string(),
phase: PerfettoPhase::Complete,
timestamp_us: 0,
duration_us: Some(1500),
pid: 1,
tid: 2,
args,
});
trace.export_to_file(&dir).unwrap();
assert!(dir.exists());
let content = std::fs::read_to_string(&dir).unwrap();
let parsed: Value = serde_json::from_str(&content).unwrap();
assert_eq!(parsed["traceEvents"].as_array().unwrap().len(), 1);
std::fs::remove_file(&dir).ok();
}
#[test]
fn test_exporter_from_profiler_report() {
use crate::profiler::{MemoryEfficiencyAnalysis, PerformanceBottleneck};
let mut dir = std::env::temp_dir();
dir.push("perfetto_profiler_report.json");
let report = ProfilerReport {
total_events: 2,
total_runtime: Duration::from_millis(100),
statistics: HashMap::new(),
bottlenecks: vec![PerformanceBottleneck {
bottleneck_type: crate::profiler::BottleneckType::CpuBound,
location: "attention".to_string(),
severity: crate::profiler::BottleneckSeverity::Medium,
description: "CPU saturated".to_string(),
suggestion: "Use flash attention".to_string(),
metrics: HashMap::new(),
}],
slowest_layers: vec![
("attention".to_string(), Duration::from_millis(10)),
("ffn".to_string(), Duration::from_millis(15)),
],
memory_efficiency: MemoryEfficiencyAnalysis::default(),
recommendations: vec![],
};
PerfettoExporter::export_profiler_report(&report, &dir).unwrap();
assert!(dir.exists());
let content = std::fs::read_to_string(&dir).unwrap();
let parsed: Value = serde_json::from_str(&content).unwrap();
let events = parsed["traceEvents"].as_array().unwrap();
assert_eq!(events.len(), 3);
assert_eq!(events[2]["ph"], "i");
std::fs::remove_file(&dir).ok();
}
#[test]
fn test_instant_and_counter_phases() {
let mut trace = PerfettoTrace::new();
trace.add_event(make_event("checkpoint", PerfettoPhase::Instant, 500, None));
trace.add_event(make_event("loss", PerfettoPhase::Counter, 600, None));
let json = trace.export_to_string().unwrap();
let parsed: Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["traceEvents"][0]["ph"], "i");
assert_eq!(parsed["traceEvents"][1]["ph"], "C");
}
}