2018-01-13 16:50:02 +01:00
|
|
|
extern crate ansi_term;
|
2018-01-13 20:53:55 +01:00
|
|
|
#[macro_use]
|
2018-01-13 16:50:02 +01:00
|
|
|
extern crate clap;
|
2018-01-13 19:01:52 +01:00
|
|
|
extern crate indicatif;
|
2018-01-13 16:50:02 +01:00
|
|
|
|
2018-01-13 21:01:30 +01:00
|
|
|
use std::cmp;
|
2018-01-14 11:41:27 +13:00
|
|
|
use std::error::Error;
|
|
|
|
|
use std::io;
|
2018-01-13 16:50:02 +01:00
|
|
|
use std::process::{Command, Stdio};
|
|
|
|
|
use std::time::Instant;
|
2018-01-14 10:48:47 +01:00
|
|
|
use std::fmt;
|
2018-01-13 16:50:02 +01:00
|
|
|
|
2018-01-13 19:01:52 +01:00
|
|
|
use indicatif::{ProgressBar, ProgressStyle};
|
2018-01-14 10:48:47 +01:00
|
|
|
use ansi_term::Colour::{Cyan, Green, Yellow};
|
2018-01-13 16:50:02 +01:00
|
|
|
use clap::{App, AppSettings, Arg};
|
|
|
|
|
|
2018-01-14 10:48:47 +01:00
|
|
|
/// Type alias for unit of time
|
|
|
|
|
type Second = f64;
|
|
|
|
|
|
|
|
|
|
/// Threshold for warning about fast execution time
|
|
|
|
|
const MIN_EXECUTION_TIME: Second = 5e-3;
|
|
|
|
|
|
2018-01-14 11:41:27 +13:00
|
|
|
/// Print error message to stderr and terminate
|
|
|
|
|
pub fn error(message: &str) -> ! {
|
|
|
|
|
eprintln!("{}", message);
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-13 16:50:02 +01:00
|
|
|
struct CmdResult {
|
2018-01-13 19:47:59 +01:00
|
|
|
/// Execution time in seconds
|
2018-01-14 10:48:47 +01:00
|
|
|
execution_time_sec: Second,
|
2018-01-13 19:47:59 +01:00
|
|
|
|
|
|
|
|
/// True if the command finished with exit code zero
|
2018-01-13 16:50:02 +01:00
|
|
|
success: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CmdResult {
|
2018-01-14 10:48:47 +01:00
|
|
|
fn new(execution_time_sec: Second, success: bool) -> CmdResult {
|
2018-01-13 16:50:02 +01:00
|
|
|
CmdResult {
|
2018-01-13 19:47:59 +01:00
|
|
|
execution_time_sec,
|
2018-01-13 16:50:02 +01:00
|
|
|
success,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-13 19:47:59 +01:00
|
|
|
/// Run the given shell command and measure the execution time
|
2018-01-14 11:41:27 +13:00
|
|
|
fn time_shell_command(shell_cmd: &str) -> io::Result<CmdResult> {
|
2018-01-13 16:50:02 +01:00
|
|
|
let start = Instant::now();
|
|
|
|
|
|
|
|
|
|
let status = Command::new("sh")
|
|
|
|
|
.arg("-c")
|
|
|
|
|
.arg(shell_cmd)
|
|
|
|
|
.stdout(Stdio::null())
|
|
|
|
|
.stderr(Stdio::null())
|
2018-01-14 11:41:27 +13:00
|
|
|
.status()?;
|
2018-01-13 16:50:02 +01:00
|
|
|
|
|
|
|
|
let duration = start.elapsed();
|
|
|
|
|
|
2018-01-13 19:47:59 +01:00
|
|
|
let execution_time_sec = duration.as_secs() as f64 + duration.subsec_nanos() as f64 * 1e-9;
|
2018-01-13 16:50:02 +01:00
|
|
|
|
2018-01-14 10:48:47 +01:00
|
|
|
Ok(CmdResult::new(execution_time_sec, status.success()))
|
2018-01-13 16:50:02 +01:00
|
|
|
}
|
|
|
|
|
|
2018-01-13 20:05:31 +01:00
|
|
|
/// Return a pre-configured progress bar
|
|
|
|
|
fn get_progress_bar(length: u64, msg: &str) -> ProgressBar {
|
|
|
|
|
let progressbar_style = ProgressStyle::default_spinner()
|
|
|
|
|
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
|
|
|
|
|
.template(" {spinner} {msg:<28} {wide_bar} ETA {eta_precise}");
|
|
|
|
|
|
|
|
|
|
let bar = ProgressBar::new(length);
|
|
|
|
|
bar.set_style(progressbar_style.clone());
|
|
|
|
|
bar.enable_steady_tick(80);
|
|
|
|
|
bar.set_message(msg);
|
|
|
|
|
|
|
|
|
|
bar
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-14 10:48:47 +01:00
|
|
|
/// Possible benchmark warnings
|
|
|
|
|
enum Warnings {
|
|
|
|
|
FastExecutionTime,
|
|
|
|
|
NonZeroExitCode,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl fmt::Display for Warnings {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
|
match self {
|
|
|
|
|
&Warnings::FastExecutionTime => write!(
|
|
|
|
|
f,
|
|
|
|
|
"Command took less than {:.0} ms to complete. \
|
|
|
|
|
The execution time is likely to be dominated by the intermediate shell.",
|
|
|
|
|
MIN_EXECUTION_TIME * 1e3
|
|
|
|
|
),
|
|
|
|
|
&Warnings::NonZeroExitCode => write!(f, "Process exited with a non-zero exit code"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-13 20:37:57 +01:00
|
|
|
/// Run the benchmark for a single shell command
|
2018-01-14 10:48:47 +01:00
|
|
|
fn run_benchmark(cmd: &str, options: &HyperfineOptions) -> io::Result<()> {
|
2018-01-13 20:37:57 +01:00
|
|
|
println!("Command: {}", Cyan.paint(cmd));
|
|
|
|
|
println!();
|
|
|
|
|
|
|
|
|
|
let mut results = vec![];
|
|
|
|
|
|
|
|
|
|
// Warmup phase
|
2018-01-13 20:53:55 +01:00
|
|
|
if let Some(warmup_count) = options.warmup_count {
|
2018-01-13 20:37:57 +01:00
|
|
|
let bar = get_progress_bar(warmup_count, "Performing warmup runs");
|
|
|
|
|
|
|
|
|
|
for _ in 1..warmup_count {
|
|
|
|
|
bar.inc(1);
|
|
|
|
|
let _ = time_shell_command(cmd);
|
|
|
|
|
}
|
|
|
|
|
bar.finish_and_clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set up progress bar (and spinner for initial measurement)
|
|
|
|
|
let bar = get_progress_bar(options.min_runs, "Initial time measurement");
|
|
|
|
|
|
|
|
|
|
// Initial timing run
|
2018-01-14 10:48:47 +01:00
|
|
|
let res = time_shell_command(cmd)?;
|
2018-01-13 20:37:57 +01:00
|
|
|
|
|
|
|
|
let runs_in_min_time = (options.min_time_sec / res.execution_time_sec) as u64;
|
|
|
|
|
|
|
|
|
|
let count = if runs_in_min_time >= options.min_runs {
|
|
|
|
|
runs_in_min_time
|
|
|
|
|
} else {
|
|
|
|
|
options.min_runs
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Save the first result
|
|
|
|
|
results.push(res);
|
|
|
|
|
|
|
|
|
|
// Re-configure the progress bar
|
|
|
|
|
bar.set_length(count);
|
|
|
|
|
bar.set_message("Collecting statistics");
|
|
|
|
|
|
|
|
|
|
// Gather statistics
|
|
|
|
|
for _ in 1..count {
|
|
|
|
|
bar.inc(1);
|
2018-01-14 10:48:47 +01:00
|
|
|
let res = time_shell_command(cmd)?;
|
2018-01-13 20:37:57 +01:00
|
|
|
results.push(res);
|
|
|
|
|
}
|
|
|
|
|
bar.finish_and_clear();
|
|
|
|
|
|
|
|
|
|
// Compute statistical quantities
|
2018-01-14 10:48:47 +01:00
|
|
|
let get_execution_time = |r: &CmdResult| -> Second { r.execution_time_sec };
|
|
|
|
|
|
|
|
|
|
// Iterator over execution times
|
|
|
|
|
let execution_times = results.iter().map(&get_execution_time);
|
|
|
|
|
|
|
|
|
|
let t_sum: Second = execution_times.clone().sum();
|
2018-01-13 20:37:57 +01:00
|
|
|
let t_mean = t_sum / (results.len() as f64);
|
|
|
|
|
|
2018-01-14 10:48:47 +01:00
|
|
|
let t2_sum: Second = execution_times.clone().map(|t| t.powi(2)).sum();
|
2018-01-13 20:37:57 +01:00
|
|
|
let t2_mean = t2_sum / (results.len() as f64);
|
|
|
|
|
|
|
|
|
|
let stddev = (t2_mean - t_mean.powi(2)).sqrt();
|
|
|
|
|
|
|
|
|
|
// Formatting and console output
|
|
|
|
|
let time_fmt = format!("{:.3} s ± {:.3} s", t_mean, stddev);
|
|
|
|
|
|
|
|
|
|
println!(" Time: {}", Green.paint(time_fmt));
|
|
|
|
|
|
2018-01-14 10:48:47 +01:00
|
|
|
// Warnings
|
|
|
|
|
let mut warnings = vec![];
|
|
|
|
|
|
|
|
|
|
// Check execution time
|
|
|
|
|
if execution_times.clone().any(|t| t < MIN_EXECUTION_TIME) {
|
|
|
|
|
warnings.push(Warnings::FastExecutionTime);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check programm exit codes
|
|
|
|
|
if results.iter().any(|r| !r.success) {
|
|
|
|
|
warnings.push(Warnings::NonZeroExitCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for warning in &warnings {
|
|
|
|
|
eprintln!(" {}: {}", Yellow.paint("Warning"), warning);
|
|
|
|
|
}
|
2018-01-13 20:37:57 +01:00
|
|
|
|
|
|
|
|
println!();
|
2018-01-14 10:48:47 +01:00
|
|
|
|
|
|
|
|
Ok(())
|
2018-01-13 20:37:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct HyperfineOptions {
|
|
|
|
|
pub warmup_count: Option<u64>,
|
|
|
|
|
pub min_runs: u64,
|
2018-01-14 10:48:47 +01:00
|
|
|
pub min_time_sec: Second,
|
2018-01-13 20:37:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for HyperfineOptions {
|
|
|
|
|
fn default() -> HyperfineOptions {
|
|
|
|
|
HyperfineOptions {
|
|
|
|
|
warmup_count: None,
|
|
|
|
|
min_runs: 10,
|
2018-01-14 10:48:47 +01:00
|
|
|
min_time_sec: 3.0,
|
2018-01-13 20:37:57 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-13 16:50:02 +01:00
|
|
|
fn main() {
|
|
|
|
|
let matches = App::new("hyperfine")
|
2018-01-13 20:53:55 +01:00
|
|
|
.version(crate_version!())
|
|
|
|
|
.setting(AppSettings::ColoredHelp)
|
|
|
|
|
.setting(AppSettings::DeriveDisplayOrder)
|
2018-01-13 16:50:02 +01:00
|
|
|
.about("A command-line benchmarking tool")
|
|
|
|
|
.arg(
|
|
|
|
|
Arg::with_name("command")
|
|
|
|
|
.help("Command to benchmark")
|
|
|
|
|
.required(true)
|
2018-01-14 11:41:27 +13:00
|
|
|
.multiple(true)
|
|
|
|
|
.empty_values(false),
|
2018-01-13 16:50:02 +01:00
|
|
|
)
|
2018-01-13 20:05:31 +01:00
|
|
|
.arg(
|
|
|
|
|
Arg::with_name("warmup")
|
|
|
|
|
.long("warmup")
|
|
|
|
|
.short("w")
|
|
|
|
|
.takes_value(true)
|
|
|
|
|
.value_name("NUM")
|
|
|
|
|
.help("Perform NUM warmup runs before the actual benchmark"),
|
|
|
|
|
)
|
2018-01-13 21:01:30 +01:00
|
|
|
.arg(
|
|
|
|
|
Arg::with_name("min-runs")
|
|
|
|
|
.long("min-runs")
|
|
|
|
|
.short("m")
|
|
|
|
|
.takes_value(true)
|
|
|
|
|
.value_name("NUM")
|
|
|
|
|
.help("Perform at least NUM runs for each command"),
|
|
|
|
|
)
|
2018-01-13 16:50:02 +01:00
|
|
|
.get_matches();
|
|
|
|
|
|
2018-01-13 21:01:30 +01:00
|
|
|
let str_to_u64 = |n| u64::from_str_radix(n, 10).ok();
|
2018-01-13 19:01:52 +01:00
|
|
|
|
2018-01-13 21:01:30 +01:00
|
|
|
// Process command line options
|
|
|
|
|
let mut options = HyperfineOptions::default();
|
|
|
|
|
options.warmup_count = matches.value_of("warmup").and_then(&str_to_u64);
|
|
|
|
|
|
|
|
|
|
if let Some(min_runs) = matches.value_of("min-runs").and_then(&str_to_u64) {
|
|
|
|
|
options.min_runs = cmp::max(1, min_runs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run the benchmarks
|
2018-01-13 16:50:02 +01:00
|
|
|
let commands = matches.values_of("command").unwrap();
|
|
|
|
|
for cmd in commands {
|
2018-01-14 10:48:47 +01:00
|
|
|
let res = run_benchmark(&cmd, &options);
|
|
|
|
|
|
|
|
|
|
match res {
|
|
|
|
|
Err(err) => error(err.description()),
|
|
|
|
|
Ok(_) => {}
|
|
|
|
|
}
|
2018-01-13 16:50:02 +01:00
|
|
|
}
|
|
|
|
|
}
|