Files
hyperfine/src/main.rs
T

444 lines
14 KiB
Rust
Raw Normal View History

2018-01-13 21:01:30 +01:00
use std::cmp;
use std::collections::BTreeMap;
2018-03-24 18:30:52 -04:00
use std::env;
2018-01-13 16:50:02 +01:00
use atty::Stream;
2018-03-24 18:30:52 -04:00
use clap::ArgMatches;
2018-05-22 22:57:51 +02:00
use colored::*;
2018-01-13 16:50:02 +01:00
2021-08-24 21:24:34 +02:00
pub mod app;
pub mod benchmark;
pub mod benchmark_result;
pub mod command;
pub mod error;
pub mod export;
pub mod format;
2021-08-24 22:16:40 +02:00
pub mod min_max;
2021-08-24 21:24:34 +02:00
pub mod options;
pub mod outlier_detection;
pub mod parameter_range;
2021-08-24 21:58:05 +02:00
pub mod progress_bar;
pub mod relative_speed;
2021-08-24 21:24:34 +02:00
pub mod shell;
pub mod timer;
2021-08-24 22:01:26 +02:00
pub mod tokenize;
2021-08-24 21:24:34 +02:00
pub mod types;
pub mod units;
pub mod warnings;
2018-01-14 10:48:47 +01:00
2021-08-24 21:24:34 +02:00
use app::get_arg_matches;
use benchmark::{mean_shell_spawning_time, run_benchmark};
2021-08-24 22:06:49 +02:00
use benchmark_result::BenchmarkResult;
2021-08-24 21:24:34 +02:00
use command::Command;
use error::OptionsError;
use export::{ExportManager, ExportType};
2021-10-04 22:34:19 +09:00
use options::{CmdFailureAction, HyperfineOptions, OutputStyleOption, Shell};
2021-08-24 21:24:34 +02:00
use parameter_range::get_parameterized_commands;
2021-08-24 22:01:26 +02:00
use tokenize::tokenize;
2021-08-24 21:24:34 +02:00
use types::ParameterValue;
use units::Unit;
2018-01-14 10:48:47 +01:00
2022-02-06 17:44:28 +01:00
use anyhow::{bail, Result};
2018-01-14 11:41:27 +13:00
2021-08-24 22:06:49 +02:00
pub fn write_benchmark_comparison(results: &[BenchmarkResult]) {
if results.len() < 2 {
return;
}
if let Some(mut annotated_results) = relative_speed::compute(results) {
annotated_results.sort_by(|l, r| relative_speed::compare_mean_time(l.result, r.result));
2021-08-24 22:06:49 +02:00
let fastest = &annotated_results[0];
let others = &annotated_results[1..];
println!("{}", "Summary".bold());
println!(" '{}' ran", fastest.result.command.cyan());
for item in others {
println!(
"{}{} times faster than '{}'",
2021-08-24 22:06:49 +02:00
format!("{:8.2}", item.relative_speed).bold().green(),
if let Some(stddev) = item.relative_speed_stddev {
format!(" ± {}", format!("{:.2}", stddev).green())
} else {
"".into()
},
2021-08-24 22:06:49 +02:00
&item.result.command.magenta()
);
}
} else {
eprintln!(
"{}: 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). \
Try to re-run the benchmark on a quiet system. If it does not help, you command is \
most likely too fast to be accurately benchmarked by hyperfine.",
"Note".bold().red()
);
}
}
2022-02-06 17:44:28 +01:00
fn run_benchmarks_and_print_comparison(
2020-10-16 10:47:26 +02:00
commands: &[Command<'_>],
options: &HyperfineOptions,
export_manager: &ExportManager,
2022-02-06 17:44:28 +01:00
) -> Result<()> {
2018-11-21 19:55:58 +01:00
let shell_spawning_time =
2019-07-13 10:38:51 +02:00
mean_shell_spawning_time(&options.shell, options.output_style, options.show_output)?;
2018-01-14 12:03:34 +01:00
let mut timing_results = vec![];
if let Some(preparation_command) = &options.preparation_command {
2019-10-13 14:46:00 +02:00
if preparation_command.len() > 1 && commands.len() != preparation_command.len() {
2022-02-06 17:44:28 +01:00
bail!(
2019-10-13 14:46:00 +02:00
"The '--prepare' option has to be provided just once or N times, where N is the \
2022-02-06 17:44:28 +01:00
number of benchmark commands."
);
}
}
2018-01-14 12:03:34 +01:00
// Run the benchmarks
2018-01-14 13:34:22 +01:00
for (num, cmd) in commands.iter().enumerate() {
timing_results.push(run_benchmark(num, cmd, shell_spawning_time, options)?);
// Export (intermediate) results
2022-02-06 17:44:28 +01:00
export_manager.write_results(&timing_results, options.time_unit)?;
2018-01-14 12:03:34 +01:00
}
2020-10-16 10:47:26 +02:00
// Print relative speed comparison
if options.output_style != OutputStyleOption::Disabled {
write_benchmark_comparison(&timing_results);
}
Ok(())
2018-01-14 12:03:34 +01:00
}
2022-02-06 17:44:28 +01:00
fn run() -> Result<()> {
2018-03-24 18:30:52 -04:00
let matches = get_arg_matches(env::args_os());
2022-02-06 17:44:28 +01:00
let options = build_hyperfine_options(&matches)?;
let commands = build_commands(&matches)?;
let export_manager = build_export_manager(&matches)?;
2018-03-24 18:30:52 -04:00
2022-02-06 17:44:28 +01:00
run_benchmarks_and_print_comparison(&commands, &options, &export_manager)
}
2018-03-24 18:30:52 -04:00
2022-02-06 17:44:28 +01:00
fn main() {
match run() {
2020-10-16 10:47:26 +02:00
Ok(_) => {}
2022-02-06 17:44:28 +01:00
Err(e) => {
eprintln!("{} {:#}", "Error:".red(), e);
std::process::exit(1);
}
2018-03-24 18:30:52 -04:00
}
}
/// Build the HyperfineOptions that correspond to the given ArgMatches
2022-02-03 20:50:39 +01:00
fn build_hyperfine_options<'a>(matches: &ArgMatches) -> Result<HyperfineOptions, OptionsError<'a>> {
2021-10-17 17:56:30 +02:00
// Enabled ANSI colors on Windows 10
#[cfg(windows)]
colored::control::set_virtual_terminal(true).unwrap();
2018-01-16 21:26:14 +01:00
let mut options = HyperfineOptions::default();
2019-08-30 22:07:14 +05:30
let param_to_u64 = |param| {
matches
.value_of(param)
.map(|n| {
2021-08-22 16:05:03 +02:00
n.parse::<u64>()
.map_err(|e| OptionsError::NumericParsingError(param, e))
})
.transpose()
2019-08-30 22:07:14 +05:30
};
2018-01-14 12:39:58 +01:00
options.warmup_count = param_to_u64("warmup")?.unwrap_or(options.warmup_count);
2018-01-13 21:01:30 +01:00
let mut min_runs = param_to_u64("min-runs")?;
let mut max_runs = param_to_u64("max-runs")?;
if let Some(runs) = param_to_u64("runs")? {
min_runs = Some(runs);
max_runs = Some(runs);
2018-01-13 21:01:30 +01:00
}
match (min_runs, max_runs) {
(Some(min), None) => {
options.runs.min = min;
}
(None, Some(max)) => {
// Since the minimum was not explicit we lower it if max is below the default min.
options.runs.min = cmp::min(options.runs.min, max);
2018-09-09 21:52:58 +01:00
options.runs.max = Some(max);
}
(Some(min), Some(max)) if min > max => {
return Err(OptionsError::EmptyRunsRange);
}
(Some(min), Some(max)) => {
options.runs.min = min;
2018-09-09 21:52:58 +01:00
options.runs.max = Some(max);
}
(None, None) => {}
};
options.setup_command = matches.value_of("setup").map(String::from);
2019-10-13 18:10:53 +05:45
options.preparation_command = matches
.values_of("prepare")
.map(|values| values.map(String::from).collect::<Vec<String>>());
2018-01-18 18:13:12 +01:00
options.cleanup_command = matches.value_of("cleanup").map(String::from);
2018-05-21 21:04:30 +02:00
options.show_output = matches.is_present("show-output");
options.output_style = match matches.value_of("style") {
Some("full") => OutputStyleOption::Full,
Some("basic") => OutputStyleOption::Basic,
2018-02-13 20:40:43 -05:00
Some("nocolor") => OutputStyleOption::NoColor,
Some("color") => OutputStyleOption::Color,
2019-10-14 00:03:29 +03:00
Some("none") => OutputStyleOption::Disabled,
2018-09-27 23:07:07 +02:00
_ => {
if !options.show_output && atty::is(Stream::Stdout) {
OutputStyleOption::Full
} else {
OutputStyleOption::Basic
}
}
};
match options.output_style {
2018-09-28 21:25:18 +02:00
OutputStyleOption::Basic | OutputStyleOption::NoColor => {
colored::control::set_override(false)
}
OutputStyleOption::Full | OutputStyleOption::Color => colored::control::set_override(true),
OutputStyleOption::Disabled => {}
2018-10-30 19:30:17 -07:00
};
2021-10-04 22:34:19 +09:00
if let Some(shell) = matches.value_of("shell") {
options.shell = Shell::parse(shell)?;
}
2018-02-02 14:42:31 +01:00
2018-01-18 18:39:17 +01:00
if matches.is_present("ignore-failure") {
options.failure_action = CmdFailureAction::Ignore;
}
2018-01-16 21:36:41 +01:00
options.time_unit = match matches.value_of("time-unit") {
Some("millisecond") => Some(Unit::MilliSecond),
Some("second") => Some(Unit::Second),
_ => None,
};
Ok(options)
2018-03-24 18:30:52 -04:00
}
/// Build the ExportManager that will export the results specified
/// in the given ArgMatches
2022-02-06 17:44:28 +01:00
fn build_export_manager(matches: &ArgMatches) -> Result<ExportManager> {
2021-10-04 23:07:38 +09:00
let mut export_manager = ExportManager::default();
2019-02-24 18:57:33 +01:00
{
2022-02-06 17:44:28 +01:00
let mut add_exporter = |flag, exporttype| -> Result<()> {
2019-02-24 18:57:33 +01:00
if let Some(filename) = matches.value_of(flag) {
export_manager.add_exporter(exporttype, filename)?;
2019-02-24 18:57:33 +01:00
}
Ok(())
2019-02-24 18:57:33 +01:00
};
add_exporter("export-asciidoc", ExportType::Asciidoc)?;
add_exporter("export-json", ExportType::Json)?;
add_exporter("export-csv", ExportType::Csv)?;
add_exporter("export-markdown", ExportType::Markdown)?;
2018-03-24 18:30:52 -04:00
}
Ok(export_manager)
2018-03-24 18:30:52 -04:00
}
/// Build the commands to benchmark
2022-02-06 11:59:21 +01:00
fn build_commands(matches: &ArgMatches) -> Result<Vec<Command>> {
let command_names = matches.values_of("command-name");
let command_strings = matches.values_of("command").unwrap();
2019-07-12 21:51:08 +02:00
if let Some(args) = matches.values_of("parameter-scan") {
2019-08-31 12:48:26 +05:30
let step_size = matches.value_of("parameter-step-size");
2022-02-06 17:44:28 +01:00
Ok(get_parameterized_commands(
command_names,
command_strings,
args,
step_size,
)?)
} else if let Some(args) = matches.values_of("parameter-list") {
let command_names = command_names.map_or(vec![], |names| names.collect::<Vec<&str>>());
let args: Vec<_> = args.collect();
let param_names_and_values: Vec<(&str, Vec<String>)> = args
.chunks_exact(2)
.map(|pair| {
let name = pair[0];
let list_str = pair[1];
(name, tokenize(list_str))
})
.collect();
{
let dupes = find_dupes(param_names_and_values.iter().map(|(name, _)| *name));
if !dupes.is_empty() {
2022-02-06 17:44:28 +01:00
bail!("Duplicate parameter names: {}", &dupes.join(", "));
}
}
let command_list = command_strings.collect::<Vec<&str>>();
let dimensions: Vec<usize> = std::iter::once(command_list.len())
.chain(
param_names_and_values
.iter()
.map(|(_, values)| values.len()),
)
.collect();
let param_space_size = dimensions.iter().product();
if param_space_size == 0 {
2022-02-06 17:44:28 +01:00
return Ok(Vec::new());
}
// `--command-name` should appear exactly once or exactly B times,
// where B is the total number of benchmarks.
let command_name_count = command_names.len();
if command_name_count > 1 && command_name_count != param_space_size {
2022-02-06 17:44:28 +01:00
return Err(OptionsError::UnexpectedCommandNameCount(
command_name_count,
param_space_size,
)
.into());
}
let mut i = 0;
let mut commands = Vec::with_capacity(param_space_size);
let mut index = vec![0usize; dimensions.len()];
'outer: loop {
let name = command_names
.get(i)
2021-08-22 16:05:03 +02:00
.or_else(|| command_names.get(0))
.copied();
i += 1;
let (command_index, params_indices) = index.split_first().unwrap();
let parameters = param_names_and_values
.iter()
.zip(params_indices)
.map(|((name, values), i)| (*name, ParameterValue::Text(values[*i].clone())))
.collect();
commands.push(Command::new_parametrized(
name,
command_list[*command_index],
parameters,
));
// Increment index, exiting loop on overflow.
for (i, n) in index.iter_mut().zip(dimensions.iter()) {
*i += 1;
if *i < *n {
continue 'outer;
} else {
*i = 0;
}
}
break 'outer;
}
2022-02-06 17:44:28 +01:00
Ok(commands)
} else {
let command_names = command_names.map_or(vec![], |names| names.collect::<Vec<&str>>());
if command_names.len() > command_strings.len() {
2022-02-06 17:44:28 +01:00
return Err(OptionsError::TooManyCommandNames(command_strings.len()).into());
}
let command_list = command_strings.collect::<Vec<&str>>();
let mut commands = Vec::with_capacity(command_list.len());
for (i, s) in command_list.iter().enumerate() {
2021-08-22 15:59:56 +02:00
commands.push(Command::new(command_names.get(i).copied(), s));
}
2022-02-06 17:44:28 +01:00
Ok(commands)
2019-07-12 21:51:08 +02:00
}
2018-01-13 16:50:02 +01:00
}
/// Finds all the strings that appear multiple times in the input iterator, returning them in
/// sorted order. If no string appears more than once, the result is an empty vector.
fn find_dupes<'a, I: IntoIterator<Item = &'a str>>(i: I) -> Vec<&'a str> {
let mut counts = BTreeMap::<&'a str, usize>::new();
for s in i {
*counts.entry(s).or_default() += 1;
}
counts
.into_iter()
.filter_map(|(k, n)| if n > 1 { Some(k) } else { None })
.collect()
}
#[test]
fn test_build_commands_cross_product() {
let matches = get_arg_matches(vec![
"hyperfine",
"-L",
2021-10-14 20:47:44 +03:00
"par1",
"a,b",
"-L",
2021-10-14 20:47:44 +03:00
"par2",
"z,y",
2021-10-14 20:47:44 +03:00
"echo {par1} {par2}",
"printf '%s\n' {par1} {par2}",
]);
2022-02-06 17:44:28 +01:00
let result = build_commands(&matches).unwrap();
2021-10-14 20:47:44 +03:00
// Iteration order: command list first, then parameters in listed order (here, "par1" before
// "par2", which is distinct from their sorted order), with parameter values in listed order.
let pv = |s: &str| ParameterValue::Text(s.to_string());
2021-10-14 20:47:44 +03:00
let cmd = |cmd: usize, par1: &str, par2: &str| {
let expression = ["echo {par1} {par2}", "printf '%s\n' {par1} {par2}"][cmd];
let params = vec![("par1", pv(par1)), ("par2", pv(par2))];
Command::new_parametrized(None, expression, params)
};
let expected = vec![
cmd(0, "a", "z"),
cmd(1, "a", "z"),
cmd(0, "b", "z"),
cmd(1, "b", "z"),
cmd(0, "a", "y"),
cmd(1, "a", "y"),
cmd(0, "b", "y"),
cmd(1, "b", "y"),
];
assert_eq!(result, expected);
}
#[test]
fn test_build_parameter_list_commands() {
let matches = get_arg_matches(vec![
"hyperfine",
"echo {foo}",
"--parameter-list",
"foo",
"1,2",
"--command-name",
"name-{foo}",
]);
2022-02-06 17:44:28 +01:00
let commands = build_commands(&matches).unwrap();
assert_eq!(commands.len(), 2);
assert_eq!(commands[0].get_name(), "name-1");
assert_eq!(commands[1].get_name(), "name-2");
assert_eq!(commands[0].get_shell_command(), "echo 1");
assert_eq!(commands[1].get_shell_command(), "echo 2");
}
#[test]
fn test_build_parameter_range_commands() {
let matches = get_arg_matches(vec![
"hyperfine",
"echo {val}",
"--parameter-scan",
"val",
"1",
"2",
"--parameter-step-size",
"1",
"--command-name",
"name-{val}",
]);
2022-02-06 17:44:28 +01:00
let commands = build_commands(&matches).unwrap();
assert_eq!(commands.len(), 2);
assert_eq!(commands[0].get_name(), "name-1");
assert_eq!(commands[1].get_name(), "name-2");
assert_eq!(commands[0].get_shell_command(), "echo 1");
assert_eq!(commands[1].get_shell_command(), "echo 2");
}