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
81fn classify_severity(value: f32, warning_threshold: f32, critical_threshold: f32) -> Severity {
84 if value > critical_threshold {
85 Severity::Critical
86 } else if value > warning_threshold {
87 Severity::Warning
88 } else {
89 Severity::Info
90 }
91}
92
93fn compare_percent_metric(
96 metric: &str,
97 baseline_val: f32,
98 current_val: f32,
99 warning_threshold: f32,
100 critical_threshold: f32,
101 changes: &mut Vec<Change>,
102) -> bool {
103 if baseline_val <= 0.0 {
104 return false;
105 }
106 let percent = (current_val - baseline_val) / baseline_val * 100.0;
107 if percent.abs() <= 0.1 {
108 return false;
109 }
110 let severity = classify_severity(percent, warning_threshold, critical_threshold);
111 let is_critical = severity == Severity::Critical;
112 changes.push(Change {
113 metric: metric.to_string(),
114 baseline: baseline_val,
115 current: current_val,
116 percent_change: percent,
117 severity,
118 });
119 is_critical
120}
121
122fn compare_occupancy(
124 baseline: &AnalysisReport,
125 current: &AnalysisReport,
126 thresholds: &DiffThresholds,
127 changes: &mut Vec<Change>,
128) -> bool {
129 let baseline_occ = baseline.estimated_occupancy * 100.0;
130 let current_occ = current.estimated_occupancy * 100.0;
131 let occ_diff = baseline_occ - current_occ; if occ_diff.abs() <= 0.1 {
133 return false;
134 }
135 let severity = if occ_diff >= thresholds.occupancy_decrease_critical {
137 Severity::Critical
138 } else if occ_diff >= thresholds.occupancy_decrease_warning {
139 Severity::Warning
140 } else {
141 Severity::Info
142 };
143 let is_critical = severity == Severity::Critical;
144 changes.push(Change {
145 metric: "estimated_occupancy".to_string(),
146 baseline: baseline_occ,
147 current: current_occ,
148 percent_change: -occ_diff, severity,
150 });
151 is_critical
152}
153
154fn compare_warning_counts(
156 baseline: &AnalysisReport,
157 current: &AnalysisReport,
158 thresholds: &DiffThresholds,
159 changes: &mut Vec<Change>,
160) {
161 let baseline_warns = baseline.warnings.len() as u32;
162 let current_warns = current.warnings.len() as u32;
163 if current_warns <= baseline_warns {
164 return;
165 }
166 let increase = current_warns - baseline_warns;
167 let severity = if increase >= thresholds.warning_count_increase {
168 Severity::Warning
169 } else {
170 Severity::Info
171 };
172 let percent_change = if baseline_warns > 0 {
173 (increase as f32 / baseline_warns as f32) * 100.0
174 } else {
175 100.0
176 };
177 changes.push(Change {
178 metric: "muda_warnings".to_string(),
179 baseline: baseline_warns as f32,
180 current: current_warns as f32,
181 percent_change,
182 severity,
183 });
184}
185
186fn build_summary(changes: &[Change]) -> String {
188 let critical_count = changes
189 .iter()
190 .filter(|c| c.severity == Severity::Critical)
191 .count();
192 let warning_count = changes
193 .iter()
194 .filter(|c| c.severity == Severity::Warning)
195 .count();
196
197 if critical_count > 0 {
198 format!(
199 "{} critical regression(s), {} warning(s)",
200 critical_count, warning_count
201 )
202 } else if warning_count > 0 {
203 format!("{} warning(s), no critical regressions", warning_count)
204 } else if changes.is_empty() {
205 "No significant changes detected".to_string()
206 } else {
207 format!("{} minor change(s)", changes.len())
208 }
209}
210
211#[must_use]
213pub fn compare_reports(
214 baseline: &AnalysisReport,
215 current: &AnalysisReport,
216 thresholds: &DiffThresholds,
217) -> DiffReport {
218 let mut changes = Vec::new();
219
220 let reg_critical = compare_percent_metric(
222 "register_count",
223 baseline.registers.total() as f32,
224 current.registers.total() as f32,
225 thresholds.register_increase_warning,
226 thresholds.register_increase_critical,
227 &mut changes,
228 );
229
230 let inst_critical = compare_percent_metric(
232 "instruction_count",
233 baseline.instruction_count as f32,
234 current.instruction_count as f32,
235 thresholds.instruction_increase_warning,
236 thresholds.instruction_increase_critical,
237 &mut changes,
238 );
239
240 let occ_critical = compare_occupancy(baseline, current, thresholds, &mut changes);
242
243 compare_warning_counts(baseline, current, thresholds, &mut changes);
245
246 let has_regression = reg_critical || inst_critical || occ_critical;
247 let summary = build_summary(&changes);
248
249 DiffReport {
250 name: current.name.clone(),
251 changes,
252 has_regression,
253 summary,
254 }
255}
256
257#[must_use]
259pub fn format_diff_text(report: &DiffReport) -> String {
260 let mut output = String::new();
261
262 output.push_str(&format!("╔══ Diff Report: {} ══╗\n", report.name));
263 output.push_str(&format!("Summary: {}\n\n", report.summary));
264
265 if report.changes.is_empty() {
266 output.push_str(" No changes detected.\n");
267 } else {
268 for change in &report.changes {
269 let icon = match change.severity {
270 Severity::Critical => "❌",
271 Severity::Warning => "⚠️",
272 Severity::Info => "ℹ️",
273 };
274 let direction = if change.percent_change > 0.0 {
275 "↑"
276 } else {
277 "↓"
278 };
279 output.push_str(&format!(
280 "{} {}: {} → {} ({}{:.1}%)\n",
281 icon,
282 change.metric,
283 change.baseline,
284 change.current,
285 direction,
286 change.percent_change.abs()
287 ));
288 }
289 }
290
291 if report.has_regression {
292 output.push_str("\n🚨 REGRESSION DETECTED - CI should fail\n");
293 }
294
295 output
296}
297
298#[must_use]
300pub fn format_diff_json(report: &DiffReport) -> String {
301 serde_json::to_string_pretty(report).unwrap_or_else(|_| "{}".to_string())
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use crate::analyzer::{MemoryPattern, MudaType, MudaWarning, RegisterUsage, RooflineMetric};
308
309 fn make_warning() -> MudaWarning {
310 MudaWarning {
311 muda_type: MudaType::Transport,
312 description: "Test warning".to_string(),
313 impact: "Minor".to_string(),
314 line: None,
315 suggestion: None,
316 }
317 }
318
319 fn make_report(name: &str, regs: u32, inst: u32, occ: f32, warns: usize) -> AnalysisReport {
320 AnalysisReport {
321 name: name.to_string(),
322 target: "test".to_string(),
323 registers: RegisterUsage {
324 f32_regs: regs,
325 f64_regs: 0,
326 pred_regs: 0,
327 ..Default::default()
328 },
329 memory: MemoryPattern::default(),
330 roofline: RooflineMetric::default(),
331 warnings: (0..warns).map(|_| make_warning()).collect(),
332 instruction_count: inst,
333 estimated_occupancy: occ,
334 }
335 }
336
337 #[test]
338 fn test_no_changes() {
339 let baseline = make_report("test", 32, 100, 0.75, 1);
340 let current = make_report("test", 32, 100, 0.75, 1);
341 let thresholds = DiffThresholds::default();
342
343 let report = compare_reports(&baseline, ¤t, &thresholds);
344
345 assert!(!report.has_regression);
346 assert!(report.changes.is_empty());
347 }
348
349 #[test]
350 fn test_register_increase_warning() {
351 let baseline = make_report("test", 32, 100, 0.75, 1);
352 let current = make_report("test", 36, 100, 0.75, 1); let thresholds = DiffThresholds::default();
354
355 let report = compare_reports(&baseline, ¤t, &thresholds);
356
357 assert!(!report.has_regression);
358 assert!(report
359 .changes
360 .iter()
361 .any(|c| c.metric == "register_count" && c.severity == Severity::Warning));
362 }
363
364 #[test]
365 fn test_register_increase_critical() {
366 let baseline = make_report("test", 32, 100, 0.75, 1);
367 let current = make_report("test", 48, 100, 0.75, 1); let thresholds = DiffThresholds::default();
369
370 let report = compare_reports(&baseline, ¤t, &thresholds);
371
372 assert!(report.has_regression);
373 assert!(report
374 .changes
375 .iter()
376 .any(|c| c.metric == "register_count" && c.severity == Severity::Critical));
377 }
378
379 #[test]
380 fn test_occupancy_decrease() {
381 let baseline = make_report("test", 32, 100, 0.75, 1);
382 let current = make_report("test", 32, 100, 0.50, 1); let thresholds = DiffThresholds::default();
384
385 let report = compare_reports(&baseline, ¤t, &thresholds);
386
387 assert!(report.has_regression);
388 }
389
390 #[test]
391 fn test_warning_count_increase() {
392 let baseline = make_report("test", 32, 100, 0.75, 1);
393 let current = make_report("test", 32, 100, 0.75, 4); let thresholds = DiffThresholds::default();
395
396 let report = compare_reports(&baseline, ¤t, &thresholds);
397
398 assert!(report.changes.iter().any(|c| c.metric == "muda_warnings"));
399 }
400
401 #[test]
402 fn test_format_text() {
403 let baseline = make_report("test", 32, 100, 0.75, 1);
404 let current = make_report("test", 40, 100, 0.75, 1);
405 let thresholds = DiffThresholds::default();
406
407 let report = compare_reports(&baseline, ¤t, &thresholds);
408 let text = format_diff_text(&report);
409
410 assert!(text.contains("Diff Report"));
411 assert!(text.contains("register_count"));
412 }
413
414 #[test]
415 fn test_format_json() {
416 let baseline = make_report("test", 32, 100, 0.75, 1);
417 let current = make_report("test", 32, 100, 0.75, 1);
418 let thresholds = DiffThresholds::default();
419
420 let report = compare_reports(&baseline, ¤t, &thresholds);
421 let json = format_diff_json(&report);
422
423 assert!(json.contains("\"name\": \"test\""));
424 }
425
426 #[test]
428 fn f086_diff_detects_register_regression() {
429 let baseline = make_report("gemm", 32, 500, 0.75, 0);
430 let current = make_report("gemm", 64, 500, 0.75, 0); let thresholds = DiffThresholds::default();
432
433 let report = compare_reports(&baseline, ¤t, &thresholds);
434
435 assert!(report.has_regression, "Should detect register regression");
436 assert!(
437 report
438 .changes
439 .iter()
440 .any(|c| c.metric == "register_count" && c.severity == Severity::Critical),
441 "Register increase should be critical"
442 );
443 }
444
445 #[test]
447 fn f089_diff_exit_code_on_regression() {
448 let baseline = make_report("gemm", 32, 500, 0.75, 0);
449 let current = make_report("gemm", 64, 800, 0.50, 5); let thresholds = DiffThresholds::default();
451
452 let report = compare_reports(&baseline, ¤t, &thresholds);
453
454 assert!(report.has_regression, "Should have regression for CI fail");
456 }
457}