trustformers_debug/export/
perfetto.rs1use std::collections::HashMap;
29use std::io::Write;
30
31use anyhow::Result;
32use serde::{Deserialize, Serialize};
33use serde_json::Value;
34
35use crate::ProfilerReport;
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45pub enum PerfettoPhase {
46 Begin,
48 End,
50 Complete,
52 Instant,
54 Counter,
56}
57
58impl PerfettoPhase {
59 pub fn as_code(&self) -> &'static str {
61 match self {
62 Self::Begin => "B",
63 Self::End => "E",
64 Self::Complete => "X",
65 Self::Instant => "i",
66 Self::Counter => "C",
67 }
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct PerfettoEvent {
92 pub name: String,
94 pub phase: PerfettoPhase,
96 pub timestamp_us: u64,
98 pub duration_us: Option<u64>,
100 pub pid: u32,
102 pub tid: u32,
104 pub args: HashMap<String, Value>,
106}
107
108#[derive(Debug, Default)]
120pub struct PerfettoTrace {
121 events: Vec<PerfettoEvent>,
122}
123
124impl PerfettoTrace {
125 pub fn new() -> Self {
127 Self::default()
128 }
129
130 pub fn add_event(&mut self, event: PerfettoEvent) {
132 self.events.push(event);
133 }
134
135 pub fn events(&self) -> &[PerfettoEvent] {
137 &self.events
138 }
139
140 pub fn len(&self) -> usize {
142 self.events.len()
143 }
144
145 pub fn is_empty(&self) -> bool {
147 self.events.is_empty()
148 }
149
150 pub fn export_to_string(&self) -> Result<String> {
166 let doc = self.build_doc();
167 Ok(serde_json::to_string_pretty(&doc)?)
168 }
169
170 pub fn export_to_file(&self, path: &std::path::Path) -> Result<()> {
186 let json = self.export_to_string()?;
187 let mut file = std::fs::File::create(path)?;
188 file.write_all(json.as_bytes())?;
189 tracing::debug!("Perfetto trace written to {}", path.display());
190 Ok(())
191 }
192
193 fn build_doc(&self) -> Value {
196 let events: Vec<Value> = self.events.iter().map(event_to_value).collect();
197 serde_json::json!({
198 "traceEvents": events,
199 "displayTimeUnit": "ms",
200 })
201 }
202}
203
204pub struct PerfettoExporter;
234
235impl PerfettoExporter {
236 pub fn export_profiler_report(report: &ProfilerReport, path: &std::path::Path) -> Result<()> {
245 let mut trace = PerfettoTrace::new();
246 let mut cursor_us: u64 = 0;
247
248 for (layer_name, duration) in &report.slowest_layers {
249 let dur_us = duration.as_micros() as u64;
250 let mut args = HashMap::new();
251 args.insert("layer_name".to_string(), Value::String(layer_name.clone()));
252 trace.add_event(PerfettoEvent {
253 name: layer_name.clone(),
254 phase: PerfettoPhase::Complete,
255 timestamp_us: cursor_us,
256 duration_us: Some(dur_us),
257 pid: 1,
258 tid: 1,
259 args,
260 });
261 cursor_us += dur_us;
262 }
263
264 for bottleneck in &report.bottlenecks {
265 let mut args = HashMap::new();
266 args.insert(
267 "description".to_string(),
268 Value::String(bottleneck.description.clone()),
269 );
270 args.insert(
271 "suggestion".to_string(),
272 Value::String(bottleneck.suggestion.clone()),
273 );
274 trace.add_event(PerfettoEvent {
275 name: format!("bottleneck:{}", bottleneck.location),
276 phase: PerfettoPhase::Instant,
277 timestamp_us: cursor_us,
278 duration_us: None,
279 pid: 1,
280 tid: 1,
281 args,
282 });
283 }
284
285 trace.export_to_file(path)
286 }
287}
288
289fn event_to_value(e: &PerfettoEvent) -> Value {
294 let mut obj = serde_json::json!({
295 "name": e.name,
296 "ph": e.phase.as_code(),
297 "ts": e.timestamp_us,
298 "pid": e.pid,
299 "tid": e.tid,
300 });
301
302 if let Some(dur) = e.duration_us {
303 obj["dur"] = Value::Number(dur.into());
304 }
305
306 if !e.args.is_empty() {
307 obj["args"] = Value::Object(e.args.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
308 }
309
310 obj
311}
312
313#[cfg(test)]
318mod tests {
319 use super::*;
320 use std::time::Duration;
321
322 fn make_event(name: &str, phase: PerfettoPhase, ts: u64, dur: Option<u64>) -> PerfettoEvent {
323 PerfettoEvent {
324 name: name.to_string(),
325 phase,
326 timestamp_us: ts,
327 duration_us: dur,
328 pid: 1,
329 tid: 1,
330 args: HashMap::new(),
331 }
332 }
333
334 #[test]
335 fn test_empty_trace_roundtrip() {
336 let trace = PerfettoTrace::new();
337 let json = trace.export_to_string().unwrap();
338 let parsed: Value = serde_json::from_str(&json).unwrap();
339 assert_eq!(parsed["traceEvents"].as_array().unwrap().len(), 0);
340 assert_eq!(parsed["displayTimeUnit"], "ms");
341 }
342
343 #[test]
344 fn test_add_complete_event() {
345 let mut trace = PerfettoTrace::new();
346 trace.add_event(make_event(
347 "forward",
348 PerfettoPhase::Complete,
349 1000,
350 Some(500),
351 ));
352 assert_eq!(trace.len(), 1);
353
354 let json = trace.export_to_string().unwrap();
355 let parsed: Value = serde_json::from_str(&json).unwrap();
356 let ev = &parsed["traceEvents"][0];
357 assert_eq!(ev["ph"], "X");
358 assert_eq!(ev["ts"], 1000_u64);
359 assert_eq!(ev["dur"], 500_u64);
360 assert_eq!(ev["name"], "forward");
361 }
362
363 #[test]
364 fn test_begin_end_phases() {
365 let mut trace = PerfettoTrace::new();
366 trace.add_event(make_event("op", PerfettoPhase::Begin, 0, None));
367 trace.add_event(make_event("op", PerfettoPhase::End, 200, None));
368 let json = trace.export_to_string().unwrap();
369 let parsed: Value = serde_json::from_str(&json).unwrap();
370 assert_eq!(parsed["traceEvents"][0]["ph"], "B");
371 assert_eq!(parsed["traceEvents"][1]["ph"], "E");
372 }
373
374 #[test]
375 fn test_export_to_file() {
376 let mut dir = std::env::temp_dir();
377 dir.push("perfetto_test_trace.json");
378
379 let mut trace = PerfettoTrace::new();
380 let mut args = HashMap::new();
381 args.insert("batch_size".to_string(), serde_json::json!(32));
382 trace.add_event(PerfettoEvent {
383 name: "attention_forward".to_string(),
384 phase: PerfettoPhase::Complete,
385 timestamp_us: 0,
386 duration_us: Some(1500),
387 pid: 1,
388 tid: 2,
389 args,
390 });
391 trace.export_to_file(&dir).unwrap();
392 assert!(dir.exists());
393
394 let content = std::fs::read_to_string(&dir).unwrap();
395 let parsed: Value = serde_json::from_str(&content).unwrap();
396 assert_eq!(parsed["traceEvents"].as_array().unwrap().len(), 1);
397
398 std::fs::remove_file(&dir).ok();
399 }
400
401 #[test]
402 fn test_exporter_from_profiler_report() {
403 use crate::profiler::{MemoryEfficiencyAnalysis, PerformanceBottleneck};
404
405 let mut dir = std::env::temp_dir();
406 dir.push("perfetto_profiler_report.json");
407
408 let report = ProfilerReport {
409 total_events: 2,
410 total_runtime: Duration::from_millis(100),
411 statistics: HashMap::new(),
412 bottlenecks: vec![PerformanceBottleneck {
413 bottleneck_type: crate::profiler::BottleneckType::CpuBound,
414 location: "attention".to_string(),
415 severity: crate::profiler::BottleneckSeverity::Medium,
416 description: "CPU saturated".to_string(),
417 suggestion: "Use flash attention".to_string(),
418 metrics: HashMap::new(),
419 }],
420 slowest_layers: vec![
421 ("attention".to_string(), Duration::from_millis(10)),
422 ("ffn".to_string(), Duration::from_millis(15)),
423 ],
424 memory_efficiency: MemoryEfficiencyAnalysis::default(),
425 recommendations: vec![],
426 };
427
428 PerfettoExporter::export_profiler_report(&report, &dir).unwrap();
429 assert!(dir.exists());
430
431 let content = std::fs::read_to_string(&dir).unwrap();
432 let parsed: Value = serde_json::from_str(&content).unwrap();
433 let events = parsed["traceEvents"].as_array().unwrap();
434 assert_eq!(events.len(), 3);
436 assert_eq!(events[2]["ph"], "i");
437
438 std::fs::remove_file(&dir).ok();
439 }
440
441 #[test]
442 fn test_instant_and_counter_phases() {
443 let mut trace = PerfettoTrace::new();
444 trace.add_event(make_event("checkpoint", PerfettoPhase::Instant, 500, None));
445 trace.add_event(make_event("loss", PerfettoPhase::Counter, 600, None));
446 let json = trace.export_to_string().unwrap();
447 let parsed: Value = serde_json::from_str(&json).unwrap();
448 assert_eq!(parsed["traceEvents"][0]["ph"], "i");
449 assert_eq!(parsed["traceEvents"][1]["ph"], "C");
450 }
451}