Skip to main content

fluxbench_cli/executor/
formatting.rs

1//! Output Formatting
2//!
3//! Human-readable output formatting for benchmark reports.
4//!
5//! Generates terminal-friendly output with:
6//! - Grouped benchmark results with status icons (✓/✗/💥/⊘)
7//! - Timing metrics (mean, median, stddev, percentiles)
8//! - Confidence intervals and throughput
9//! - Allocation and CPU cycle statistics
10//! - Comparison tables with speedup calculations
11//! - Verification results summary
12
13use fluxbench_report::{BenchmarkReportResult, BenchmarkStatus, Report};
14
15/// Format a report for human-readable terminal display
16///
17/// # Arguments
18/// * `report` - Complete benchmark report
19///
20/// # Returns
21/// Formatted string suitable for terminal output
22pub 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    // Group results
31    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                // Show CPU cycles if available (x86_64 only)
79                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    // Comparisons
96    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        // Find max benchmark name length for alignment
102        let max_name_len = cmp
103            .entries
104            .iter()
105            .map(|e| e.benchmark_id.len())
106            .max()
107            .unwrap_or(20);
108
109        // Header
110        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        // Entries sorted by speedup (fastest first)
120        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    // Comparison Series (grouped multi-point comparisons for charts)
147    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        // Find max series name length for alignment
153        let max_name_len = series
154            .series_names
155            .iter()
156            .map(|n| n.len())
157            .max()
158            .unwrap_or(12);
159
160        // Determine column width based on x values and data
161        let col_width = series
162            .x_values
163            .iter()
164            .map(|x| x.len())
165            .max()
166            .unwrap_or(8)
167            .max(10); // At least 10 chars for numbers
168
169        // Header row with x values
170        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        // Separator
177        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        // Data rows
184        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                // Format nicely: use scientific notation for very large/small numbers
194                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    // Computed Metrics (Synthetics)
210    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    // Verifications (only show if there are non-skipped ones)
225    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    // Summary
248    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}