use std::fs;
use std::io;
use colored::{Color, ColoredString, Colorize};
use crate::measure::Accounting;
use crate::stats::BenchResult;
pub fn format_duration_secs(secs: f64) -> String {
if secs < 1.0 {
format!("{:.1} ms", secs * 1000.0)
} else {
format!("{secs:.2} s")
}
}
pub fn format_bytes(bytes: f64) -> String {
const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
let mut value = bytes;
let mut unit = 0;
while value >= 1024.0 && unit < UNITS.len() - 1 {
value /= 1024.0;
unit += 1;
}
format!("{value:.2} {}", UNITS[unit])
}
fn accounting_note(accounting: Accounting) -> &'static str {
match accounting {
Accounting::Cgroup => "",
Accounting::Sampled => " ~",
}
}
fn pad(text: &str, width: usize) -> String {
format!("{text:<width$}")
}
pub fn print_single(result: &BenchResult) {
let note = accounting_note(result.accounting());
println!("{} {}", "Benchmark".dimmed(), result.command.bold().cyan());
println!();
metric_row(
"Time",
format!(
"{} {} {}",
format_duration_secs(result.mean_wall_secs()).bold().green(),
"\u{b1}".dimmed(),
format_duration_secs(result.stddev_wall_secs()).dimmed()
),
);
metric_row("CPU", format!("{:.1} %", result.mean_cpu_percent()).bold());
metric_row(
"CPU time",
format_duration_secs(result.mean_cpu_secs()).bold(),
);
metric_row(
"Peak memory",
with_note(format_bytes(result.mean_peak_memory_bytes()), note),
);
metric_row(
"Disk read",
with_note(format_bytes(result.mean_disk_read_bytes()), note),
);
metric_row(
"Disk write",
with_note(format_bytes(result.mean_disk_write_bytes()), note),
);
metric_row(
"Processes",
format!("{:.0}", result.mean_max_processes()).bold(),
);
metric_row(
"Threads",
format!("{:.0}", result.mean_max_threads()).bold(),
);
println!();
println!(" {}", format!("{} runs", result.runs.len()).dimmed());
if !note.is_empty() {
println!("\n{}", sampling_footnote());
}
}
fn with_note(value: String, note: &str) -> ColoredString {
if note.is_empty() {
value.bold()
} else {
format!("{value}{}", note.yellow()).bold()
}
}
fn metric_row(label: &str, value: impl std::fmt::Display) {
println!(" {} {value}", pad(label, 13).dimmed());
}
fn sampling_footnote() -> ColoredString {
"~ estimated via sampling (no cgroup v2 delegation available)"
.italic()
.dimmed()
}
const LABEL_WIDTH: usize = 14;
pub fn print_comparison(results: &[BenchResult]) {
let fastest_idx = results
.iter()
.enumerate()
.min_by(|a, b| a.1.mean_wall_secs().total_cmp(&b.1.mean_wall_secs()))
.map(|(i, _)| i)
.unwrap_or(0);
let col_width = results
.iter()
.map(|r| r.command.len())
.max()
.unwrap_or(10)
.max(12)
+ 3;
print!("{}", pad("", LABEL_WIDTH));
for r in results {
print!("{}", pad(&r.command, col_width).bold());
}
println!();
println!(
"{}",
"\u{2500}"
.repeat(LABEL_WIDTH + col_width * results.len())
.dimmed()
);
print_row(results, col_width, "Time", Some(true), |r| {
(r.mean_wall_secs(), format_duration_secs(r.mean_wall_secs()))
});
print_row(results, col_width, "CPU", None, |r| {
(
r.mean_cpu_percent(),
format!("{:.1} %", r.mean_cpu_percent()),
)
});
print_row(results, col_width, "CPU time", Some(true), |r| {
(r.mean_cpu_secs(), format_duration_secs(r.mean_cpu_secs()))
});
print_row(results, col_width, "Peak memory", Some(true), |r| {
let value = r.mean_peak_memory_bytes();
(
value,
format!("{}{}", format_bytes(value), accounting_note(r.accounting())),
)
});
print_row(results, col_width, "Disk read", Some(true), |r| {
let value = r.mean_disk_read_bytes();
(
value,
format!("{}{}", format_bytes(value), accounting_note(r.accounting())),
)
});
print_row(results, col_width, "Disk write", Some(true), |r| {
let value = r.mean_disk_write_bytes();
(
value,
format!("{}{}", format_bytes(value), accounting_note(r.accounting())),
)
});
print_row(results, col_width, "Processes", Some(true), |r| {
let value = r.mean_max_processes();
(value, format!("{value:.0}"))
});
let fastest = &results[fastest_idx];
println!();
println!(
"{} {}",
"\u{2713}".bold().green(),
fastest.command.bold().green()
);
for (i, r) in results.iter().enumerate() {
if i == fastest_idx {
continue;
}
let speed_ratio = r.mean_wall_secs() / fastest.mean_wall_secs().max(f64::EPSILON);
println!(
" {} than {}",
format!("{speed_ratio:.2}\u{d7} faster").bold().green(),
r.command.dimmed()
);
print_delta_line(
"memory",
fastest.mean_peak_memory_bytes(),
r.mean_peak_memory_bytes(),
);
print_delta_line(
"disk reads",
fastest.mean_disk_read_bytes(),
r.mean_disk_read_bytes(),
);
print_delta_line(
"disk writes",
fastest.mean_disk_write_bytes(),
r.mean_disk_write_bytes(),
);
}
if results
.iter()
.any(|r| r.accounting() == Accounting::Sampled)
{
println!("\n{}", sampling_footnote());
}
}
fn print_row(
results: &[BenchResult],
col_width: usize,
label: &str,
highlight: Option<bool>,
value_and_text: impl Fn(&BenchResult) -> (f64, String),
) {
let cells: Vec<(f64, String)> = results.iter().map(&value_and_text).collect();
let winner = highlight.and_then(|lower_is_better| winning_index(&cells, lower_is_better));
print!("{}", pad(label, LABEL_WIDTH).dimmed());
for (i, (_, text)) in cells.iter().enumerate() {
let cell = pad(text, col_width);
if winner == Some(i) {
print!("{}", cell.bold().on_truecolor(20, 70, 40));
} else {
print!("{cell}");
}
}
println!();
}
fn winning_index(cells: &[(f64, String)], lower_is_better: bool) -> Option<usize> {
let (min, max) = cells
.iter()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), (v, _)| {
(lo.min(*v), hi.max(*v))
});
if cells.len() < 2 || min >= max {
return None;
}
let target = if lower_is_better { min } else { max };
cells.iter().position(|(v, _)| *v == target)
}
fn print_delta_line(label: &str, ours: f64, theirs: f64) {
let (text, color) = describe_delta(label, ours, theirs);
println!(" {}", text.color(color));
}
fn describe_delta(label: &str, ours: f64, theirs: f64) -> (String, Color) {
if ours <= 0.0 && theirs <= 0.0 {
return (format!("same {label}"), Color::White);
}
if theirs <= 0.0 {
return (format!("uses {label}, baseline used none"), Color::Yellow);
}
if ours <= theirs {
let percent = 100.0 * (1.0 - ours / theirs);
(format!("{percent:.0}% less {label}"), Color::Green)
} else {
(
format!("{:.2}\u{d7} more {label}", ours / theirs),
Color::Yellow,
)
}
}
pub fn export_json(results: &[BenchResult], path: &str) -> io::Result<()> {
let commands: Vec<_> = results
.iter()
.map(|r| {
serde_json::json!({
"command": r.command,
"runs": r.runs.len(),
"accounting": match r.accounting() {
Accounting::Cgroup => "cgroup",
Accounting::Sampled => "sampled",
},
"mean_wall_time_secs": r.mean_wall_secs(),
"stddev_wall_time_secs": r.stddev_wall_secs(),
"mean_cpu_time_secs": r.mean_cpu_secs(),
"mean_cpu_percent": r.mean_cpu_percent(),
"mean_peak_memory_bytes": r.mean_peak_memory_bytes(),
"mean_disk_read_bytes": r.mean_disk_read_bytes(),
"mean_disk_write_bytes": r.mean_disk_write_bytes(),
"mean_max_processes": r.mean_max_processes(),
"mean_max_threads": r.mean_max_threads(),
})
})
.collect();
let value = serde_json::json!({ "results": commands });
fs::write(path, serde_json::to_string_pretty(&value)?)
}
pub fn export_csv(results: &[BenchResult], path: &str) -> io::Result<()> {
let mut out = String::from(
"command,runs,accounting,mean_wall_time_secs,stddev_wall_time_secs,mean_cpu_time_secs,mean_cpu_percent,mean_peak_memory_bytes,mean_disk_read_bytes,mean_disk_write_bytes,mean_max_processes,mean_max_threads\n",
);
for r in results {
out.push_str(&format!(
"{},{},{},{},{},{},{},{},{},{},{},{}\n",
csv_escape(&r.command),
r.runs.len(),
match r.accounting() {
Accounting::Cgroup => "cgroup",
Accounting::Sampled => "sampled",
},
r.mean_wall_secs(),
r.stddev_wall_secs(),
r.mean_cpu_secs(),
r.mean_cpu_percent(),
r.mean_peak_memory_bytes(),
r.mean_disk_read_bytes(),
r.mean_disk_write_bytes(),
r.mean_max_processes(),
r.mean_max_threads(),
));
}
fs::write(path, out)
}
fn csv_escape(value: &str) -> String {
if value.contains(',') || value.contains('"') || value.contains('\n') {
format!("\"{}\"", value.replace('"', "\"\""))
} else {
value.to_string()
}
}
pub fn export_markdown(results: &[BenchResult], path: &str) -> io::Result<()> {
let mut out = String::from(
"| Command | Time | CPU | CPU time | Peak memory | Disk read | Disk write | Processes |\n|---|---|---|---|---|---|---|---|\n",
);
for r in results {
out.push_str(&format!(
"| {} | {} \u{b1} {} | {:.1}% | {} | {}{} | {}{} | {}{} | {:.0} |\n",
r.command,
format_duration_secs(r.mean_wall_secs()),
format_duration_secs(r.stddev_wall_secs()),
r.mean_cpu_percent(),
format_duration_secs(r.mean_cpu_secs()),
format_bytes(r.mean_peak_memory_bytes()),
accounting_note(r.accounting()),
format_bytes(r.mean_disk_read_bytes()),
accounting_note(r.accounting()),
format_bytes(r.mean_disk_write_bytes()),
accounting_note(r.accounting()),
r.mean_max_processes(),
));
}
fs::write(path, out)
}