use crate::models::{BenchmarkSuite, ComparisonResult};
use crate::parser;
use anyhow::Result;
use colored::Colorize;
use std::path::Path;
use tabled::{settings::Style, Table, Tabled};
#[derive(Tabled)]
struct ComparisonRow {
#[tabled(rename = "Benchmark")]
name: String,
#[tabled(rename = "Baseline (ns)")]
baseline: String,
#[tabled(rename = "Current (ns)")]
current: String,
#[tabled(rename = "Change")]
change: String,
#[tabled(rename = "Status")]
status: String,
}
pub fn generate_text_report(
comparisons: &[ComparisonResult],
baseline: &BenchmarkSuite,
threshold: f64,
missing_baselines: &[String],
missing_current: &[String],
) -> Result<()> {
println!("{}", "=== Benchmark Comparison Report ===".bold());
println!();
println!(
"Baseline: {} ({})",
baseline.name.cyan(),
baseline.created_at
);
if let Some(commit) = &baseline.commit {
println!("Baseline commit: {}", commit);
}
println!("Regression threshold: {}%", threshold);
println!();
if comparisons.is_empty() {
println!("{}", "No benchmarks to compare!".yellow());
return Ok(());
}
let mut rows = Vec::new();
let mut regressions = 0;
let mut improvements = 0;
let mut stable = 0;
for comp in comparisons {
let full_name = match &comp.parameter {
Some(param) => format!("{}/{}", comp.name, param),
None => comp.name.clone(),
};
let change_str = if comp.change_percent >= 0.0 {
format!("+{:.2}%", comp.change_percent)
} else {
format!("{:.2}%", comp.change_percent)
};
let (status, colored_change) = if comp.is_regression {
regressions += 1;
("REGRESSION".red().to_string(), change_str.red().to_string())
} else if comp.is_improvement {
improvements += 1;
(
"IMPROVEMENT".green().to_string(),
change_str.green().to_string(),
)
} else {
stable += 1;
("STABLE".blue().to_string(), change_str.blue().to_string())
};
rows.push(ComparisonRow {
name: full_name,
baseline: format!("{:.2}", comp.baseline_mean),
current: format!("{:.2}", comp.current_mean),
change: colored_change,
status,
});
}
rows.sort_by(|a, b| {
let a_priority = if a.status.contains("REGRESSION") {
0
} else if a.status.contains("IMPROVEMENT") {
2
} else {
1
};
let b_priority = if b.status.contains("REGRESSION") {
0
} else if b.status.contains("IMPROVEMENT") {
2
} else {
1
};
a_priority
.cmp(&b_priority)
.then_with(|| a.name.cmp(&b.name))
});
let table = Table::new(rows).with(Style::modern()).to_string();
println!("{}", table);
println!();
println!("{}", "Summary:".bold());
println!(
" {} Regressions",
if regressions > 0 {
format!("{}", regressions).red()
} else {
format!("{}", regressions).normal()
}
);
println!(
" {} Improvements",
if improvements > 0 {
format!("{}", improvements).green()
} else {
format!("{}", improvements).normal()
}
);
println!(" {} Stable", stable);
println!(" {} Total", comparisons.len());
println!();
if !missing_baselines.is_empty() {
println!("{}", "New benchmarks (not in baseline):".yellow());
for name in missing_baselines {
println!(" - {}", name);
}
println!();
}
if !missing_current.is_empty() {
println!(
"{}",
"Missing benchmarks (in baseline but not current):".red()
);
for name in missing_current {
println!(" - {}", name);
}
println!();
}
Ok(())
}
pub fn generate_json_report(
comparisons: &[ComparisonResult],
baseline: &BenchmarkSuite,
) -> Result<()> {
use serde_json::json;
let report = json!({
"baseline": {
"name": baseline.name,
"created_at": baseline.created_at,
"commit": baseline.commit,
},
"comparisons": comparisons.iter().map(|c| {
json!({
"name": c.name,
"parameter": c.parameter,
"baseline_mean_ns": c.baseline_mean,
"current_mean_ns": c.current_mean,
"change_percent": c.change_percent,
"is_regression": c.is_regression,
"is_improvement": c.is_improvement,
})
}).collect::<Vec<_>>(),
});
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub fn generate_html_report(
comparisons: &[ComparisonResult],
baseline: &BenchmarkSuite,
) -> Result<()> {
let mut html = String::from(
r#"<!DOCTYPE html>
<html>
<head>
<title>Benchmark Comparison Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #4CAF50; color: white; }
tr:nth-child(even) { background-color: #f2f2f2; }
.regression { color: red; font-weight: bold; }
.improvement { color: green; font-weight: bold; }
.stable { color: blue; }
.summary { margin-top: 20px; padding: 10px; background-color: #f9f9f9; border-radius: 5px; }
</style>
</head>
<body>
<h1>Benchmark Comparison Report</h1>
"#,
);
html.push_str(&format!(
"<p><strong>Baseline:</strong> {} ({})</p>",
baseline.name, baseline.created_at
));
if let Some(commit) = &baseline.commit {
html.push_str(&format!("<p><strong>Commit:</strong> {}</p>", commit));
}
html.push_str("<table><tr><th>Benchmark</th><th>Baseline (ns)</th><th>Current (ns)</th><th>Change</th><th>Status</th></tr>");
let mut regressions = 0;
let mut improvements = 0;
let mut stable = 0;
for comp in comparisons {
let full_name = match &comp.parameter {
Some(param) => format!("{}/{}", comp.name, param),
None => comp.name.clone(),
};
let change_str = if comp.change_percent >= 0.0 {
format!("+{:.2}%", comp.change_percent)
} else {
format!("{:.2}%", comp.change_percent)
};
let (status, class) = if comp.is_regression {
regressions += 1;
("REGRESSION", "regression")
} else if comp.is_improvement {
improvements += 1;
("IMPROVEMENT", "improvement")
} else {
stable += 1;
("STABLE", "stable")
};
html.push_str(&format!(
"<tr><td>{}</td><td>{:.2}</td><td>{:.2}</td><td class=\"{}\">{}</td><td class=\"{}\">{}</td></tr>",
full_name, comp.baseline_mean, comp.current_mean, class, change_str, class, status
));
}
html.push_str("</table>");
html.push_str(&format!(
r#"<div class="summary">
<h2>Summary</h2>
<p><span class="regression">Regressions: {}</span></p>
<p><span class="improvement">Improvements: {}</span></p>
<p><span class="stable">Stable: {}</span></p>
<p><strong>Total: {}</strong></p>
</div>
</body>
</html>"#,
regressions,
improvements,
stable,
comparisons.len()
));
println!("{}", html);
Ok(())
}
pub fn show_stats(name: &str, criterion_dir: &Path) -> Result<()> {
let results = parser::parse_criterion_output(criterion_dir)?;
let matching: Vec<_> = results.iter().filter(|r| r.name == name).collect();
if matching.is_empty() {
anyhow::bail!("No benchmark found with name: {}", name);
}
println!("{}", format!("=== Statistics for {} ===", name).bold());
println!();
for result in matching {
let param_str = result
.parameter
.as_ref()
.map(|p| format!(" (parameter: {})", p))
.unwrap_or_default();
println!("{}{}", result.name.cyan().bold(), param_str);
println!(" Timestamp: {}", result.timestamp);
println!();
let est = &result.estimates;
println!(" {}", "Mean:".bold());
println!(" Point estimate: {:.2} ns", est.mean.point_estimate);
println!(" Standard error: {:.2} ns", est.mean.standard_error);
println!(
" 95% CI: [{:.2}, {:.2}] ns",
est.mean.confidence_interval.lower_bound, est.mean.confidence_interval.upper_bound
);
println!();
println!(" {}", "Median:".bold());
println!(" Point estimate: {:.2} ns", est.median.point_estimate);
println!(
" 95% CI: [{:.2}, {:.2}] ns",
est.median.confidence_interval.lower_bound, est.median.confidence_interval.upper_bound
);
println!();
println!(" {}", "Standard Deviation:".bold());
println!(" Point estimate: {:.2} ns", est.std_dev.point_estimate);
println!(
" 95% CI: [{:.2}, {:.2}] ns",
est.std_dev.confidence_interval.lower_bound,
est.std_dev.confidence_interval.upper_bound
);
println!();
}
Ok(())
}