fluxbench_cli/executor/
formatting.rs1use fluxbench_report::{BenchmarkReportResult, BenchmarkStatus, Report, format_duration};
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: {} median: {} stddev: {}\n",
55 format_duration(metrics.mean_ns),
56 format_duration(metrics.median_ns),
57 format_duration(metrics.std_dev_ns),
58 ));
59 output.push_str(&format!(
60 " min: {} max: {} samples: {}\n",
61 format_duration(metrics.min_ns),
62 format_duration(metrics.max_ns),
63 metrics.samples,
64 ));
65 output.push_str(&format!(
66 " p50: {} p95: {} p99: {}\n",
67 format_duration(metrics.p50_ns),
68 format_duration(metrics.p95_ns),
69 format_duration(metrics.p99_ns),
70 ));
71 output.push_str(&format!(
72 " 95% CI: [{}, {}]\n",
73 format_duration(metrics.ci_lower_ns),
74 format_duration(metrics.ci_upper_ns),
75 ));
76 if let Some(throughput) = metrics.throughput_ops_sec {
77 output.push_str(&format!(" throughput: {:.2} ops/sec\n", throughput));
78 }
79 if metrics.alloc_bytes > 0 {
80 output.push_str(&format!(
81 " allocations: {} bytes ({} allocs)\n",
82 metrics.alloc_bytes, metrics.alloc_count
83 ));
84 }
85 if metrics.mean_cycles > 0.0 {
87 output.push_str(&format!(
88 " cycles: mean {:.0} median {:.0} ({:.2} GHz)\n",
89 metrics.mean_cycles, metrics.median_cycles, metrics.cycles_per_ns
90 ));
91 }
92 }
93
94 if let Some(failure) = &result.failure {
95 output.push_str(&format!(" error: {}\n", failure.message));
96 }
97
98 output.push('\n');
99 }
100 }
101
102 for cmp in &report.comparisons {
104 output.push_str(&format!("\n{}\n", cmp.title));
105 output.push_str(&"-".repeat(60));
106 output.push('\n');
107
108 let max_name_len = cmp
110 .entries
111 .iter()
112 .map(|e| e.benchmark_id.len())
113 .max()
114 .unwrap_or(20);
115
116 output.push_str(&format!(
118 " {:<width$} {:>12} {:>10}\n",
119 "Benchmark",
120 cmp.metric,
121 "Speedup",
122 width = max_name_len
123 ));
124 output.push_str(&format!(" {}\n", "-".repeat(max_name_len + 26)));
125
126 let mut sorted_entries: Vec<_> = cmp.entries.iter().collect();
128 sorted_entries.sort_by(|a, b| {
129 b.speedup
130 .partial_cmp(&a.speedup)
131 .unwrap_or(std::cmp::Ordering::Equal)
132 });
133
134 for entry in sorted_entries {
135 let baseline_marker = if entry.is_baseline { " (baseline)" } else { "" };
136 let speedup_str = if entry.is_baseline {
137 "1.00x".to_string()
138 } else {
139 format!("{:.2}x", entry.speedup)
140 };
141
142 output.push_str(&format!(
143 " {:<width$} {:>12} {:>10}{}\n",
144 entry.benchmark_id,
145 format_duration(entry.value),
146 speedup_str,
147 baseline_marker,
148 width = max_name_len
149 ));
150 }
151 }
152
153 for series in &report.comparison_series {
155 output.push_str(&format!("\n{} ({})\n", series.title, series.metric));
156 output.push_str(&"-".repeat(60));
157 output.push('\n');
158
159 let max_name_len = series
161 .series_names
162 .iter()
163 .map(|n| n.len())
164 .max()
165 .unwrap_or(12);
166
167 let col_width = series
169 .x_values
170 .iter()
171 .map(|x| x.len())
172 .max()
173 .unwrap_or(8)
174 .max(10); output.push_str(&format!(" {:<width$}", "", width = max_name_len));
178 for x in &series.x_values {
179 output.push_str(&format!(" | {:>w$}", x, w = col_width));
180 }
181 output.push('\n');
182
183 output.push_str(&format!(" {}", "-".repeat(max_name_len)));
185 for _ in &series.x_values {
186 output.push_str(&format!("-+-{}", "-".repeat(col_width)));
187 }
188 output.push('\n');
189
190 for (series_idx, name) in series.series_names.iter().enumerate() {
192 output.push_str(&format!(" {:<width$}", name, width = max_name_len));
193 for x_idx in 0..series.x_values.len() {
194 let value = series
195 .series_data
196 .get(series_idx)
197 .and_then(|row| row.get(x_idx))
198 .copied()
199 .unwrap_or(0.0);
200 let formatted = if value == 0.0 {
202 "-".to_string()
203 } else if value.abs() >= 1_000_000.0 || (value.abs() < 0.001 && value != 0.0) {
204 format!("{:.2e}", value)
205 } else if value.abs() >= 1000.0 {
206 format!("{:.0}", value)
207 } else {
208 format!("{:.2}", value)
209 };
210 output.push_str(&format!(" | {:>w$}", formatted, w = col_width));
211 }
212 output.push('\n');
213 }
214 }
215
216 if !report.synthetics.is_empty() {
218 output.push_str("\nComputed Metrics\n");
219 output.push_str(&"-".repeat(60));
220 output.push('\n');
221
222 for s in &report.synthetics {
223 let unit = s.unit.as_deref().unwrap_or("");
224 output.push_str(&format!(
225 " {} = {:.2}{} ({})\n",
226 s.id, s.value, unit, s.formula
227 ));
228 }
229 }
230
231 let active_verifications: Vec<_> = report
233 .verifications
234 .iter()
235 .filter(|v| {
236 !matches!(
237 v.status,
238 fluxbench_logic::VerificationStatus::Skipped { .. }
239 )
240 })
241 .collect();
242
243 if !active_verifications.is_empty() {
244 output.push_str("\nVerifications\n");
245 output.push_str(&"-".repeat(60));
246 output.push('\n');
247
248 for v in active_verifications {
249 let icon = if v.passed() { "✓" } else { "✗" };
250 output.push_str(&format!(" {} {} : {}\n", icon, v.id, v.message));
251 }
252 }
253
254 output.push_str("\nSummary\n");
256 output.push_str(&"-".repeat(60));
257 output.push('\n');
258 output.push_str(&format!(
259 " Total: {} Passed: {} Failed: {} Crashed: {} Skipped: {}\n",
260 report.summary.total_benchmarks,
261 report.summary.passed,
262 report.summary.failed,
263 report.summary.crashed,
264 report.summary.skipped
265 ));
266 output.push_str(&format!(
267 " Duration: {:.2} ms\n",
268 report.summary.total_duration_ms
269 ));
270
271 output
272}