1use crate::analyzer::AnalysisReport;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct MetricComparison {
11 pub name: String,
13 pub value_a: f32,
15 pub value_b: f32,
17 pub winner: String,
19 pub notes: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ComparisonReport {
26 pub report_a_name: String,
28 pub report_b_name: String,
30 pub metrics: Vec<MetricComparison>,
32 pub recommendation: String,
34}
35
36#[derive(Copy, Clone)]
38enum Direction {
39 LowerIsBetter,
40 HigherIsBetter,
41}
42
43fn pick_winner(value_a: f32, value_b: f32, dir: Direction) -> &'static str {
44 let (a_better, b_better) = match dir {
45 Direction::LowerIsBetter => (value_a < value_b, value_b < value_a),
46 Direction::HigherIsBetter => (value_a > value_b, value_b > value_a),
47 };
48 if a_better {
49 "A"
50 } else if b_better {
51 "B"
52 } else {
53 "Tie"
54 }
55}
56
57fn metric_comparison(
58 name: &str,
59 value_a: f32,
60 value_b: f32,
61 dir: Direction,
62 notes: &str,
63) -> MetricComparison {
64 MetricComparison {
65 name: name.to_string(),
66 value_a,
67 value_b,
68 winner: pick_winner(value_a, value_b, dir).to_string(),
69 notes: notes.to_string(),
70 }
71}
72
73fn collect_metrics(a: &AnalysisReport, b: &AnalysisReport) -> Vec<MetricComparison> {
74 vec![
75 metric_comparison(
76 "Register Count",
77 a.registers.total() as f32,
78 b.registers.total() as f32,
79 Direction::LowerIsBetter,
80 "Lower is better (higher occupancy)",
81 ),
82 metric_comparison(
83 "Instruction Count",
84 a.instruction_count as f32,
85 b.instruction_count as f32,
86 Direction::LowerIsBetter,
87 "Lower is better (less work)",
88 ),
89 metric_comparison(
90 "Estimated Occupancy",
91 a.estimated_occupancy * 100.0,
92 b.estimated_occupancy * 100.0,
93 Direction::HigherIsBetter,
94 "Higher is better (GPU utilization)",
95 ),
96 metric_comparison(
97 "Muda Warnings",
98 a.warnings.len() as f32,
99 b.warnings.len() as f32,
100 Direction::LowerIsBetter,
101 "Lower is better (less waste)",
102 ),
103 metric_comparison(
104 "Memory Coalescing",
105 a.memory.coalesced_ratio * 100.0,
106 b.memory.coalesced_ratio * 100.0,
107 Direction::HigherIsBetter,
108 "Higher is better (bandwidth efficiency)",
109 ),
110 ]
111}
112
113fn recommendation_text(metrics: &[MetricComparison], name_a: &str, name_b: &str) -> String {
114 let a_wins = metrics.iter().filter(|m| m.winner == "A").count();
115 let b_wins = metrics.iter().filter(|m| m.winner == "B").count();
116 match a_wins.cmp(&b_wins) {
117 std::cmp::Ordering::Greater => format!("{name_a} wins {a_wins} to {b_wins} metrics"),
118 std::cmp::Ordering::Less => format!("{name_b} wins {b_wins} to {a_wins} metrics"),
119 std::cmp::Ordering::Equal => "Both configurations are comparable".to_string(),
120 }
121}
122
123#[must_use]
125pub fn compare_analyses(report_a: &AnalysisReport, report_b: &AnalysisReport) -> ComparisonReport {
126 let metrics = collect_metrics(report_a, report_b);
127 let recommendation = recommendation_text(&metrics, &report_a.name, &report_b.name);
128 ComparisonReport {
129 report_a_name: report_a.name.clone(),
130 report_b_name: report_b.name.clone(),
131 metrics,
132 recommendation,
133 }
134}
135
136#[must_use]
138pub fn format_comparison_text(report: &ComparisonReport) -> String {
139 let mut output = String::new();
140
141 output.push_str(&format!(
142 "╔══ Comparison: {} vs {} ══╗\n\n",
143 report.report_a_name, report.report_b_name
144 ));
145
146 output.push_str(&format!(
147 "{:<25} {:>12} {:>12} {:>8}\n",
148 "Metric", &report.report_a_name, &report.report_b_name, "Winner"
149 ));
150 output.push_str(&format!("{}\n", "─".repeat(60)));
151
152 for metric in &report.metrics {
153 let winner_icon = match metric.winner.as_str() {
154 "A" => "◀",
155 "B" => "▶",
156 _ => "═",
157 };
158 output.push_str(&format!(
159 "{:<25} {:>12.1} {:>12.1} {:>6} {}\n",
160 metric.name, metric.value_a, metric.value_b, winner_icon, metric.winner
161 ));
162 }
163
164 output.push_str(&format!("\n{}\n", report.recommendation));
165
166 output
167}
168
169#[must_use]
171pub fn format_comparison_json(report: &ComparisonReport) -> String {
172 serde_json::to_string_pretty(report).unwrap_or_else(|_| "{}".to_string())
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::analyzer::{MemoryPattern, MudaWarning, RegisterUsage, RooflineMetric};
179
180 fn make_report(
181 name: &str,
182 regs: u32,
183 inst: u32,
184 occ: f32,
185 warns: usize,
186 coal: f32,
187 ) -> AnalysisReport {
188 AnalysisReport {
189 name: name.to_string(),
190 target: "PTX".to_string(),
191 registers: RegisterUsage {
192 f32_regs: regs,
193 ..Default::default()
194 },
195 memory: MemoryPattern {
196 coalesced_ratio: coal,
197 ..Default::default()
198 },
199 roofline: RooflineMetric::default(),
200 warnings: (0..warns)
201 .map(|_| MudaWarning {
202 muda_type: crate::analyzer::MudaType::Transport,
203 description: "test".to_string(),
204 impact: "test".to_string(),
205 line: None,
206 suggestion: None,
207 })
208 .collect(),
209 instruction_count: inst,
210 estimated_occupancy: occ,
211 }
212 }
213
214 #[test]
215 fn test_compare_identical() {
216 let report_a = make_report("A", 32, 100, 0.75, 0, 0.95);
217 let report_b = make_report("B", 32, 100, 0.75, 0, 0.95);
218
219 let comparison = compare_analyses(&report_a, &report_b);
220
221 assert!(comparison.metrics.iter().all(|m| m.winner == "Tie"));
223 }
224
225 #[test]
226 fn test_compare_clear_winner() {
227 let report_a = make_report("Optimized", 16, 50, 0.90, 0, 0.98);
228 let report_b = make_report("Baseline", 64, 200, 0.50, 3, 0.70);
229
230 let comparison = compare_analyses(&report_a, &report_b);
231
232 let a_wins = comparison
234 .metrics
235 .iter()
236 .filter(|m| m.winner == "A")
237 .count();
238 assert!(a_wins >= 4, "Optimized should win most metrics");
239 assert!(comparison.recommendation.contains("Optimized"));
240 }
241
242 #[test]
243 fn test_compare_mixed() {
244 let report_a = make_report("LowReg", 16, 100, 0.90, 5, 0.80);
246 let report_b = make_report("HighReg", 64, 100, 0.50, 0, 0.95);
247
248 let comparison = compare_analyses(&report_a, &report_b);
249
250 let a_wins = comparison
252 .metrics
253 .iter()
254 .filter(|m| m.winner == "A")
255 .count();
256 let b_wins = comparison
257 .metrics
258 .iter()
259 .filter(|m| m.winner == "B")
260 .count();
261 assert!(a_wins > 0 && b_wins > 0, "Should have mixed winners");
262 }
263
264 #[test]
265 fn test_format_text() {
266 let report_a = make_report("A", 32, 100, 0.75, 1, 0.90);
267 let report_b = make_report("B", 48, 150, 0.60, 2, 0.85);
268
269 let comparison = compare_analyses(&report_a, &report_b);
270 let text = format_comparison_text(&comparison);
271
272 assert!(text.contains("Comparison"));
273 assert!(text.contains("Register Count"));
274 assert!(text.contains("Instruction Count"));
275 }
276
277 #[test]
278 fn test_format_json() {
279 let report_a = make_report("A", 32, 100, 0.75, 0, 0.90);
280 let report_b = make_report("B", 32, 100, 0.75, 0, 0.90);
281
282 let comparison = compare_analyses(&report_a, &report_b);
283 let json = format_comparison_json(&comparison);
284
285 assert!(json.contains("\"report_a_name\": \"A\""));
286 assert!(json.contains("\"report_b_name\": \"B\""));
287 }
288}