1use crate::analysis::regression::RegressionDetector;
6use crate::metrics::catalog::FullProfile;
7use crate::metrics::export;
8use anyhow::Result;
9use serde::Serialize;
10use std::path::Path;
11
12#[derive(Debug, Serialize)]
14pub struct MetricDiff {
15 pub name: String,
16 pub baseline: f64,
17 pub current: f64,
18 pub change_pct: f64,
19 pub verdict: &'static str,
20}
21
22pub fn diff_profiles(baseline: &FullProfile, current: &FullProfile) -> Vec<MetricDiff> {
24 let mut diffs = Vec::new();
25
26 add_diff(
28 &mut diffs,
29 "wall_clock_time_us",
30 baseline.timing.wall_clock_time_us,
31 current.timing.wall_clock_time_us,
32 true, );
34
35 add_diff(
37 &mut diffs,
38 "tflops",
39 baseline.throughput.tflops,
40 current.throughput.tflops,
41 false, );
43 add_diff(
44 &mut diffs,
45 "bandwidth_gbps",
46 baseline.throughput.bandwidth_gbps,
47 current.throughput.bandwidth_gbps,
48 false,
49 );
50
51 if let (Some(bg), Some(cg)) = (&baseline.gpu_compute, ¤t.gpu_compute) {
53 add_diff(
54 &mut diffs,
55 "sm_utilization_pct",
56 bg.sm_utilization_pct,
57 cg.sm_utilization_pct,
58 false,
59 );
60 add_diff(
61 &mut diffs,
62 "achieved_occupancy_pct",
63 bg.achieved_occupancy_pct,
64 cg.achieved_occupancy_pct,
65 false,
66 );
67 add_diff(
68 &mut diffs,
69 "warp_exec_efficiency_pct",
70 bg.warp_execution_efficiency_pct,
71 cg.warp_execution_efficiency_pct,
72 false,
73 );
74 }
75
76 if let (Some(bm), Some(cm)) = (&baseline.gpu_memory, ¤t.gpu_memory) {
78 add_diff(
79 &mut diffs,
80 "l2_hit_rate_pct",
81 bm.l2_hit_rate_pct,
82 cm.l2_hit_rate_pct,
83 false,
84 );
85 add_diff(
86 &mut diffs,
87 "global_load_efficiency_pct",
88 bm.global_load_efficiency_pct,
89 cm.global_load_efficiency_pct,
90 false,
91 );
92 }
93
94 diffs
95}
96
97fn add_diff(
98 diffs: &mut Vec<MetricDiff>,
99 name: &str,
100 baseline: f64,
101 current: f64,
102 lower_better: bool,
103) {
104 if baseline == 0.0 && current == 0.0 {
105 return;
106 }
107 let change_pct = if baseline != 0.0 {
108 (current - baseline) / baseline * 100.0
109 } else {
110 0.0
111 };
112
113 let verdict = if change_pct.abs() < 2.0 {
114 "="
115 } else if lower_better {
116 if current < baseline {
117 "IMPROVED"
118 } else {
119 "REGRESSED"
120 }
121 } else if current > baseline {
122 "IMPROVED"
123 } else {
124 "REGRESSED"
125 };
126
127 diffs.push(MetricDiff {
128 name: name.to_string(),
129 baseline,
130 current,
131 change_pct,
132 verdict,
133 });
134}
135
136pub fn render_diff(diffs: &[MetricDiff], baseline_name: &str, current_name: &str) {
138 println!("\n=== CGP Profile Diff ===\n");
139 println!(" Baseline: {baseline_name}");
140 println!(" Current: {current_name}\n");
141
142 println!(
143 " {:30} {:>14} {:>14} {:>10} {:>10}",
144 "Metric", "Baseline", "Current", "Change", "Verdict"
145 );
146 println!(" {}", "-".repeat(82));
147
148 for d in diffs {
149 let change_str = format!("{:+.1}%", d.change_pct);
150 println!(
151 " {:30} {:>14.2} {:>14.2} {:>10} {:>10}",
152 d.name, d.baseline, d.current, change_str, d.verdict
153 );
154 }
155
156 let regressions = diffs.iter().filter(|d| d.verdict == "REGRESSED").count();
158 let improvements = diffs.iter().filter(|d| d.verdict == "IMPROVED").count();
159 println!();
160 if regressions > 0 {
161 println!(" \x1b[31m{regressions} regression(s)\x1b[0m, {improvements} improvement(s)");
162 } else if improvements > 0 {
163 println!(" \x1b[32m{improvements} improvement(s)\x1b[0m, no regressions");
164 } else {
165 println!(" No significant changes.");
166 }
167 println!();
168}
169
170pub fn run_diff(
172 baseline: Option<&str>,
173 current: Option<&str>,
174 _before: Option<&str>,
175 _after: Option<&str>,
176 json: bool,
177) -> Result<()> {
178 let (baseline_path, current_path) = match (baseline, current) {
179 (Some(b), Some(c)) => (b, c),
180 _ => {
181 anyhow::bail!(
182 "Usage: cgp diff --baseline <file.json> --current <file.json>\n\
183 Or: cgp diff --before <commit> --after <commit> (not yet implemented)"
184 );
185 }
186 };
187
188 let start = std::time::Instant::now();
189
190 let baseline_profile = export::load_json(Path::new(baseline_path))?;
191 let current_profile = export::load_json(Path::new(current_path))?;
192
193 let diffs = diff_profiles(&baseline_profile, ¤t_profile);
194
195 if json {
196 println!("{}", serde_json::to_string_pretty(&diffs)?);
197 return Ok(());
198 }
199
200 render_diff(&diffs, baseline_path, current_path);
201
202 if baseline_profile.timing.samples > 1 && current_profile.timing.samples > 1 {
204 let detector = RegressionDetector::new();
205 let b_mean = baseline_profile.timing.wall_clock_time_us;
207 let b_std = baseline_profile.timing.stddev_us.max(b_mean * 0.01);
208 let c_mean = current_profile.timing.wall_clock_time_us;
209 let c_std = current_profile.timing.stddev_us.max(c_mean * 0.01);
210
211 let b_samples: Vec<f64> = (0..30)
212 .map(|i| b_mean + b_std * ((i as f64 - 15.0) / 15.0))
213 .collect();
214 let c_samples: Vec<f64> = (0..30)
215 .map(|i| c_mean + c_std * ((i as f64 - 15.0) / 15.0))
216 .collect();
217
218 let result = detector.compare(&b_samples, &c_samples);
219 println!(
220 " Statistical: {} (change {:.1}%, Cohen's d = {:.2})",
221 result.verdict, result.change_pct, result.effect_size_cohens_d
222 );
223 } else {
224 let b = baseline_profile.timing.wall_clock_time_us;
226 let c = current_profile.timing.wall_clock_time_us;
227 if b > 0.0 && c > 0.0 {
228 let change = (c - b) / b * 100.0;
229 let verdict = if change < -5.0 {
230 "IMPROVED"
231 } else if change > 5.0 {
232 "REGRESSED"
233 } else {
234 "NO_CHANGE"
235 };
236 println!(" Statistical: {verdict} (change {change:.1}%, single-sample)");
237 }
238 }
239
240 let elapsed = start.elapsed();
241 println!(
242 " Diff completed in {:.0}ms",
243 elapsed.as_secs_f64() * 1000.0
244 );
245 println!();
246
247 Ok(())
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::metrics::catalog::*;
254
255 fn make_profile(time_us: f64, tflops: f64) -> FullProfile {
256 FullProfile {
257 version: "2.0".to_string(),
258 timing: TimingMetrics {
259 wall_clock_time_us: time_us,
260 samples: 50,
261 ..Default::default()
262 },
263 throughput: ThroughputMetrics {
264 tflops,
265 ..Default::default()
266 },
267 ..Default::default()
268 }
269 }
270
271 #[test]
272 fn test_diff_improvement() {
273 let baseline = make_profile(35.7, 7.5);
274 let current = make_profile(23.2, 11.6);
275 let diffs = diff_profiles(&baseline, ¤t);
276
277 let time_diff = diffs
278 .iter()
279 .find(|d| d.name == "wall_clock_time_us")
280 .unwrap();
281 assert_eq!(time_diff.verdict, "IMPROVED"); assert!(time_diff.change_pct < -30.0);
283
284 let tflops_diff = diffs.iter().find(|d| d.name == "tflops").unwrap();
285 assert_eq!(tflops_diff.verdict, "IMPROVED"); }
287
288 #[test]
289 fn test_diff_regression() {
290 let baseline = make_profile(23.2, 11.6);
291 let current = make_profile(35.7, 7.5);
292 let diffs = diff_profiles(&baseline, ¤t);
293
294 let time_diff = diffs
295 .iter()
296 .find(|d| d.name == "wall_clock_time_us")
297 .unwrap();
298 assert_eq!(time_diff.verdict, "REGRESSED");
299 }
300
301 #[test]
302 fn test_diff_no_change() {
303 let baseline = make_profile(23.2, 11.6);
304 let current = make_profile(23.4, 11.5);
305 let diffs = diff_profiles(&baseline, ¤t);
306
307 let time_diff = diffs
308 .iter()
309 .find(|d| d.name == "wall_clock_time_us")
310 .unwrap();
311 assert_eq!(time_diff.verdict, "="); }
313
314 #[test]
316 fn test_diff_speed() {
317 let baseline = make_profile(23.2, 11.6);
318 let current = make_profile(35.7, 7.5);
319
320 let start = std::time::Instant::now();
321 for _ in 0..100 {
322 let _ = diff_profiles(&baseline, ¤t);
323 }
324 let elapsed = start.elapsed();
325 assert!(
327 elapsed.as_millis() < 100,
328 "100 diffs took {}ms",
329 elapsed.as_millis()
330 );
331 }
332}