fluxbench_cli/executor/
formatting.rs1use fluxbench_report::{BenchmarkReportResult, BenchmarkStatus, Report};
14
15pub fn format_human_output(report: &Report) -> String {
23 let mut output = String::new();
24
25 output.push('\n');
26 output.push_str("FluxBench Results\n");
27 output.push_str(&"=".repeat(60));
28 output.push_str("\n\n");
29
30 let mut groups: std::collections::BTreeMap<&str, Vec<&BenchmarkReportResult>> =
32 std::collections::BTreeMap::new();
33 for result in &report.results {
34 groups.entry(&result.group).or_default().push(result);
35 }
36
37 for (group, results) in groups {
38 output.push_str(&format!("Group: {}\n", group));
39 output.push_str(&"-".repeat(60));
40 output.push('\n');
41
42 for result in results {
43 let status_icon = match result.status {
44 BenchmarkStatus::Passed => "✓",
45 BenchmarkStatus::Failed => "✗",
46 BenchmarkStatus::Crashed => "💥",
47 BenchmarkStatus::Skipped => "⊘",
48 };
49
50 output.push_str(&format!(" {} {}\n", status_icon, result.id));
51
52 if let Some(metrics) = &result.metrics {
53 output.push_str(&format!(
54 " mean: {:.2} ns median: {:.2} ns stddev: {:.2} ns\n",
55 metrics.mean_ns, metrics.median_ns, metrics.std_dev_ns
56 ));
57 output.push_str(&format!(
58 " min: {:.2} ns max: {:.2} ns samples: {}\n",
59 metrics.min_ns, metrics.max_ns, metrics.samples
60 ));
61 output.push_str(&format!(
62 " p50: {:.2} ns p95: {:.2} ns p99: {:.2} ns\n",
63 metrics.p50_ns, metrics.p95_ns, metrics.p99_ns
64 ));
65 output.push_str(&format!(
66 " 95% CI: [{:.2}, {:.2}] ns\n",
67 metrics.ci_lower_ns, metrics.ci_upper_ns
68 ));
69 if let Some(throughput) = metrics.throughput_ops_sec {
70 output.push_str(&format!(" throughput: {:.2} ops/sec\n", throughput));
71 }
72 if metrics.alloc_bytes > 0 {
73 output.push_str(&format!(
74 " allocations: {} bytes ({} allocs)\n",
75 metrics.alloc_bytes, metrics.alloc_count
76 ));
77 }
78 if metrics.mean_cycles > 0.0 {
80 output.push_str(&format!(
81 " cycles: mean {:.0} median {:.0} ({:.2} GHz)\n",
82 metrics.mean_cycles, metrics.median_cycles, metrics.cycles_per_ns
83 ));
84 }
85 }
86
87 if let Some(failure) = &result.failure {
88 output.push_str(&format!(" error: {}\n", failure.message));
89 }
90
91 output.push('\n');
92 }
93 }
94
95 for cmp in &report.comparisons {
97 output.push_str(&format!("\n{}\n", cmp.title));
98 output.push_str(&"-".repeat(60));
99 output.push('\n');
100
101 let max_name_len = cmp
103 .entries
104 .iter()
105 .map(|e| e.benchmark_id.len())
106 .max()
107 .unwrap_or(20);
108
109 output.push_str(&format!(
111 " {:<width$} {:>12} {:>10}\n",
112 "Benchmark",
113 cmp.metric,
114 "Speedup",
115 width = max_name_len
116 ));
117 output.push_str(&format!(" {}\n", "-".repeat(max_name_len + 26)));
118
119 let mut sorted_entries: Vec<_> = cmp.entries.iter().collect();
121 sorted_entries.sort_by(|a, b| {
122 b.speedup
123 .partial_cmp(&a.speedup)
124 .unwrap_or(std::cmp::Ordering::Equal)
125 });
126
127 for entry in sorted_entries {
128 let baseline_marker = if entry.is_baseline { " (baseline)" } else { "" };
129 let speedup_str = if entry.is_baseline {
130 "1.00x".to_string()
131 } else {
132 format!("{:.2}x", entry.speedup)
133 };
134
135 output.push_str(&format!(
136 " {:<width$} {:>12.2} {:>10}{}\n",
137 entry.benchmark_id,
138 entry.value,
139 speedup_str,
140 baseline_marker,
141 width = max_name_len
142 ));
143 }
144 }
145
146 for series in &report.comparison_series {
148 output.push_str(&format!("\n{} ({})\n", series.title, series.metric));
149 output.push_str(&"-".repeat(60));
150 output.push('\n');
151
152 let max_name_len = series
154 .series_names
155 .iter()
156 .map(|n| n.len())
157 .max()
158 .unwrap_or(12);
159
160 let col_width = series
162 .x_values
163 .iter()
164 .map(|x| x.len())
165 .max()
166 .unwrap_or(8)
167 .max(10); output.push_str(&format!(" {:<width$}", "", width = max_name_len));
171 for x in &series.x_values {
172 output.push_str(&format!(" | {:>w$}", x, w = col_width));
173 }
174 output.push('\n');
175
176 output.push_str(&format!(" {}", "-".repeat(max_name_len)));
178 for _ in &series.x_values {
179 output.push_str(&format!("-+-{}", "-".repeat(col_width)));
180 }
181 output.push('\n');
182
183 for (series_idx, name) in series.series_names.iter().enumerate() {
185 output.push_str(&format!(" {:<width$}", name, width = max_name_len));
186 for x_idx in 0..series.x_values.len() {
187 let value = series
188 .series_data
189 .get(series_idx)
190 .and_then(|row| row.get(x_idx))
191 .copied()
192 .unwrap_or(0.0);
193 let formatted = if value == 0.0 {
195 "-".to_string()
196 } else if value.abs() >= 1_000_000.0 || (value.abs() < 0.001 && value != 0.0) {
197 format!("{:.2e}", value)
198 } else if value.abs() >= 1000.0 {
199 format!("{:.0}", value)
200 } else {
201 format!("{:.2}", value)
202 };
203 output.push_str(&format!(" | {:>w$}", formatted, w = col_width));
204 }
205 output.push('\n');
206 }
207 }
208
209 if !report.synthetics.is_empty() {
211 output.push_str("\nComputed Metrics\n");
212 output.push_str(&"-".repeat(60));
213 output.push('\n');
214
215 for s in &report.synthetics {
216 let unit = s.unit.as_deref().unwrap_or("");
217 output.push_str(&format!(
218 " {} = {:.2}{} ({})\n",
219 s.id, s.value, unit, s.formula
220 ));
221 }
222 }
223
224 let active_verifications: Vec<_> = report
226 .verifications
227 .iter()
228 .filter(|v| {
229 !matches!(
230 v.status,
231 fluxbench_logic::VerificationStatus::Skipped { .. }
232 )
233 })
234 .collect();
235
236 if !active_verifications.is_empty() {
237 output.push_str("\nVerifications\n");
238 output.push_str(&"-".repeat(60));
239 output.push('\n');
240
241 for v in active_verifications {
242 let icon = if v.passed() { "✓" } else { "✗" };
243 output.push_str(&format!(" {} {} : {}\n", icon, v.id, v.message));
244 }
245 }
246
247 output.push_str("\nSummary\n");
249 output.push_str(&"-".repeat(60));
250 output.push('\n');
251 output.push_str(&format!(
252 " Total: {} Passed: {} Failed: {} Crashed: {} Skipped: {}\n",
253 report.summary.total_benchmarks,
254 report.summary.passed,
255 report.summary.failed,
256 report.summary.crashed,
257 report.summary.skipped
258 ));
259 output.push_str(&format!(
260 " Duration: {:.2} ms\n",
261 report.summary.total_duration_ms
262 ));
263
264 output
265}