Add support for showing other metrics in the terminal

This commit is contained in:
David Peter committed 2026-10-06 12:53:42 +02:00
1 parent 7395dde05d
commit 908b33cc1d
31 files changed
+1514 -1898

No files matched your search

+1
View File
@@ -1,4 +1,5 @@
pub mod progress_bar;
pub mod report;
pub mod warnings;
/// Mark console write errors so broken pipes can be handled separately from file errors.
+22 -15
View File
@@ -16,29 +16,36 @@ const DEFAULT_TEMPLATE: &str = " {spinner} {msg:<30} {wide_bar} ETA {eta_precise
const INITIAL_TEMPLATE: &str =
" {spinner} Initial run: {initial_elapsed:<17} {wide_bar} ETA {eta_precise} ";
fn style_with_template(bar: &ProgressBar, template: &str) -> ProgressStyle {
let template = if bar.prefix().is_empty() {
template.to_owned()
} else {
format!("{{prefix}}\n{template}")
};
bar.style().template(&template).expect("no template error")
}
/// Show the benchmark heading above the progress bar, clearing both on completion.
pub fn set_benchmark_header(bar: &ProgressBar, header: String) {
bar.set_prefix(header);
bar.set_style(style_with_template(bar, DEFAULT_TEMPLATE));
}
/// Show the time elapsed since the initial benchmark command started.
pub fn start_initial_measurement(bar: &ProgressBar, started: Instant) {
let style = bar
.style()
.template(INITIAL_TEMPLATE)
.expect("no template error")
.with_key(
"initial_elapsed",
move |_: &ProgressState, w: &mut dyn fmt::Write| {
write!(w, "{}", FormattedDuration(started.elapsed())).unwrap();
},
);
let style = style_with_template(bar, INITIAL_TEMPLATE).with_key(
"initial_elapsed",
move |_: &ProgressState, w: &mut dyn fmt::Write| {
write!(w, "{}", FormattedDuration(started.elapsed())).unwrap();
},
);
bar.set_style(style);
}
/// Stop showing the initial timer and display the next phase's message.
pub fn finish_initial_measurement(bar: &ProgressBar, message: String) {
bar.set_message(message);
bar.set_style(
bar.style()
.template(DEFAULT_TEMPLATE)
.expect("no template error"),
);
bar.set_style(style_with_template(bar, DEFAULT_TEMPLATE));
}
/// Return a pre-configured progress bar
+190
View File
@@ -0,0 +1,190 @@
use std::io::{self, Write};
use anyhow::{ensure, Result};
use colored::Colorize;
use crate::benchmark::{benchmark_result::BenchmarkResult, measurement::Measurements};
use crate::metric::MetricSelection;
use crate::output::console_writeln;
// Reserve enough space for typical values and every supported unit suffix so
// independently printed benchmarks keep the same layout.
const MIN_VALUE_WIDTH: usize = 7;
const MIN_UNIT_WIDTH: usize = 3;
const MIN_CHANGE_WIDTH: usize = 12;
/// Print a completed benchmark immediately, comparing every metric with benchmark 1.
pub fn print(
number: usize,
name: &str,
measurements: &Measurements,
metrics: &[MetricSelection],
skip_unavailable_metrics: bool,
reference: Option<&BenchmarkResult>,
) -> Result<()> {
let comparison = reference.is_some();
let multiple_runs = measurements.len() > 1;
let value_columns = 1..if multiple_runs { 5 } else { 2 };
let mut header = vec![
String::new(),
"mean".to_owned(),
"σ".to_owned(),
"min".to_owned(),
"max".to_owned(),
];
if !multiple_runs {
header.truncate(2);
header[1] = "Value".to_owned();
}
if comparison {
header.push(String::new());
}
let mut rows = vec![(header, "")];
for selection in metrics {
let Some(summary) = selection.metric.summarize(measurements) else {
ensure!(
skip_unavailable_metrics,
"Metric '{}' is unavailable for '{name}'",
selection.metric.name()
);
continue;
};
let baseline = reference.and_then(|r| selection.metric.summarize(&r.measurements));
// Keep auto units consistent with the first command for easier comparison.
let scale_mean = baseline
.as_ref()
.filter(|stats| stats.mean != 0.0)
.unwrap_or(&summary)
.mean;
let unit = selection.display_unit(scale_mean);
let mut row = vec![
selection.metric.label().to_owned(),
unit.format_value(summary.mean),
];
if let Some(stddev) = summary.stddev {
row.push(unit.format_value(stddev));
row.push(unit.format_value(summary.min));
row.push(unit.format_value(summary.max));
}
if comparison {
row.push(baseline.as_ref().map_or_else(
|| "N/A".to_owned(),
|baseline| summary.format_change_from(baseline),
));
}
rows.push((
row,
if unit.symbol == "count" {
""
} else {
unit.symbol
},
));
}
// Align numbers and unit suffixes independently so mixed units do not shift
// the values or the ± and … separators.
let unit_width = rows
.iter()
.map(|(_, unit)| unit.chars().count())
.max()
.unwrap()
.max(MIN_UNIT_WIDTH);
// Keep labels aligned across benchmarks even when some metrics are skipped.
let metric_width = metrics
.iter()
.map(|selection| selection.metric.label().chars().count())
.max()
.unwrap_or(0);
let widths: Vec<_> = (0..rows[0].0.len())
.map(|column| {
let width = rows
.iter()
.map(|(row, _)| row[column].chars().count())
.max()
.unwrap();
if column == 0 {
width.max(metric_width)
} else if value_columns.contains(&column) {
width.max(MIN_VALUE_WIDTH)
} else {
width.max(MIN_CHANGE_WIDTH)
}
})
.collect();
let mut stdout = io::stdout().lock();
console_writeln!(
stdout,
"{}: {} {}",
format!("Benchmark {}", number + 1).bold(),
name.white().bold(),
format!(
"({} {})",
measurements.len(),
if measurements.len() == 1 {
"run"
} else {
"runs"
}
)
.dimmed()
)?;
for (index, (row, unit)) in rows.iter().enumerate() {
let mut line = String::new();
for (column, value) in row.iter().enumerate() {
let is_change = comparison && column == row.len() - 1;
if index == 0 && is_change {
break;
}
if column > 0 {
line.push_str(match (multiple_runs, column) {
(true, 2) => " ± ",
(true, 4) => " … ",
_ => " ",
});
}
let padding = " ".repeat(widths[column] - value.chars().count());
let has_unit = value_columns.contains(&column);
let text = if has_unit && !unit.is_empty() {
format!("{value} {unit}")
} else {
value.clone()
};
let styled = if is_change {
if value.starts_with('-') {
text.green().to_string()
} else if value.starts_with('+') {
text.red().to_string()
} else {
text.normal().to_string()
}
} else if column == 1 {
if index == 1 {
text.green().bold().to_string()
} else {
text.green().to_string()
}
} else if column == 2 {
text.green().to_string()
} else if column == 3 {
text.cyan().to_string()
} else if column == 4 {
text.purple().to_string()
} else {
text.normal().to_string()
};
if column == 0 {
line.push_str(&format!("{styled}{padding}"));
} else {
line.push_str(&format!("{padding}{styled}"));
}
if has_unit && unit_width > 0 {
// Empty suffixes (headers and unscaled counters) also need the
// space normally separating a number from its unit.
let unit_padding = unit_width - unit.chars().count() + usize::from(unit.is_empty());
line.push_str(&" ".repeat(unit_padding));
}
}
console_writeln!(stdout, " {}", line.trim_end())?;
}
Ok(())
}
+6 -15
View File
@@ -1,7 +1,8 @@
use std::fmt;
use crate::benchmark::MIN_EXECUTION_TIME;
use crate::quantity::{FormatQuantity, Time, TimeUnit};
use crate::metric::MetricSelection;
use crate::quantity::{millisecond, second, Time};
pub struct OutlierWarningOptions {
pub warmup_in_use: bool,
@@ -13,7 +14,6 @@ pub enum Warnings {
FastExecutionTime,
NonZeroExitCode,
SlowInitialRun(Time, OutlierWarningOptions),
OutliersDetected(OutlierWarningOptions),
}
impl fmt::Display for Warnings {
@@ -21,11 +21,11 @@ impl fmt::Display for Warnings {
match *self {
Warnings::FastExecutionTime => write!(
f,
"Command took less than {} ms to complete. Note that the results might be \
"Command took less than {} ms to complete. Note that the wall-clock measurements might be \
inaccurate because hyperfine can not calibrate the shell startup time much \
more precise than this limit. You can try to use the `-N`/`--shell=none` \
option to disable the shell completely.",
MIN_EXECUTION_TIME.format_with_precision(TimeUnit::MilliSecond, 0)
MIN_EXECUTION_TIME.get::<millisecond>()
),
Warnings::NonZeroExitCode => write!(f, "Ignoring non-zero exit code."),
Warnings::SlowInitialRun(time_first_run, ref options) => write!(
@@ -33,7 +33,7 @@ impl fmt::Display for Warnings {
"The first benchmarking run for this command was significantly slower than the \
rest ({time}). This could be caused by (filesystem) caches that were not filled until \
after the first run. {hints}",
time=time_first_run.format_auto(),
time=MetricSelection::default().display_unit(time_first_run.get::<second>()).format(time_first_run.get::<second>()),
hints=match (options.warmup_in_use, options.prepare_in_use) {
(true, true) => "You are already using both the '--warmup' option as well \
as the '--prepare' option. Consider re-running the benchmark on a quiet system. \
@@ -53,16 +53,7 @@ impl fmt::Display for Warnings {
option to clear the caches before each timing run."
}
),
Warnings::OutliersDetected(ref options) => write!(
f,
"Statistical outliers were detected. Consider re-running this benchmark on a quiet \
system without any interferences from other programs.{hint}",
hint=if options.warmup_in_use && options.prepare_in_use {
""
} else {
" It might help to use the '--warmup' or '--prepare' options."
}
),
}
}
}