use crate ::prelude :: *;
use std ::collections ::HashMap;
#[ derive(Debug, Clone) ]
pub struct BenchmarkDiff
{
pub benchmark_name: String,
pub baseline: BenchmarkResult,
pub current: BenchmarkResult,
pub analysis: PerformanceChange,
}
#[ derive(Debug, Clone) ]
pub struct PerformanceChange
{
pub ops_per_sec_change: f64,
pub mean_time_change: f64,
pub change_type: ChangeType,
pub significance: ChangeSignificanceLevel,
pub summary: String,
}
#[ derive(Debug, Clone, PartialEq) ]
pub enum ChangeType
{
Improvement,
Regression,
MinorImprovement,
MinorRegression,
NoChange,
}
#[ derive(Debug, Clone, PartialEq) ]
pub enum ChangeSignificanceLevel
{
High,
Medium,
Low,
NotSignificant,
}
impl BenchmarkDiff
{
pub fn new(
benchmark_name: &str,
baseline: BenchmarkResult,
current: BenchmarkResult,
) -> Self
{
let analysis = Self ::analyze_change(&baseline, ¤t);
Self
{
benchmark_name: benchmark_name.to_string(),
baseline,
current,
analysis,
}
}
fn analyze_change(baseline: &BenchmarkResult, current: &BenchmarkResult) -> PerformanceChange
{
let baseline_ops = baseline.operations_per_second();
let current_ops = current.operations_per_second();
let baseline_mean = baseline.mean_time().as_secs_f64();
let current_mean = current.mean_time().as_secs_f64();
let ops_change = if baseline_ops > 0.0
{
((current_ops - baseline_ops) / baseline_ops) * 100.0
}
else
{
0.0
};
let time_change = if baseline_mean > 0.0
{
((current_mean - baseline_mean) / baseline_mean) * 100.0
}
else
{
0.0
};
let abs_ops_change = ops_change.abs();
let significance = if abs_ops_change > 20.0
{
ChangeSignificanceLevel ::High
}
else if abs_ops_change > 5.0
{
ChangeSignificanceLevel ::Medium
}
else if abs_ops_change > 1.0
{
ChangeSignificanceLevel ::Low
}
else
{
ChangeSignificanceLevel ::NotSignificant
};
let change_type = match significance
{
ChangeSignificanceLevel ::High =>
{
if ops_change > 0.0
{
ChangeType ::Improvement
}
else
{
ChangeType ::Regression
}
}
ChangeSignificanceLevel ::Medium =>
{
if ops_change > 0.0
{
ChangeType ::MinorImprovement
}
else
{
ChangeType ::MinorRegression
}
}
ChangeSignificanceLevel ::Low =>
{
if ops_change > 0.0
{
ChangeType ::MinorImprovement
}
else
{
ChangeType ::MinorRegression
}
}
ChangeSignificanceLevel ::NotSignificant => ChangeType ::NoChange,
};
let summary = match change_type
{
ChangeType ::Improvement => format!("🚀 Performance improved by {:.1}%", ops_change),
ChangeType ::Regression => format!("📉 Performance regressed by {:.1}%", ops_change.abs()),
ChangeType ::MinorImprovement => format!("📈 Minor improvement: +{:.1}%", ops_change),
ChangeType ::MinorRegression => format!("📊 Minor regression: -{:.1}%", ops_change.abs()),
ChangeType ::NoChange => "🔄 No significant change".to_string(),
};
PerformanceChange
{
ops_per_sec_change: ops_change,
mean_time_change: time_change,
change_type,
significance,
summary,
}
}
pub fn to_diff_format( &self ) -> String
{
let mut output = String ::new();
output.push_str(&format!("diff --benchmark a/{} b/{}\n", self.benchmark_name, self.benchmark_name));
output.push_str(&format!("index baseline..current\n"));
output.push_str(&format!("--- a/{}\n", self.benchmark_name));
output.push_str(&format!("+++ b/{}\n", self.benchmark_name));
output.push_str("@@");
match self.analysis.change_type
{
ChangeType ::Improvement => output.push_str(" Performance Improvement "),
ChangeType ::Regression => output.push_str(" Performance Regression "),
ChangeType ::MinorImprovement => output.push_str(" Minor Improvement "),
ChangeType ::MinorRegression => output.push_str(" Minor Regression "),
ChangeType ::NoChange => output.push_str(" No Change "),
}
output.push_str("@@\n");
let baseline_ops = self.baseline.operations_per_second();
let current_ops = self.current.operations_per_second();
output.push_str(&format!("-Operations/sec: {:.0}\n", baseline_ops));
output.push_str(&format!("+Operations/sec: {:.0}\n", current_ops));
output.push_str(&format!("-Mean time: {:.2?}\n", self.baseline.mean_time()));
output.push_str(&format!("+Mean time: {:.2?}\n", self.current.mean_time()));
output.push_str(&format!("\nSummary: {}\n", self.analysis.summary));
output
}
pub fn to_summary( &self ) -> String
{
let change_symbol = match self.analysis.change_type
{
ChangeType ::Improvement => "✅",
ChangeType ::Regression => "❌",
ChangeType ::MinorImprovement => "📈",
ChangeType ::MinorRegression => "📉",
ChangeType ::NoChange => "🔄",
};
format!(
"{} {} : {} ({:.0} → {:.0} ops/sec)",
change_symbol,
self.benchmark_name,
self.analysis.summary,
self.baseline.operations_per_second(),
self.current.operations_per_second()
)
}
pub fn is_significant( &self ) -> bool
{
matches!(
self.analysis.significance,
ChangeSignificanceLevel ::High | ChangeSignificanceLevel ::Medium
)
}
pub fn is_regression( &self ) -> bool
{
matches!(
self.analysis.change_type,
ChangeType ::Regression | ChangeType ::MinorRegression
)
}
pub fn is_improvement( &self ) -> bool
{
matches!(
self.analysis.change_type,
ChangeType ::Improvement | ChangeType ::MinorImprovement
)
}
}
#[ derive(Debug, Clone) ]
pub struct BenchmarkDiffSet
{
pub diffs: Vec< BenchmarkDiff >,
pub baseline_timestamp: Option< String >,
pub current_timestamp: Option< String >,
pub summary_stats: DiffSummaryStats,
}
#[ derive(Debug, Clone) ]
pub struct DiffSummaryStats
{
pub total_benchmarks: usize,
pub improvements: usize,
pub regressions: usize,
pub no_change: usize,
pub average_change: f64,
}
impl BenchmarkDiffSet
{
pub fn compare_results(
baseline_results: &[ (String, BenchmarkResult)],
current_results: &[ (String, BenchmarkResult)],
) -> Self
{
let mut diffs = Vec ::new();
let baseline_map: HashMap< &String, &BenchmarkResult > = baseline_results.iter().map(|(k, v)| (k, v)).collect();
let _current_map: HashMap< &String, &BenchmarkResult > = current_results.iter().map(|(k, v)| (k, v)).collect();
for (name, current_result) in current_results
{
if let Some(baseline_result) = baseline_map.get(name)
{
let diff = BenchmarkDiff ::new(name, (*baseline_result).clone(), current_result.clone());
diffs.push(diff);
}
}
let summary_stats = Self ::calculate_summary_stats(&diffs);
Self
{
diffs,
baseline_timestamp: None,
current_timestamp: None,
summary_stats,
}
}
fn calculate_summary_stats(diffs: &[ BenchmarkDiff]) -> DiffSummaryStats
{
let total = diffs.len();
let mut improvements = 0;
let mut regressions = 0;
let mut no_change = 0;
let mut total_change = 0.0;
for diff in diffs
{
match diff.analysis.change_type
{
ChangeType ::Improvement | ChangeType ::MinorImprovement => improvements += 1,
ChangeType ::Regression | ChangeType ::MinorRegression => regressions += 1,
ChangeType ::NoChange => no_change += 1,
}
total_change += diff.analysis.ops_per_sec_change;
}
let average_change = if total > 0 { total_change / total as f64 } else { 0.0 };
DiffSummaryStats
{
total_benchmarks: total,
improvements,
regressions,
no_change,
average_change,
}
}
pub fn to_report( &self ) -> String
{
let mut output = String ::new();
output.push_str("# Benchmark Diff Report\n\n");
if let (Some(baseline), Some(current)) = (&self.baseline_timestamp, &self.current_timestamp)
{
output.push_str(&format!("**Baseline** : {}\n", baseline));
output.push_str(&format!("**Current** : {}\n\n", current));
}
output.push_str("## Summary\n\n");
output.push_str(&format!("- **Total benchmarks** : {}\n", self.summary_stats.total_benchmarks));
output.push_str(&format!("- **Improvements** : {} 📈\n", self.summary_stats.improvements));
output.push_str(&format!("- **Regressions** : {} 📉\n", self.summary_stats.regressions));
output.push_str(&format!("- **No change** : {} 🔄\n", self.summary_stats.no_change));
output.push_str(&format!("- **Average change** : {:.1}%\n\n", self.summary_stats.average_change));
output.push_str("## Individual Results\n\n");
for diff in &self.diffs
{
output.push_str(&format!("{}\n", diff.to_summary()));
}
let significant_changes: Vec< _ > = self.diffs.iter()
.filter(|d| d.is_significant())
.collect();
if !significant_changes.is_empty()
{
output.push_str("\n## Significant Changes\n\n");
for diff in significant_changes
{
output.push_str(&format!("### {}\n\n", diff.benchmark_name));
output.push_str(&format!("{}\n", diff.to_diff_format()));
output.push_str("\n");
}
}
output
}
pub fn regressions( &self ) -> Vec< &BenchmarkDiff >
{
self.diffs.iter().filter(|d| d.is_regression()).collect()
}
pub fn improvements( &self ) -> Vec< &BenchmarkDiff >
{
self.diffs.iter().filter(|d| d.is_improvement()).collect()
}
pub fn significant_changes( &self ) -> Vec< &BenchmarkDiff >
{
self.diffs.iter().filter(|d| d.is_significant()).collect()
}
}
pub fn diff_benchmark_results(
name: &str,
baseline: BenchmarkResult,
current: BenchmarkResult,
) -> BenchmarkDiff
{
BenchmarkDiff ::new(name, baseline, current)
}
pub fn diff_benchmark_sets(
baseline_results: &[ (String, BenchmarkResult)],
current_results: &[ (String, BenchmarkResult)],
) -> BenchmarkDiffSet
{
BenchmarkDiffSet ::compare_results(baseline_results, current_results)
}