use super::result::{BenchmarkBaseline, BenchmarkResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegressionStatus {
Improved,
Unchanged,
Regressed,
}
impl RegressionStatus {
#[must_use]
pub fn symbol(&self) -> &'static str {
match self {
Self::Improved => "+",
Self::Unchanged => "=",
Self::Regressed => "-",
}
}
#[must_use]
pub fn text(&self) -> &'static str {
match self {
Self::Improved => "IMPROVED",
Self::Unchanged => "UNCHANGED",
Self::Regressed => "REGRESSED",
}
}
}
#[derive(Debug, Clone)]
pub struct RegressionEntry {
pub workload_id: String,
pub size: usize,
pub current_throughput: f64,
pub baseline_throughput: f64,
pub percent_change: f64,
pub status: RegressionStatus,
}
impl RegressionEntry {
#[must_use]
pub fn new(
workload_id: impl Into<String>,
size: usize,
current_throughput: f64,
baseline_throughput: f64,
threshold: f64,
) -> Self {
let percent_change = if baseline_throughput > 0.0 {
((current_throughput - baseline_throughput) / baseline_throughput) * 100.0
} else {
0.0
};
let status = if percent_change < -threshold * 100.0 {
RegressionStatus::Regressed
} else if percent_change > threshold * 100.0 {
RegressionStatus::Improved
} else {
RegressionStatus::Unchanged
};
Self {
workload_id: workload_id.into(),
size,
current_throughput,
baseline_throughput,
percent_change,
status,
}
}
}
#[derive(Debug, Clone)]
pub struct RegressionReport {
pub entries: Vec<RegressionEntry>,
pub regression_count: usize,
pub improvement_count: usize,
pub unchanged_count: usize,
pub overall_status: RegressionStatus,
pub threshold: f64,
}
impl RegressionReport {
#[must_use]
pub fn compare(
current: &[BenchmarkResult],
baseline: &BenchmarkBaseline,
threshold: f64,
) -> Self {
let mut entries = Vec::new();
let mut regression_count = 0;
let mut improvement_count = 0;
let mut unchanged_count = 0;
for result in current {
if let Some(base) = baseline.get(&result.workload_id, result.size) {
let entry = RegressionEntry::new(
&result.workload_id,
result.size,
result.throughput_ops,
base.throughput_ops,
threshold,
);
match entry.status {
RegressionStatus::Regressed => regression_count += 1,
RegressionStatus::Improved => improvement_count += 1,
RegressionStatus::Unchanged => unchanged_count += 1,
}
entries.push(entry);
}
}
let overall_status = if regression_count > 0 {
RegressionStatus::Regressed
} else if improvement_count > 0 {
RegressionStatus::Improved
} else {
RegressionStatus::Unchanged
};
Self {
entries,
regression_count,
improvement_count,
unchanged_count,
overall_status,
threshold,
}
}
#[must_use]
pub fn has_regressions(&self) -> bool {
self.regression_count > 0
}
#[must_use]
pub fn total_comparisons(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn worst_regression(&self) -> Option<&RegressionEntry> {
self.entries
.iter()
.filter(|e| e.status == RegressionStatus::Regressed)
.min_by(|a, b| a.percent_change.partial_cmp(&b.percent_change).unwrap())
}
#[must_use]
pub fn best_improvement(&self) -> Option<&RegressionEntry> {
self.entries
.iter()
.filter(|e| e.status == RegressionStatus::Improved)
.max_by(|a, b| a.percent_change.partial_cmp(&b.percent_change).unwrap())
}
#[must_use]
pub fn summary(&self) -> String {
format!(
"Overall: {} | {} regressions, {} improvements, {} unchanged | Threshold: {:.0}%",
self.overall_status.text(),
self.regression_count,
self.improvement_count,
self.unchanged_count,
self.threshold * 100.0
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_regression_status() {
assert_eq!(RegressionStatus::Improved.symbol(), "+");
assert_eq!(RegressionStatus::Unchanged.symbol(), "=");
assert_eq!(RegressionStatus::Regressed.symbol(), "-");
}
#[test]
fn test_regression_entry() {
let entry = RegressionEntry::new("test", 1000, 80.0, 100.0, 0.10);
assert_eq!(entry.status, RegressionStatus::Regressed);
assert!(entry.percent_change < 0.0);
let entry = RegressionEntry::new("test", 1000, 120.0, 100.0, 0.10);
assert_eq!(entry.status, RegressionStatus::Improved);
assert!(entry.percent_change > 0.0);
let entry = RegressionEntry::new("test", 1000, 105.0, 100.0, 0.10);
assert_eq!(entry.status, RegressionStatus::Unchanged);
}
#[test]
fn test_regression_report() {
let baseline_results = vec![BenchmarkResult::new(
"workload_a",
1000,
Duration::from_millis(100),
)];
let baseline = BenchmarkBaseline::from_results(&baseline_results, "v1.0");
let current_regressed = vec![BenchmarkResult {
workload_id: "workload_a".to_string(),
size: 1000,
throughput_ops: 8000.0, total_time: Duration::from_millis(125),
iterations: None,
converged: None,
measurement_times: vec![Duration::from_millis(125)],
custom_metrics: Default::default(),
}];
let report = RegressionReport::compare(¤t_regressed, &baseline, 0.10);
assert!(report.has_regressions());
assert_eq!(report.regression_count, 1);
let current_improved = vec![BenchmarkResult {
workload_id: "workload_a".to_string(),
size: 1000,
throughput_ops: 12000.0, total_time: Duration::from_millis(83),
iterations: None,
converged: None,
measurement_times: vec![Duration::from_millis(83)],
custom_metrics: Default::default(),
}];
let report = RegressionReport::compare(¤t_improved, &baseline, 0.10);
assert!(!report.has_regressions());
assert_eq!(report.improvement_count, 1);
}
}