mirror of
https://github.com/sharkdp/hyperfine.git
synced 2026-11-02 09:01:25 +00:00
Add support for showing other metrics in the terminal
This commit is contained in:
1 parent
7395dde05d
commit
908b33cc1d
31 files changed
+1514
-1898
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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."
|
||||
}
|
||||
),
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user