Files
rustytorch/crates/tooling/rtx-kernel-bench/src/report.rs
T
2026-03-04 00:08:42 +00:00

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 |"));
}
}