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, format_duration};
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: {}  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                // Show CPU cycles if available (x86_64 only)
86                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    // Comparisons
103    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        // Find max benchmark name length for alignment
109        let max_name_len = cmp
110            .entries
111            .iter()
112            .map(|e| e.benchmark_id.len())
113            .max()
114            .unwrap_or(20);
115
116        // Header
117        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        // Entries sorted by speedup (fastest first)
127        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    // Comparison Series (grouped multi-point comparisons for charts)
154    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        // Find max series name length for alignment
160        let max_name_len = series
161            .series_names
162            .iter()
163            .map(|n| n.len())
164            .max()
165            .unwrap_or(12);
166
167        // Determine column width based on x values and data
168        let col_width = series
169            .x_values
170            .iter()
171            .map(|x| x.len())
172            .max()
173            .unwrap_or(8)
174            .max(10); // At least 10 chars for numbers
175
176        // Header row with x values
177        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        // Separator
184        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        // Data rows
191        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                // Format nicely: use scientific notation for very large/small numbers
201                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    // Computed Metrics (Synthetics)
217    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    // Verifications (only show if there are non-skipped ones)
232    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    // Summary
255    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}