318 lines
9.2 KiB
Rust
318 lines
9.2 KiB
Rust
//! 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<chrono::Utc>,
|
|
/// All benchmark results
|
|
pub results: Vec<BenchmarkResult>,
|
|
/// 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<String>,
|
|
/// 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<String>, results: Vec<BenchmarkResult>) -> 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<String>) -> 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<String> {
|
|
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<Path>, 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<Path>) -> Result<Self> {
|
|
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<OutputFormat> 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>) -> 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<usize>) -> 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 |"));
|
|
}
|
|
}
|