1use crate::bench::manifest::Tolerance;
13use crate::bench::report::BenchReport;
14
15#[derive(Debug)]
16pub struct DiffReport {
17 pub scenario: String,
18 pub p50: MetricDiff,
19 pub p95: MetricDiff,
20 pub regressed: bool,
22 pub notes: Vec<String>,
25}
26
27#[derive(Debug, Clone, Copy)]
28pub struct MetricDiff {
29 pub baseline: f64,
30 pub current: f64,
31 pub delta_pct: f64,
32 pub tolerance_pct: f64,
33 pub regressed: bool,
34}
35
36impl MetricDiff {
37 fn new(baseline: f64, current: f64, tolerance_pct: f64) -> Self {
38 let delta_pct = if baseline > 0.0 {
39 ((current - baseline) / baseline) * 100.0
40 } else {
41 0.0
42 };
43 let regressed = delta_pct > tolerance_pct;
45 Self {
46 baseline,
47 current,
48 delta_pct,
49 tolerance_pct,
50 regressed,
51 }
52 }
53}
54
55pub fn diff(current: &BenchReport, baseline: &BenchReport, tolerance: Tolerance) -> DiffReport {
56 let p50 = MetricDiff::new(
57 baseline.iterations.p50_ms,
58 current.iterations.p50_ms,
59 tolerance.wall_time_p50_pct,
60 );
61 let p95 = MetricDiff::new(
62 baseline.iterations.p95_ms,
63 current.iterations.p95_ms,
64 tolerance.wall_time_p95_pct,
65 );
66
67 let mut notes = Vec::new();
68 if current.scenario.target != baseline.scenario.target {
69 notes.push(format!(
70 "target changed: baseline={} current={} — different units, do not trust the diff",
71 baseline.scenario.target, current.scenario.target
72 ));
73 }
74 if current.passes_applied != baseline.passes_applied {
75 notes.push(format!(
76 "passes_applied changed: baseline={:?} current={:?}",
77 baseline.passes_applied, current.passes_applied
78 ));
79 }
80 match (current.response_bytes, baseline.response_bytes) {
81 (Some(c), Some(b)) if c != b => {
82 notes.push(format!("response_bytes: baseline={} current={}", b, c));
83 }
84 _ => {}
85 }
86
87 DiffReport {
88 scenario: current.scenario.name.clone(),
89 p50,
90 p95,
91 regressed: p50.regressed || p95.regressed,
92 notes,
93 }
94}
95
96pub fn format_diff(diff: &DiffReport) -> String {
100 use std::fmt::Write;
101
102 fn fmt_ms(ms: f64) -> String {
103 if ms >= 1.0 {
104 format!("{:.2}ms", ms)
105 } else {
106 format!("{:.0}µs", ms * 1000.0)
107 }
108 }
109 fn fmt_metric(label: &str, m: &MetricDiff) -> String {
110 let sign = if m.delta_pct >= 0.0 { "+" } else { "" };
111 let verdict = if m.regressed {
112 format!("REGRESSION (limit +{:.0}%)", m.tolerance_pct)
113 } else {
114 "ok".to_string()
115 };
116 format!(
117 " {:<6} {} (baseline {}, {}{:.1}%) {}",
118 label,
119 fmt_ms(m.current),
120 fmt_ms(m.baseline),
121 sign,
122 m.delta_pct,
123 verdict,
124 )
125 }
126
127 let mut out = String::new();
128 writeln!(
129 out,
130 "{}: {}",
131 diff.scenario,
132 if diff.regressed { "REGRESSION" } else { "ok" }
133 )
134 .ok();
135 writeln!(out, "{}", fmt_metric("p50", &diff.p50)).ok();
136 writeln!(out, "{}", fmt_metric("p95", &diff.p95)).ok();
137 for note in &diff.notes {
138 writeln!(out, " note: {}", note).ok();
139 }
140 out
141}