1use std::fmt::Write as _;
2
3use crate::{
4 metrics::RunResult,
5 report::compare::{BootstrapDelta, ComparisonReport, MetricKey, Verdict},
6 runner::abba::AbbaResult,
7};
8
9pub fn render_abba(result: &AbbaResult) -> String {
11 let mut s = String::new();
12 let _ = writeln!(s, "## arb-bench: `{}`", result.manifest_name);
13 let _ = writeln!(s, "- Iterations: {}", result.iterations);
14 let _ = writeln!(s, "- Verdict: **{}**", result.verdict.label());
15 if let Verdict::Regression { metric, delta_pct } = &result.verdict {
16 let _ = writeln!(s, "- Regressing metric: `{metric}` (Δ {delta_pct:+.2}%)");
17 }
18 let _ = writeln!(s);
19 let _ = writeln!(
20 s,
21 "| Metric | Baseline | Feature | Δ mean | 95% CI | Verdict |"
22 );
23 let _ = writeln!(s, "|---|---|---|---|---|---|");
24 for (key, d) in &result.deltas {
25 let row = render_metric_row(*key, d);
26 let _ = writeln!(s, "{row}");
27 }
28 s
29}
30
31pub fn render_comparison(report: &ComparisonReport) -> String {
32 let mut s = String::new();
33 let _ = writeln!(s, "## arb-bench compare: `{}`", report.manifest_name);
34 let _ = writeln!(s, "- Verdict: **{}**", report.verdict.label());
35 let _ = writeln!(s);
36 let _ = writeln!(s, "| Metric | Baseline | Feature | Δ mean | 95% CI |");
37 let _ = writeln!(s, "|---|---|---|---|---|");
38 for (key, d) in &report.deltas {
39 let row = render_metric_row(*key, d);
40 let _ = writeln!(s, "{row}");
41 }
42 s
43}
44
45pub fn render_run_summary(r: &RunResult) -> String {
46 let mut s = String::new();
47 let _ = writeln!(s, "### `{}`", r.manifest_name);
48 let _ = writeln!(s, "- Blocks: {}", r.summary.block_count);
49 let _ = writeln!(s, "- Total gas: {}", r.summary.total_gas);
50 let _ = writeln!(s, "- Mean gas/sec: {:.0}", r.summary.gas_per_sec_mean);
51 let _ = writeln!(
52 s,
53 "- p50 / p95 / p99 wall (ms): {:.2} / {:.2} / {:.2}",
54 r.summary.wall_clock_ns_p50 as f64 / 1.0e6,
55 r.summary.wall_clock_ns_p95 as f64 / 1.0e6,
56 r.summary.wall_clock_ns_p99 as f64 / 1.0e6
57 );
58 let _ = writeln!(
59 s,
60 "- Peak RSS: {:.1} MiB",
61 r.summary.peak_rss_bytes as f64 / 1024.0 / 1024.0
62 );
63 if r.summary.monotonic_slowdown_detected {
64 let _ = writeln!(s, "- ⚠️ Monotonic slowdown detected.");
65 }
66 s
67}
68
69fn render_metric_row(key: MetricKey, d: &BootstrapDelta) -> String {
70 let (baseline_fmt, feature_fmt, delta_fmt, ci_fmt, verdict_label) = match key {
71 MetricKey::WallClockNs => {
72 let bl = d.baseline_mean / 1.0e6;
73 let ft = d.feature_mean / 1.0e6;
74 let pct = if d.baseline_mean > 0.0 {
75 d.mean / d.baseline_mean * 100.0
76 } else {
77 0.0
78 };
79 let label = direction_label(pct, false);
80 (
81 format!("{bl:.2} ms"),
82 format!("{ft:.2} ms"),
83 format!("{:+.2} ms ({pct:+.2}%)", d.mean / 1.0e6),
84 format!(
85 "[{:+.2}, {:+.2}] ms",
86 d.ci_low_95 / 1.0e6,
87 d.ci_high_95 / 1.0e6
88 ),
89 label,
90 )
91 }
92 MetricKey::CpuNs => {
93 let bl = d.baseline_mean / 1.0e6;
94 let ft = d.feature_mean / 1.0e6;
95 let pct = if d.baseline_mean > 0.0 {
96 d.mean / d.baseline_mean * 100.0
97 } else {
98 0.0
99 };
100 let label = direction_label(pct, false);
101 (
102 format!("{bl:.2} ms"),
103 format!("{ft:.2} ms"),
104 format!("{:+.2} ms ({pct:+.2}%)", d.mean / 1.0e6),
105 format!(
106 "[{:+.2}, {:+.2}] ms",
107 d.ci_low_95 / 1.0e6,
108 d.ci_high_95 / 1.0e6
109 ),
110 label,
111 )
112 }
113 MetricKey::GasPerSec => {
114 let bl = d.baseline_mean / 1.0e6;
115 let ft = d.feature_mean / 1.0e6;
116 let pct = if d.baseline_mean > 0.0 {
117 d.mean / d.baseline_mean * 100.0
118 } else {
119 0.0
120 };
121 let label = direction_label(pct, true);
122 (
123 format!("{bl:.1} Mgas/s"),
124 format!("{ft:.1} Mgas/s"),
125 format!("{:+.1} Mgas/s ({pct:+.2}%)", d.mean / 1.0e6),
126 format!(
127 "[{:+.1}, {:+.1}] Mgas/s",
128 d.ci_low_95 / 1.0e6,
129 d.ci_high_95 / 1.0e6
130 ),
131 label,
132 )
133 }
134 MetricKey::RssBytes => {
135 let bl = d.baseline_mean / (1024.0 * 1024.0);
136 let ft = d.feature_mean / (1024.0 * 1024.0);
137 let pct = if d.baseline_mean > 0.0 {
138 d.mean / d.baseline_mean * 100.0
139 } else {
140 0.0
141 };
142 let label = direction_label(pct, false);
143 (
144 format!("{bl:.1} MiB"),
145 format!("{ft:.1} MiB"),
146 format!("{:+.1} MiB ({pct:+.2}%)", d.mean / (1024.0 * 1024.0)),
147 format!(
148 "[{:+.1}, {:+.1}] MiB",
149 d.ci_low_95 / (1024.0 * 1024.0),
150 d.ci_high_95 / (1024.0 * 1024.0)
151 ),
152 label,
153 )
154 }
155 };
156 format!(
157 "| {key:?} | {baseline_fmt} | {feature_fmt} | {delta_fmt} | {ci_fmt} | {verdict_label} |"
158 )
159}
160
161fn direction_label(pct: f64, higher_is_better: bool) -> String {
162 let improvement = (pct < 0.0 && !higher_is_better) || (pct > 0.0 && higher_is_better);
163 let regression = (pct > 0.0 && !higher_is_better) || (pct < 0.0 && higher_is_better);
164 if pct.abs() < 0.5 {
165 "≈".into()
166 } else if improvement {
167 "↑ better".into()
168 } else if regression {
169 "↓ worse".into()
170 } else {
171 "≈".into()
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::metrics::SummaryMetrics;
179
180 #[test]
181 fn render_compare_well_formed() {
182 let report = ComparisonReport {
183 manifest_name: "t".into(),
184 baseline_summary: SummaryMetrics::default(),
185 feature_summary: SummaryMetrics::default(),
186 deltas: vec![(
187 MetricKey::WallClockNs,
188 BootstrapDelta {
189 n: 10,
190 baseline_mean: 1.0e6,
191 feature_mean: 9.0e5,
192 mean: -1.0e5,
193 ci_low_95: -2.0e5,
194 ci_high_95: -1.0e4,
195 },
196 )],
197 verdict: Verdict::Improvement,
198 };
199 let s = render_comparison(&report);
200 assert!(s.contains("IMPROVEMENT"));
201 assert!(s.contains("WallClockNs"));
202 }
203}