Skip to main content

trueno_explain/
compare.rs

1//! Backend comparison module
2//!
3//! Compares kernel performance characteristics across different backends or configurations.
4
5use crate::analyzer::AnalysisReport;
6use serde::{Deserialize, Serialize};
7
8/// Comparison result for a single metric
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct MetricComparison {
11    /// Metric name
12    pub name: String,
13    /// Value in first report
14    pub value_a: f32,
15    /// Value in second report
16    pub value_b: f32,
17    /// Winner ("A", "B", or "Tie")
18    pub winner: String,
19    /// Notes about the comparison
20    pub notes: String,
21}
22
23/// Full comparison report
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ComparisonReport {
26    /// First report name
27    pub report_a_name: String,
28    /// Second report name
29    pub report_b_name: String,
30    /// Individual metric comparisons
31    pub metrics: Vec<MetricComparison>,
32    /// Overall recommendation
33    pub recommendation: String,
34}
35
36/// Direction that makes a metric "better" when comparing two reports.
37#[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/// Compare two analysis reports
124#[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/// Format comparison report as text
137#[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/// Format comparison report as JSON
170#[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        // All ties
222        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        // A should win on all metrics
233        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        // A has fewer registers but more warnings
245        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        // Should have mixed results
251        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}