//! Report Generation //! //! Generate reports in various formats from benchmark results. use serde::{Deserialize, Serialize}; use std::io::Write; use std::path::Path; use crate::benchmark::BenchmarkResult; use crate::comparison::ABComparison; use crate::config::OutputFormat; use crate::{BenchmarkError, Result}; /// A complete benchmark report #[derive(Debug, Serialize, Deserialize)] pub struct Report { /// Title of the report pub title: String, /// Description pub description: String, /// Timestamp pub timestamp: chrono::DateTime, /// All benchmark results pub results: Vec, /// System information pub system_info: SystemInfo, } /// System information for reproducibility #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SystemInfo { /// Operating system pub os: String, /// CPU info pub cpu: String, /// Memory info pub memory_gb: f64, /// GPU info (if available) pub gpu: Option, /// Rust version pub rust_version: String, } impl Default for SystemInfo { fn default() -> Self { Self { os: std::env::consts::OS.to_string(), cpu: "Unknown".to_string(), memory_gb: 0.0, gpu: None, rust_version: String::new(), } } } impl Report { /// Create a new report pub fn new(title: impl Into, results: Vec) -> Self { Self { title: title.into(), description: String::new(), timestamp: chrono::Utc::now(), results, system_info: SystemInfo::default(), } } /// Set description pub fn with_description(mut self, desc: impl Into) -> Self { self.description = desc.into(); self } /// Set system info pub fn with_system_info(mut self, info: SystemInfo) -> Self { self.system_info = info; self } /// Export to JSON pub fn to_json(&self) -> Result { serde_json::to_string_pretty(self).map_err(BenchmarkError::from) } /// Export to CSV pub fn to_csv(&self) -> String { let mut csv = String::new(); // Header csv.push_str( "benchmark,implementation,backend,shape,mean_ms,std_dev_ms,min_ms,max_ms,samples\n", ); // Data rows for result in &self.results { csv.push_str(&format!( "{},{},{},{:?},{:.6},{:.6},{:.6},{:.6},{}\n", result.name, result.implementation, result.backend.name(), result.shape, result.metrics.timing.mean.as_secs_f64() * 1000.0, result.metrics.timing.std_dev.as_secs_f64() * 1000.0, result.metrics.timing.min.as_secs_f64() * 1000.0, result.metrics.timing.max.as_secs_f64() * 1000.0, result.metrics.sample_count, )); } csv } /// Export to Markdown pub fn to_markdown(&self) -> String { let mut md = String::new(); md.push_str(&format!("# {}\n\n", self.title)); if !self.description.is_empty() { md.push_str(&format!("{}\n\n", self.description)); } md.push_str(&format!( "Generated: {}\n\n", self.timestamp.format("%Y-%m-%d %H:%M:%S UTC") )); // System info md.push_str("## System Information\n\n"); md.push_str(&format!("- **OS**: {}\n", self.system_info.os)); md.push_str(&format!("- **CPU**: {}\n", self.system_info.cpu)); md.push_str(&format!( "- **Memory**: {:.1} GB\n", self.system_info.memory_gb )); if let Some(ref gpu) = self.system_info.gpu { md.push_str(&format!("- **GPU**: {}\n", gpu)); } md.push('\n'); // Results table md.push_str("## Results\n\n"); md.push_str("| Benchmark | Implementation | Shape | Mean (ms) | Std Dev | Samples |\n"); md.push_str("|-----------|----------------|-------|-----------|---------|--------|\n"); for result in &self.results { md.push_str(&format!( "| {} | {} | {:?} | {:.3} | ±{:.3} | {} |\n", result.name, result.implementation, result.shape, result.metrics.timing.mean.as_secs_f64() * 1000.0, result.metrics.timing.std_dev.as_secs_f64() * 1000.0, result.metrics.sample_count, )); } md } /// Save report to file pub fn save(&self, path: impl AsRef, format: ReportFormat) -> Result<()> { let content = match format { ReportFormat::Json => self.to_json()?, ReportFormat::Csv => self.to_csv(), ReportFormat::Markdown => self.to_markdown(), }; let mut file = std::fs::File::create(path)?; file.write_all(content.as_bytes())?; Ok(()) } /// Load report from JSON file pub fn load(path: impl AsRef) -> Result { let content = std::fs::read_to_string(path)?; serde_json::from_str(&content).map_err(BenchmarkError::from) } } /// Report format for saving #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ReportFormat { Json, Csv, Markdown, } impl From for ReportFormat { fn from(format: OutputFormat) -> Self { match format { OutputFormat::Json => ReportFormat::Json, OutputFormat::Csv => ReportFormat::Csv, OutputFormat::Markdown => ReportFormat::Markdown, OutputFormat::Table => ReportFormat::Markdown, // Default to markdown for table } } } /// Generate a comparison report between two implementations pub fn comparison_report(comparison: &ABComparison, title: impl Into) -> String { let mut report = String::new(); let title = title.into(); report.push_str(&format!("# {}\n\n", title)); report.push_str(&format!( "Generated: {}\n\n", chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC") )); // Get comparison results let comparisons = comparison.compare(); let (winner, avg_speedup) = comparison.overall_winner(); // Summary report.push_str("## Summary\n\n"); report.push_str(&format!("- **Overall Winner**: {:?}\n", winner)); report.push_str(&format!("- **Average Speedup**: {:.2}x\n", avg_speedup)); report.push_str(&format!( "- **Configurations Tested**: {}\n\n", comparisons.len() )); // Detailed results report.push_str("## Detailed Results\n\n"); report.push_str("| Shape | Impl A (ms) | Impl B (ms) | Speedup | Winner |\n"); report.push_str("|-------|-------------|-------------|---------|--------|\n"); for comp in &comparisons { let speedup_str = if comp.speedup > 1.0 { format!("{:.2}x", comp.speedup) } else { format!("{:.2}x", 1.0 / comp.speedup) }; report.push_str(&format!( "| {:?} | {:.3} | {:.3} | {} | {:?} |\n", comp.shape, comp.mean_a_ns as f64 / 1_000_000.0, comp.mean_b_ns as f64 / 1_000_000.0, speedup_str, comp.winner, )); } report } #[cfg(test)] mod tests { use super::*; use crate::benchmark::Measurement; use crate::{Backend, Metrics, OperationType}; use std::time::Duration; fn make_result(name: &str, impl_name: &str, shape: Vec) -> BenchmarkResult { let measurements: Vec<_> = (0..10) .map(|_| Measurement { duration: Duration::from_millis(10), memory_bytes: None, throughput: None, }) .collect(); BenchmarkResult::from_measurements( name.to_string(), OperationType::Add, impl_name.to_string(), Backend::Cpu, shape, measurements, ) } #[test] fn test_report_creation() { let results = vec![ make_result("test", "impl_a", vec![64, 64]), make_result("test", "impl_b", vec![64, 64]), ]; let report = Report::new("Test Report", results).with_description("A test report"); assert_eq!(report.title, "Test Report"); assert_eq!(report.results.len(), 2); } #[test] fn test_to_json() { let results = vec![make_result("test", "impl", vec![64])]; let report = Report::new("Test", results); let json = report.to_json().unwrap(); assert!(json.contains("\"title\": \"Test\"")); } #[test] fn test_to_csv() { let results = vec![make_result("test", "impl", vec![64])]; let report = Report::new("Test", results); let csv = report.to_csv(); assert!(csv.contains("benchmark,implementation")); assert!(csv.contains("test,impl")); } #[test] fn test_to_markdown() { let results = vec![make_result("test", "impl", vec![64])]; let report = Report::new("Test Report", results); let md = report.to_markdown(); assert!(md.contains("# Test Report")); assert!(md.contains("| Benchmark |")); } }