cbtop/optimize/
regression.rs1use crate::error::CbtopError;
4use serde::{Deserialize, Serialize};
5
6use super::suite::{BaselineEntry, BaselineReport};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct RegressionEntry {
11 pub workload: String,
13 pub size: usize,
15 pub baseline_gflops: f64,
17 pub current_gflops: f64,
19 pub change_percent: f64,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct RegressionReport {
26 pub passed: bool,
28 pub regressions: Vec<RegressionEntry>,
30 pub improvements: Vec<RegressionEntry>,
32 pub summary: String,
34}
35
36pub struct RegressionDetector {
38 baseline: BaselineReport,
40 threshold_percent: f64,
42}
43
44impl RegressionDetector {
45 pub fn new(baseline: BaselineReport, threshold_percent: f64) -> Self {
47 Self {
48 baseline,
49 threshold_percent,
50 }
51 }
52
53 pub fn from_file(path: &std::path::Path, threshold_percent: f64) -> Result<Self, CbtopError> {
55 let baseline = BaselineReport::load(path)?;
56 Ok(Self::new(baseline, threshold_percent))
57 }
58
59 pub fn check(&self, current: &BaselineReport) -> RegressionReport {
61 let mut regressions = Vec::new();
62 let mut improvements = Vec::new();
63
64 for current_entry in ¤t.entries {
65 if let Some(baseline_entry) = self.find_baseline(current_entry) {
66 if baseline_entry.gflops > 0.0 {
67 let change = (current_entry.gflops - baseline_entry.gflops)
68 / baseline_entry.gflops
69 * 100.0;
70
71 if change < -self.threshold_percent {
72 regressions.push(RegressionEntry {
73 workload: current_entry.workload.clone(),
74 size: current_entry.size,
75 baseline_gflops: baseline_entry.gflops,
76 current_gflops: current_entry.gflops,
77 change_percent: change,
78 });
79 } else if change > self.threshold_percent {
80 improvements.push(RegressionEntry {
81 workload: current_entry.workload.clone(),
82 size: current_entry.size,
83 baseline_gflops: baseline_entry.gflops,
84 current_gflops: current_entry.gflops,
85 change_percent: change,
86 });
87 }
88 }
89 }
90 }
91
92 let passed = regressions.is_empty();
93 let summary = if passed {
94 if improvements.is_empty() {
95 "All benchmarks within threshold".to_string()
96 } else {
97 format!("{} improvements detected", improvements.len())
98 }
99 } else {
100 format!(
101 "FAILED: {} regressions detected (threshold: {}%)",
102 regressions.len(),
103 self.threshold_percent
104 )
105 };
106
107 RegressionReport {
108 passed,
109 regressions,
110 improvements,
111 summary,
112 }
113 }
114
115 fn find_baseline(&self, current: &BaselineEntry) -> Option<&BaselineEntry> {
116 self.baseline.entries.iter().find(|b| {
117 b.workload == current.workload && b.size == current.size && b.backend == current.backend
118 })
119 }
120}
121
122impl RegressionReport {
123 pub fn exit_code(&self) -> i32 {
125 if self.passed {
126 0
127 } else {
128 1
129 }
130 }
131
132 pub fn format_report(&self) -> String {
134 let mut report = String::new();
135
136 report.push_str("# Regression Check Report\n\n");
137 report.push_str(&format!("**Status**: {}\n\n", self.summary));
138
139 if !self.regressions.is_empty() {
140 report.push_str("## Regressions\n\n");
141 for r in &self.regressions {
142 report.push_str(&format!(
143 "- **{}** @ {}: {:.1} -> {:.1} GFLOP/s ({:.1}%)\n",
144 r.workload, r.size, r.baseline_gflops, r.current_gflops, r.change_percent
145 ));
146 }
147 report.push('\n');
148 }
149
150 if !self.improvements.is_empty() {
151 report.push_str("## Improvements\n\n");
152 for i in &self.improvements {
153 report.push_str(&format!(
154 "- **{}** @ {}: {:.1} -> {:.1} GFLOP/s (+{:.1}%)\n",
155 i.workload, i.size, i.baseline_gflops, i.current_gflops, i.change_percent
156 ));
157 }
158 }
159
160 report
161 }
162}