1use crate::analyzer::AnalysisReport;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum Severity {
12 Info,
14 Warning,
16 Critical,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Change {
23 pub metric: String,
25 pub baseline: f32,
27 pub current: f32,
29 pub percent_change: f32,
31 pub severity: Severity,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct DiffReport {
38 pub name: String,
40 pub changes: Vec<Change>,
42 pub has_regression: bool,
44 pub summary: String,
46}
47
48#[derive(Debug, Clone)]
50pub struct DiffThresholds {
51 pub register_increase_warning: f32,
53 pub register_increase_critical: f32,
55 pub instruction_increase_warning: f32,
57 pub instruction_increase_critical: f32,
59 pub occupancy_decrease_warning: f32,
61 pub occupancy_decrease_critical: f32,
63 pub warning_count_increase: u32,
65}
66
67impl Default for DiffThresholds {
68 fn default() -> Self {
69 Self {
70 register_increase_warning: 10.0, register_increase_critical: 25.0, instruction_increase_warning: 15.0, instruction_increase_critical: 50.0, occupancy_decrease_warning: 10.0, occupancy_decrease_critical: 25.0, warning_count_increase: 2, }
78 }
79}
80
81#[must_use]
83pub fn compare_reports(
84 baseline: &AnalysisReport,
85 current: &AnalysisReport,
86 thresholds: &DiffThresholds,
87) -> DiffReport {
88 let mut changes = Vec::new();
89 let mut has_regression = false;
90
91 let baseline_regs = baseline.registers.total() as f32;
93 let current_regs = current.registers.total() as f32;
94 if baseline_regs > 0.0 {
95 let percent = (current_regs - baseline_regs) / baseline_regs * 100.0;
96 if percent.abs() > 0.1 {
97 let severity = if percent > thresholds.register_increase_critical {
98 has_regression = true;
99 Severity::Critical
100 } else if percent > thresholds.register_increase_warning {
101 Severity::Warning
102 } else {
103 Severity::Info
104 };
105 changes.push(Change {
106 metric: "register_count".to_string(),
107 baseline: baseline_regs,
108 current: current_regs,
109 percent_change: percent,
110 severity,
111 });
112 }
113 }
114
115 let baseline_inst = baseline.instruction_count as f32;
117 let current_inst = current.instruction_count as f32;
118 if baseline_inst > 0.0 {
119 let percent = (current_inst - baseline_inst) / baseline_inst * 100.0;
120 if percent.abs() > 0.1 {
121 let severity = if percent > thresholds.instruction_increase_critical {
122 has_regression = true;
123 Severity::Critical
124 } else if percent > thresholds.instruction_increase_warning {
125 Severity::Warning
126 } else {
127 Severity::Info
128 };
129 changes.push(Change {
130 metric: "instruction_count".to_string(),
131 baseline: baseline_inst,
132 current: current_inst,
133 percent_change: percent,
134 severity,
135 });
136 }
137 }
138
139 let baseline_occ = baseline.estimated_occupancy * 100.0;
141 let current_occ = current.estimated_occupancy * 100.0;
142 let occ_diff = baseline_occ - current_occ; if occ_diff.abs() > 0.1 {
144 let severity = if occ_diff >= thresholds.occupancy_decrease_critical {
145 has_regression = true;
146 Severity::Critical
147 } else if occ_diff >= thresholds.occupancy_decrease_warning {
148 Severity::Warning
149 } else {
150 Severity::Info
151 };
152 changes.push(Change {
153 metric: "estimated_occupancy".to_string(),
154 baseline: baseline_occ,
155 current: current_occ,
156 percent_change: -occ_diff, severity,
158 });
159 }
160
161 let baseline_warns = baseline.warnings.len() as u32;
163 let current_warns = current.warnings.len() as u32;
164 if current_warns > baseline_warns {
165 let increase = current_warns - baseline_warns;
166 let severity = if increase >= thresholds.warning_count_increase {
167 Severity::Warning
168 } else {
169 Severity::Info
170 };
171 changes.push(Change {
172 metric: "muda_warnings".to_string(),
173 baseline: baseline_warns as f32,
174 current: current_warns as f32,
175 percent_change: if baseline_warns > 0 {
176 (increase as f32 / baseline_warns as f32) * 100.0
177 } else {
178 100.0
179 },
180 severity,
181 });
182 }
183
184 let critical_count = changes
186 .iter()
187 .filter(|c| c.severity == Severity::Critical)
188 .count();
189 let warning_count = changes
190 .iter()
191 .filter(|c| c.severity == Severity::Warning)
192 .count();
193
194 let summary = if critical_count > 0 {
195 format!(
196 "{} critical regression(s), {} warning(s)",
197 critical_count, warning_count
198 )
199 } else if warning_count > 0 {
200 format!("{} warning(s), no critical regressions", warning_count)
201 } else if changes.is_empty() {
202 "No significant changes detected".to_string()
203 } else {
204 format!("{} minor change(s)", changes.len())
205 };
206
207 DiffReport {
208 name: current.name.clone(),
209 changes,
210 has_regression,
211 summary,
212 }
213}
214
215#[must_use]
217pub fn format_diff_text(report: &DiffReport) -> String {
218 let mut output = String::new();
219
220 output.push_str(&format!("╔══ Diff Report: {} ══╗\n", report.name));
221 output.push_str(&format!("Summary: {}\n\n", report.summary));
222
223 if report.changes.is_empty() {
224 output.push_str(" No changes detected.\n");
225 } else {
226 for change in &report.changes {
227 let icon = match change.severity {
228 Severity::Critical => "❌",
229 Severity::Warning => "⚠️",
230 Severity::Info => "ℹ️",
231 };
232 let direction = if change.percent_change > 0.0 {
233 "↑"
234 } else {
235 "↓"
236 };
237 output.push_str(&format!(
238 "{} {}: {} → {} ({}{:.1}%)\n",
239 icon,
240 change.metric,
241 change.baseline,
242 change.current,
243 direction,
244 change.percent_change.abs()
245 ));
246 }
247 }
248
249 if report.has_regression {
250 output.push_str("\n🚨 REGRESSION DETECTED - CI should fail\n");
251 }
252
253 output
254}
255
256#[must_use]
258pub fn format_diff_json(report: &DiffReport) -> String {
259 serde_json::to_string_pretty(report).unwrap_or_else(|_| "{}".to_string())
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::analyzer::{MemoryPattern, MudaType, MudaWarning, RegisterUsage, RooflineMetric};
266
267 fn make_warning() -> MudaWarning {
268 MudaWarning {
269 muda_type: MudaType::Transport,
270 description: "Test warning".to_string(),
271 impact: "Minor".to_string(),
272 line: None,
273 suggestion: None,
274 }
275 }
276
277 fn make_report(name: &str, regs: u32, inst: u32, occ: f32, warns: usize) -> AnalysisReport {
278 AnalysisReport {
279 name: name.to_string(),
280 target: "test".to_string(),
281 registers: RegisterUsage {
282 f32_regs: regs,
283 f64_regs: 0,
284 pred_regs: 0,
285 ..Default::default()
286 },
287 memory: MemoryPattern::default(),
288 roofline: RooflineMetric::default(),
289 warnings: (0..warns).map(|_| make_warning()).collect(),
290 instruction_count: inst,
291 estimated_occupancy: occ,
292 }
293 }
294
295 #[test]
296 fn test_no_changes() {
297 let baseline = make_report("test", 32, 100, 0.75, 1);
298 let current = make_report("test", 32, 100, 0.75, 1);
299 let thresholds = DiffThresholds::default();
300
301 let report = compare_reports(&baseline, ¤t, &thresholds);
302
303 assert!(!report.has_regression);
304 assert!(report.changes.is_empty());
305 }
306
307 #[test]
308 fn test_register_increase_warning() {
309 let baseline = make_report("test", 32, 100, 0.75, 1);
310 let current = make_report("test", 36, 100, 0.75, 1); let thresholds = DiffThresholds::default();
312
313 let report = compare_reports(&baseline, ¤t, &thresholds);
314
315 assert!(!report.has_regression);
316 assert!(report
317 .changes
318 .iter()
319 .any(|c| c.metric == "register_count" && c.severity == Severity::Warning));
320 }
321
322 #[test]
323 fn test_register_increase_critical() {
324 let baseline = make_report("test", 32, 100, 0.75, 1);
325 let current = make_report("test", 48, 100, 0.75, 1); let thresholds = DiffThresholds::default();
327
328 let report = compare_reports(&baseline, ¤t, &thresholds);
329
330 assert!(report.has_regression);
331 assert!(report
332 .changes
333 .iter()
334 .any(|c| c.metric == "register_count" && c.severity == Severity::Critical));
335 }
336
337 #[test]
338 fn test_occupancy_decrease() {
339 let baseline = make_report("test", 32, 100, 0.75, 1);
340 let current = make_report("test", 32, 100, 0.50, 1); let thresholds = DiffThresholds::default();
342
343 let report = compare_reports(&baseline, ¤t, &thresholds);
344
345 assert!(report.has_regression);
346 }
347
348 #[test]
349 fn test_warning_count_increase() {
350 let baseline = make_report("test", 32, 100, 0.75, 1);
351 let current = make_report("test", 32, 100, 0.75, 4); let thresholds = DiffThresholds::default();
353
354 let report = compare_reports(&baseline, ¤t, &thresholds);
355
356 assert!(report.changes.iter().any(|c| c.metric == "muda_warnings"));
357 }
358
359 #[test]
360 fn test_format_text() {
361 let baseline = make_report("test", 32, 100, 0.75, 1);
362 let current = make_report("test", 40, 100, 0.75, 1);
363 let thresholds = DiffThresholds::default();
364
365 let report = compare_reports(&baseline, ¤t, &thresholds);
366 let text = format_diff_text(&report);
367
368 assert!(text.contains("Diff Report"));
369 assert!(text.contains("register_count"));
370 }
371
372 #[test]
373 fn test_format_json() {
374 let baseline = make_report("test", 32, 100, 0.75, 1);
375 let current = make_report("test", 32, 100, 0.75, 1);
376 let thresholds = DiffThresholds::default();
377
378 let report = compare_reports(&baseline, ¤t, &thresholds);
379 let json = format_diff_json(&report);
380
381 assert!(json.contains("\"name\": \"test\""));
382 }
383
384 #[test]
386 fn f086_diff_detects_register_regression() {
387 let baseline = make_report("gemm", 32, 500, 0.75, 0);
388 let current = make_report("gemm", 64, 500, 0.75, 0); let thresholds = DiffThresholds::default();
390
391 let report = compare_reports(&baseline, ¤t, &thresholds);
392
393 assert!(report.has_regression, "Should detect register regression");
394 assert!(
395 report
396 .changes
397 .iter()
398 .any(|c| c.metric == "register_count" && c.severity == Severity::Critical),
399 "Register increase should be critical"
400 );
401 }
402
403 #[test]
405 fn f089_diff_exit_code_on_regression() {
406 let baseline = make_report("gemm", 32, 500, 0.75, 0);
407 let current = make_report("gemm", 64, 800, 0.50, 5); let thresholds = DiffThresholds::default();
409
410 let report = compare_reports(&baseline, ¤t, &thresholds);
411
412 assert!(report.has_regression, "Should have regression for CI fail");
414 }
415}