Initial commit
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
//! 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 |"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user