Compare commits

...
Author SHA1 Message Date
David Peter aba7bdec07 Use C++ compiler drivers in parameter-list examples 2026-10-02 11:29:13 +02:00
Marshall Porter 1f5a2ff70c ci: add Linux ARM64 musl release target (#924) 2026-10-02 11:26:01 +02:00
c0b3834277 Handle broken pipe errors on stdout gracefully (#932)
Replace `println!` calls on the results paths with `writeln!` on
`io::stdout()` and propagate the `io::Error` to `main`, where
`ErrorKind::BrokenPipe` is turned into a quiet `exit(0)`. This matches
the conventional behavior of command-line tools when their output pipe
is closed early (e.g. `hyperfine ... | head -n 0`), and fixes panics in
all run-output paths: the per-benchmark header and results, the speed
comparison summary, and `-`-style exports to stdout. The warnings and
"Note" outputs to stderr are converted the same way.


---------

Co-authored-by: Mathjk <Mathjk@users.noreply.github.com>
Co-authored-by: David Peter <mail@david-peter.de>
2026-10-02 11:18:56 +02:00
David Peter 4d073e08ae Dependency and MSRV update (#934) 2026-10-02 11:05:02 +02:00
David Peter 4b561defef Use cargo_bin! to locate the integration test binary (#933) 2026-10-02 10:54:51 +02:00
AlexandDavid Peter 27a079e0df docs: load aliases before parsing their benchmark invocation (#929)
* docs: load aliases before parsing their benchmark invocation

* Remove the alias example

---------

Co-authored-by: David Peter <mail@david-peter.de>
2026-10-02 10:50:46 +02:00
David Peter 148632c5f9 Remove shell alias example from README 2026-10-02 10:45:59 +02:00
Kobi Hikri 09f02e4f59 docs: update stale version metadata (README.md) 2026-10-02 10:42:06 +02:00
Kobi Hikri c30ac680a3 docs: update stale version metadata (CITATION.cff) 2026-10-02 10:42:06 +02:00
David Peter f12f3d9f86 Remove sponsors 2026-04-30 10:23:22 +02:00
David Peter 327d5f4d91 Update README 2026-02-14 14:01:52 +01:00
22 changed files with 449 additions and 568 deletions

No files matched your search

+1
View File
@@ -79,6 +79,7 @@ jobs:
matrix:
job:
- { target: aarch64-unknown-linux-gnu , os: ubuntu-24.04, use-cross: true }
- { target: aarch64-unknown-linux-musl , os: ubuntu-24.04, use-cross: true }
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-24.04, use-cross: true }
- { target: arm-unknown-linux-musleabihf, os: ubuntu-24.04, use-cross: true }
- { target: i686-pc-windows-msvc , os: windows-2022 }
+1 -1
View File
@@ -247,7 +247,7 @@ Package authors: note that Hyperfine now comes with a set of shell completion fi
necessarily required numeric arguments.
``` bash
hyperfine --parameter-list compiler "gcc,clang" \
hyperfine --parameter-list compiler "g++,clang++" \
"{compiler} -O2 main.cpp"
```
+2 -2
View File
@@ -13,5 +13,5 @@ authors:
repository-code: 'https://github.com/sharkdp/hyperfine'
abstract: A command-line benchmarking tool.
license: MIT
version: 1.16.1
date-released: '2023-03-21'
version: 1.20.0
date-released: '2025-11-18'
Generated
+295 -464
View File
File diff suppressed because it is too large. Load diff
+15 -14
View File
@@ -10,22 +10,23 @@ repository = "https://github.com/sharkdp/hyperfine"
version = "1.20.0"
edition = "2018"
build = "build.rs"
rust-version = "1.88.0"
rust-version = "1.97.0"
[features]
# Use the nightly feature windows_process_extensions_main_thread_handle
windows_process_extensions_main_thread_handle = []
[dependencies]
colored = "2.1"
colored = "3.1"
# Newer versions regress ETA estimates: https://github.com/console-rs/indicatif/issues/580
indicatif = "=0.17.4"
statistical = "1.0"
csv = "1.3"
csv = "1.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rust_decimal = "1.36"
rand = "0.8"
shell-words = "1.0"
rust_decimal = "1.43"
rand = "0.10"
shell-words = "1.1"
thiserror = "2.0"
anyhow = "1.0"
@@ -33,7 +34,7 @@ anyhow = "1.0"
libc = "0.2"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [
windows-sys = { version = "0.61", features = [
"Win32_Foundation",
"Win32_Security",
"Win32_System_JobObjects",
@@ -42,10 +43,10 @@ windows-sys = { version = "0.59", features = [
] }
[target.'cfg(all(windows, not(windows_process_extensions_main_thread_handle)))'.dependencies]
once_cell = "1.19"
once_cell = "1.21"
[target.'cfg(target_os="linux")'.dependencies]
nix = { version = "0.29", features = ["zerocopy"] }
nix = { version = "0.31", features = ["zerocopy"] }
[dependencies.clap]
version = "4"
@@ -62,18 +63,18 @@ features = [
[dev-dependencies]
approx = "0.5"
assert_cmd = "2.0"
insta = { version = "1.41.1", features = ["yaml"] }
assert_cmd = "2.2"
insta = { version = "1.48", features = ["yaml"] }
predicates = "3.1"
tempfile = "3.23"
tempfile = "3.27"
[profile.dev.package]
insta.opt-level = 3
similar.opt-level = 3
[build-dependencies]
clap = "4.5.48"
clap_complete = "4.2.1"
clap = "4.6"
clap_complete = "4.6"
[profile.release]
lto = true
+4 -30
View File
@@ -10,29 +10,6 @@ A command-line benchmarking tool.
![hyperfine](https://i.imgur.com/z19OYxE.gif)
### Sponsors
A special *thank you* goes to our biggest <a href="doc/sponsors.md">sponsors</a>:<br>
<a href="https://www.warp.dev/hyperfine">
<img src="doc/sponsors/warp-logo.png" width="200" alt="Warp">
<br>
<strong>Warp, the intelligent terminal</strong>
<br>
<sub>Available on MacOS, Linux, Windows</sub>
</a>
<br>
<br>
<a href="https://tuple.app/hyperfine">
<img src="doc/sponsors/tuple-logo.png" width="200" alt="Tuple">
<br>
<strong>Tuple, the premier screen sharing app for developers</strong>
<br>
<sub>Available for MacOS &amp; Windows</sub>
</a>
## Features
* Statistical analysis across multiple runs.
@@ -107,7 +84,7 @@ This runs `sleep 0.3`, `sleep 0.5` and `sleep 0.7`.
For non-numeric parameters, you can also supply a list of values with the `-L`/`--parameter-list`
option:
```
hyperfine -L compiler gcc,clang '{compiler} -O2 main.cpp'
hyperfine -L compiler g++,clang++ '{compiler} -O2 main.cpp'
```
### Intermediate shell
@@ -131,7 +108,7 @@ hyperfine -N 'grep TODO /home/user'
```
### Shell functions and aliases
### Shell functions
If you are using bash, you can export shell functions to directly benchmark them with hyperfine:
@@ -141,13 +118,10 @@ export -f my_function
hyperfine --shell=bash my_function
```
Otherwise, inline them into or source them from the benchmarked program:
Otherwise, inline the function into the benchmarked command:
```sh
hyperfine 'my_function() { sleep 1; }; my_function'
echo 'alias my_alias="sleep 1"' > /tmp/my_alias.sh
hyperfine '. /tmp/my_alias.sh; my_alias'
```
### Exporting results
@@ -328,7 +302,7 @@ Hyperfine can be installed from source via [cargo](https://doc.rust-lang.org/car
cargo install --locked hyperfine
```
Make sure that you use Rust 1.76 or newer.
Make sure that you use Rust 1.97 or newer.
### From binaries (Linux, macOS, Windows)
+2 -2
View File
@@ -184,11 +184,11 @@ Replaces the string '{\fIVAR\fP}' in each command by the current parameter value
.RS
Example:
.RS
\fBhyperfine\fR \fB\-L\fR compiler gcc,clang '{compiler} \-O2 main.cpp'
\fBhyperfine\fR \fB\-L\fR compiler g++,clang++ '{compiler} \-O2 main.cpp'
.RE
.RE
.IP
This performs benchmarks for 'gcc \-O2 main.cpp' and 'clang \-O2 main.cpp'.
This performs benchmarks for 'g++ \-O2 main.cpp' and 'clang++ \-O2 main.cpp'.
.IP
The option can be specified multiple times to run benchmarks for all
possible parameter combinations.
-14
View File
@@ -1,14 +0,0 @@
## Sponsors
`hyperfine` development is sponsored by many individuals and companies. Thank you very much!
Please note, that being sponsored does not affect the individuality of the `hyperfine`
project or affect the maintainers' actions in any way.
We remain impartial and continue to assess pull requests solely on merit - the
features added, bugs solved, and effect on the overall complexity of the code.
No issue will have a different priority based on sponsorship status of the
reporter.
Contributions from anybody are most welcomed.
If you want to see our biggest sponsors, check the top of [`README.md`](../README.md#sponsors).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

+19 -11
View File
@@ -5,6 +5,7 @@ pub mod scheduler;
pub mod timing_result;
use std::cmp;
use std::io::{self, Write};
use crate::benchmark::executor::BenchmarkIteration;
use crate::command::Command;
@@ -12,6 +13,7 @@ use crate::options::{
CmdFailureAction, CommandOutputPolicy, ExecutorKind, Options, OutputStyleOption,
};
use crate::outlier_detection::{modified_zscores, OUTLIER_THRESHOLD};
use crate::output::console_writeln;
use crate::output::format::{format_duration, format_duration_unit};
use crate::output::progress_bar::get_progress_bar;
use crate::output::warnings::{OutlierWarningOptions, Warnings};
@@ -140,12 +142,13 @@ impl<'a> Benchmark<'a> {
/// Run the benchmark for a single command
pub fn run(&self) -> Result<BenchmarkResult> {
if self.options.output_style != OutputStyleOption::Disabled {
println!(
console_writeln!(
io::stdout(),
"{}{}: {}",
"Benchmark ".bold(),
(self.number + 1).to_string().bold(),
self.command.get_name_with_unused_parameters(),
);
)?;
}
let mut times_real: Vec<Second> = vec![];
@@ -358,19 +361,22 @@ impl<'a> Benchmark<'a> {
let system_str = format_duration(system_mean, Some(time_unit));
if self.options.output_style != OutputStyleOption::Disabled {
let mut stdout = io::stdout().lock();
if times_real.len() == 1 {
println!(
console_writeln!(
stdout,
" Time ({} ≡): {:>8} {:>8} [User: {}, System: {}]",
"abs".green().bold(),
mean_str.green().bold(),
" ", // alignment
user_str.blue(),
system_str.blue()
);
)?;
} else {
let stddev_str = format_duration(t_stddev.unwrap(), Some(time_unit));
println!(
console_writeln!(
stdout,
" Time ({} ± {}): {:>8} ± {:>8} [User: {}, System: {}]",
"mean".green().bold(),
"σ".green(),
@@ -378,16 +384,17 @@ impl<'a> Benchmark<'a> {
stddev_str.green(),
user_str.blue(),
system_str.blue()
);
)?;
println!(
console_writeln!(
stdout,
" Range ({} … {}): {:>8} … {:>8} {}",
"min".cyan(),
"max".purple(),
min_str.cyan(),
max_str.purple(),
num_str.dimmed()
);
)?;
}
}
@@ -430,15 +437,16 @@ impl<'a> Benchmark<'a> {
}
if !warnings.is_empty() {
eprintln!(" ");
let mut stderr = io::stderr().lock();
console_writeln!(stderr, " ")?;
for warning in &warnings {
eprintln!(" {}: {}", "Warning".yellow(), warning);
console_writeln!(stderr, " {}: {}", "Warning".yellow(), warning)?;
}
}
if self.options.output_style != OutputStyleOption::Disabled {
println!(" ");
console_writeln!(io::stdout(), " ")?;
}
self.run_cleanup_command(self.command.get_parameters().iter().cloned(), output_policy)?;
+24 -15
View File
@@ -3,10 +3,12 @@ use super::executor::{Executor, MockExecutor, RawExecutor, ShellExecutor};
use super::{relative_speed, Benchmark};
use colored::*;
use std::cmp::Ordering;
use std::io::{self, Write};
use crate::command::{Command, Commands};
use crate::export::ExportManager;
use crate::options::{ExecutorKind, Options, OutputStyleOption, SortOrder};
use crate::output::console_writeln;
use anyhow::Result;
@@ -58,13 +60,13 @@ impl<'a> Scheduler<'a> {
Ok(())
}
pub fn print_relative_speed_comparison(&self) {
pub fn print_relative_speed_comparison(&self) -> Result<()> {
if self.options.output_style == OutputStyleOption::Disabled {
return;
return Ok(());
}
if self.results.len() < 2 {
return;
return Ok(());
}
let reference = self
@@ -79,17 +81,19 @@ impl<'a> Scheduler<'a> {
reference,
self.options.sort_order_speed_comparison,
) {
let mut stdout = io::stdout().lock();
match self.options.sort_order_speed_comparison {
SortOrder::MeanTime => {
println!("{}", "Summary".bold());
console_writeln!(stdout, "{}", "Summary".bold())?;
let reference = annotated_results.iter().find(|r| r.is_reference).unwrap();
let others = annotated_results.iter().filter(|r| !r.is_reference);
println!(
console_writeln!(
stdout,
" {} ran",
reference.result.command_with_unused_parameters.cyan()
);
)?;
for item in others {
let stddev = if let Some(stddev) = item.relative_speed_stddev {
@@ -114,18 +118,20 @@ impl<'a> Scheduler<'a> {
stddev
),
};
println!(
console_writeln!(
stdout,
"{} {}",
comparator,
&item.result.command_with_unused_parameters.magenta()
);
item.result.command_with_unused_parameters.magenta()
)?;
}
}
SortOrder::Command => {
println!("{}", "Relative speed comparison".bold());
console_writeln!(stdout, "{}", "Relative speed comparison".bold())?;
for item in annotated_results {
println!(
console_writeln!(
stdout,
" {}{} {}",
format!("{:10.2}", item.relative_speed).bold().green(),
if item.is_reference {
@@ -135,13 +141,14 @@ impl<'a> Scheduler<'a> {
} else {
" ".into()
},
&item.result.command_with_unused_parameters,
);
item.result.command_with_unused_parameters,
)?;
}
}
}
} else {
eprintln!(
console_writeln!(
io::stderr(),
"{}: The benchmark comparison could not be computed as some benchmark times are zero. \
This could be caused by background interference during the initial calibration phase \
of hyperfine, in combination with very fast commands (faster than a few milliseconds). \
@@ -149,8 +156,10 @@ impl<'a> Scheduler<'a> {
--shell=none/-N option. If it does not help either, you command is most likely too fast \
to be accurately benchmarked by hyperfine.",
"Note".bold().red()
);
)?;
}
Ok(())
}
pub fn final_export(&self) -> Result<()> {
+2 -2
View File
@@ -194,8 +194,8 @@ fn build_command() -> Command {
.help(
"Perform benchmark runs for each value in the comma-separated list VALUES. \
Replaces the string '{VAR}' in each command by the current parameter value\
.\n\nExample: hyperfine -L compiler gcc,clang '{compiler} -O2 main.cpp'\n\n\
This performs benchmarks for 'gcc -O2 main.cpp' and 'clang -O2 main.cpp'.\n\n\
.\n\nExample: hyperfine -L compiler g++,clang++ '{compiler} -O2 main.cpp'\n\n\
This performs benchmarks for 'g++ -O2 main.cpp' and 'clang++ -O2 main.cpp'.\n\n\
The option can be specified multiple times to run benchmarks for all \
possible parameter combinations.\n"
),
+4 -2
View File
@@ -158,7 +158,9 @@ impl<'a> Commands<'a> {
});
let args: Vec<_> = args.map(|v| v.as_str()).collect::<Vec<_>>();
let param_names_and_values: Vec<(&str, Vec<String>)> = args
.chunks_exact(2)
.as_chunks::<2>()
.0
.iter()
.map(|pair| {
let name = pair[0];
let list_str = pair[1];
@@ -169,7 +171,7 @@ impl<'a> Commands<'a> {
let duplicates =
Self::find_duplicates(param_names_and_values.iter().map(|(name, _)| *name));
if !duplicates.is_empty() {
bail!("Duplicate parameter names: {}", &duplicates.join(", "));
bail!("Duplicate parameter names: {}", duplicates.join(", "));
}
}
+4
View File
@@ -3,6 +3,10 @@ use std::num::{self, ParseFloatError, ParseIntError};
use rust_decimal::Error as DecimalError;
use thiserror::Error;
#[derive(Debug, Error)]
#[error(transparent)]
pub struct ConsoleOutputError(pub std::io::Error);
#[derive(Debug, Error)]
pub enum ParameterScanError {
#[error("Error while parsing parameter scan arguments ({0})")]
+5 -3
View File
@@ -1,5 +1,5 @@
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::io::{self, Write};
mod asciidoc;
mod csv;
@@ -18,6 +18,7 @@ use self::orgmode::OrgmodeExporter;
use crate::benchmark::benchmark_result::BenchmarkResult;
use crate::options::SortOrder;
use crate::output::console_writeln;
use crate::util::units::Unit;
use anyhow::{Context, Result};
@@ -144,8 +145,9 @@ impl ExportManager {
}
ExportTarget::Stdout => {
if !intermediate {
println!();
println!("{}", String::from_utf8(content()?).unwrap());
let mut stdout = io::stdout().lock();
console_writeln!(stdout)?;
console_writeln!(stdout, "{}", String::from_utf8(content()?).unwrap())?;
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ impl MarkupExporter for OrgmodeExporter {
format!(
"| {} | {} |\n",
cells.first().unwrap(),
&cells[1..].join(" | ")
cells[1..].join(" | ")
)
}
+16 -2
View File
@@ -4,10 +4,12 @@
)]
use std::env;
use std::io::{self, Write};
use benchmark::scheduler::Scheduler;
use cli::get_cli_arguments;
use command::Commands;
use error::ConsoleOutputError;
use export::ExportManager;
use options::Options;
@@ -44,17 +46,29 @@ fn run() -> Result<()> {
let mut scheduler = Scheduler::new(&commands, &options, &export_manager);
scheduler.run_benchmarks()?;
scheduler.print_relative_speed_comparison();
scheduler.print_relative_speed_comparison()?;
scheduler.final_export()?;
Ok(())
}
fn caused_by_broken_console_pipe(error: &anyhow::Error) -> bool {
error
.downcast_ref::<ConsoleOutputError>()
.is_some_and(|e| e.0.kind() == io::ErrorKind::BrokenPipe)
}
fn main() {
match run() {
Ok(_) => {}
Err(e) => {
eprintln!("{} {:#}", "Error:".red(), e);
// Exit quietly when a reader stops consuming console output early.
if caused_by_broken_console_pipe(&e) {
std::process::exit(0);
}
// The write to stderr can itself fail if stderr is closed; the
// error message cannot be shown in that case anyway.
let _ = writeln!(io::stderr(), "{} {:#}", "Error:".red(), e);
std::process::exit(1);
}
}
+10
View File
@@ -1,3 +1,13 @@
pub mod format;
pub mod progress_bar;
pub mod warnings;
/// Mark console write errors so broken pipes can be handled separately from file errors.
macro_rules! console_writeln {
($writer:expr $(, $($args:tt)*)?) => {
writeln!($writer $(, $($args)*)?)
.map_err($crate::error::ConsoleOutputError)
};
}
pub(crate) use console_writeln;
+1 -1
View File
@@ -4,5 +4,5 @@
/// [1] Mytkowicz, 2009. Producing Wrong Data Without Doing Anything Obviously Wrong!.
/// Sigplan Notices - SIGPLAN. 44. 265-276. 10.1145/1508284.1508275.
pub fn value() -> String {
"X".repeat(rand::random::<usize>() % 4096usize)
"X".repeat(rand::random_range(0..4096))
}
+1 -3
View File
@@ -1,9 +1,7 @@
use std::process::Command;
use assert_cmd::cargo::CommandCargoExt;
pub fn hyperfine_raw_command() -> Command {
let mut cmd = Command::cargo_bin("hyperfine").unwrap();
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("hyperfine"));
cmd.current_dir("tests/");
cmd
}
+42 -1
View File
@@ -1,5 +1,5 @@
mod common;
use common::hyperfine;
use common::{hyperfine, hyperfine_raw_command};
use predicates::prelude::*;
@@ -33,6 +33,47 @@ fn one_run_is_supported() {
.success();
}
/// Regression test: hyperfine must not panic when writing to a closed
/// stdout pipe (e.g. `hyperfine ... | head -n 1` or a downstream process
/// that terminates early). A `BrokenPipe` error should result in a quiet
/// exit with code 0 instead of a `println!` panic.
#[test]
fn exits_quietly_when_stdout_is_closed() {
use std::process::Stdio;
for args in [
vec!["--runs=1", "echo dummy benchmark"],
vec!["--runs=1", "--export-json=-", "echo dummy benchmark"],
vec!["--runs=1", "echo dummy benchmark", "echo second benchmark"],
] {
// A pipe whose read end has been closed before hyperfine is even
// started: every write to it will fail with `BrokenPipe`.
let (reader, writer) = std::io::pipe().expect("failed to create pipe");
drop(reader);
let output = hyperfine_raw_command()
.args(&args)
.stdout(writer)
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn hyperfine")
.wait_with_output()
.expect("failed to wait for hyperfine");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("panicked"),
"hyperfine panicked while writing to a closed pipe:\n{}",
stderr
);
assert!(
output.status.success(),
"hyperfine did not exit cleanly; stderr:\n{}",
stderr
);
}
}
#[test]
fn can_run_commands_without_a_shell() {
hyperfine()