Compare commits

...
12 changed files with 169 additions and 164 deletions

No files matched your search

+64 -47
View File
@@ -3,27 +3,67 @@ use std::process::{ExitStatus, Stdio};
use crate::command::Command; use crate::command::Command;
use crate::options::{CmdFailureAction, CommandOutputPolicy, Options, OutputStyleOption, Shell}; use crate::options::{CmdFailureAction, CommandOutputPolicy, Options, OutputStyleOption, Shell};
use crate::output::progress_bar::get_progress_bar; use crate::output::progress_bar::get_progress_bar;
use crate::shell::execute_and_time; use crate::timer::{execute_and_measure, TimerResult};
use crate::timer::wallclocktimer::WallClockTimer; use crate::util::randomized_environment_offset;
use crate::util::units::Second; use crate::util::units::Second;
use super::timing_result::TimingResult; use super::timing_result::TimingResult;
use anyhow::{bail, Result}; use anyhow::{bail, Context, Result};
use statistical::mean; use statistical::mean;
pub trait Executor { pub trait Executor {
fn time_command( /// Run the given command and measure the execution time
fn run_command_and_measure(
&self, &self,
command: &Command<'_>, command: &Command<'_>,
command_failure_action: Option<CmdFailureAction>, command_failure_action: Option<CmdFailureAction>,
) -> Result<(TimingResult, ExitStatus)>; ) -> Result<(TimingResult, ExitStatus)>;
/// Perform a calibration of this executor. For example,
/// when running commands through a shell, we need to
/// measure the shell spawning time separately in order
/// to subtract it from the full runtime later.
fn calibrate(&mut self) -> Result<()>; fn calibrate(&mut self) -> Result<()>;
/// Return the time overhead for this executor when
/// performing a measurement. This should return the time
/// that is being used in addition to the actual runtime
/// of the command.
fn time_overhead(&self) -> Second; fn time_overhead(&self) -> Second;
} }
fn run_command_and_measure_common(
mut command: std::process::Command,
command_failure_action: CmdFailureAction,
command_output_policy: CommandOutputPolicy,
command_name: &str,
) -> Result<TimerResult> {
let (stdout, stderr) = command_output_policy.get_stdout_stderr();
command.stdin(Stdio::null()).stdout(stdout).stderr(stderr);
command.env(
"HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET",
randomized_environment_offset::value(),
);
let result = execute_and_measure(command)
.with_context(|| format!("Failed to run command '{}'", command_name))?;
if command_failure_action == CmdFailureAction::RaiseError && !result.status.success() {
bail!(
"{}. Use the '-i'/'--ignore-failure' option if you want to ignore this. \
Alternatively, use the '--show-output' option to debug what went wrong.",
result.status.code().map_or(
"The process has been terminated by a signal".into(),
|c| format!("Command terminated with non-zero exit code: {}", c)
)
);
}
Ok(result)
}
pub struct ShellExecutor<'a> { pub struct ShellExecutor<'a> {
options: &'a Options, options: &'a Options,
shell: &'a Shell, shell: &'a Shell,
@@ -31,14 +71,6 @@ pub struct ShellExecutor<'a> {
} }
impl<'a> ShellExecutor<'a> { impl<'a> ShellExecutor<'a> {
fn subtract_shell_spawning_time(&self, time: Second, shell_spawning_time: Second) -> Second {
if time < shell_spawning_time {
0.0
} else {
time - shell_spawning_time
}
}
pub fn new(shell: &'a Shell, options: &'a Options) -> Self { pub fn new(shell: &'a Shell, options: &'a Options) -> Self {
ShellExecutor { ShellExecutor {
shell, shell,
@@ -49,50 +81,35 @@ impl<'a> ShellExecutor<'a> {
} }
impl<'a> Executor for ShellExecutor<'a> { impl<'a> Executor for ShellExecutor<'a> {
/// Run the given shell command and measure the execution time fn run_command_and_measure(
fn time_command(
&self, &self,
command: &Command<'_>, command: &Command<'_>,
command_failure_action: Option<CmdFailureAction>, command_failure_action: Option<CmdFailureAction>,
) -> Result<(TimingResult, ExitStatus)> { ) -> Result<(TimingResult, ExitStatus)> {
let (stdout, stderr) = match self.options.command_output_policy { let mut command_builder = self.shell.command();
CommandOutputPolicy::Discard => (Stdio::null(), Stdio::null()), command_builder
CommandOutputPolicy::Forward => (Stdio::inherit(), Stdio::inherit()), .arg(if cfg!(windows) { "/C" } else { "-c" })
}; .arg(command.get_command_line());
let wallclock_timer = WallClockTimer::start(); let mut result = run_command_and_measure_common(
let result = execute_and_time(stdout, stderr, &command.get_shell_command(), self.shell)?; command_builder,
let mut time_real = wallclock_timer.stop(); command_failure_action.unwrap_or(self.options.command_failure_action),
self.options.command_output_policy,
let mut time_user = result.user_time; &command.get_command_line(),
let mut time_system = result.system_time; )?;
if command_failure_action.unwrap_or(self.options.command_failure_action)
== CmdFailureAction::RaiseError
&& !result.status.success()
{
bail!(
"{}. Use the '-i'/'--ignore-failure' option if you want to ignore this. \
Alternatively, use the '--show-output' option to debug what went wrong.",
result.status.code().map_or(
"The process has been terminated by a signal".into(),
|c| format!("Command terminated with non-zero exit code: {}", c)
)
);
}
// Subtract shell spawning time // Subtract shell spawning time
if let Some(spawning_time) = self.shell_spawning_time { if let Some(spawning_time) = self.shell_spawning_time {
time_real = self.subtract_shell_spawning_time(time_real, spawning_time.time_real); result.time_real = (result.time_real - spawning_time.time_real).max(0.0);
time_user = self.subtract_shell_spawning_time(time_user, spawning_time.time_user); result.time_user = (result.time_user - spawning_time.time_user).max(0.0);
time_system = self.subtract_shell_spawning_time(time_system, spawning_time.time_system); result.time_system = (result.time_system - spawning_time.time_system).max(0.0);
} }
Ok(( Ok((
TimingResult { TimingResult {
time_real, time_real: result.time_real,
time_user, time_user: result.time_user,
time_system, time_system: result.time_system,
}, },
result.status, result.status,
)) ))
@@ -117,7 +134,7 @@ impl<'a> Executor for ShellExecutor<'a> {
for _ in 0..COUNT { for _ in 0..COUNT {
// Just run the shell without any command // Just run the shell without any command
let res = self.time_command(&Command::new(None, ""), None); let res = self.run_command_and_measure(&Command::new(None, ""), None);
match res { match res {
Err(_) => { Err(_) => {
@@ -183,7 +200,7 @@ impl MockExecutor {
} }
impl Executor for MockExecutor { impl Executor for MockExecutor {
fn time_command( fn run_command_and_measure(
&self, &self,
command: &Command<'_>, command: &Command<'_>,
_command_failure_action: Option<CmdFailureAction>, _command_failure_action: Option<CmdFailureAction>,
@@ -202,7 +219,7 @@ impl Executor for MockExecutor {
Ok(( Ok((
TimingResult { TimingResult {
time_real: Self::extract_time(command.get_shell_command()), time_real: Self::extract_time(command.get_command_line()),
time_user: 0.0, time_user: 0.0,
time_system: 0.0, time_system: 0.0,
}, },
+4 -4
View File
@@ -57,7 +57,7 @@ impl<'a> Benchmark<'a> {
error_output: &'static str, error_output: &'static str,
) -> Result<TimingResult> { ) -> Result<TimingResult> {
self.executor self.executor
.time_command(command, Some(CmdFailureAction::RaiseError)) .run_command_and_measure(command, Some(CmdFailureAction::RaiseError))
.map(|r| r.0) .map(|r| r.0)
.map_err(|_| anyhow!(error_output)) .map_err(|_| anyhow!(error_output))
} }
@@ -163,7 +163,7 @@ impl<'a> Benchmark<'a> {
for _ in 0..self.options.warmup_count { for _ in 0..self.options.warmup_count {
let _ = run_preparation_command()?; let _ = run_preparation_command()?;
let _ = self.executor.time_command(self.command, None)?; let _ = self.executor.run_command_and_measure(self.command, None)?;
if let Some(bar) = progress_bar.as_ref() { if let Some(bar) = progress_bar.as_ref() {
bar.inc(1) bar.inc(1)
} }
@@ -189,7 +189,7 @@ impl<'a> Benchmark<'a> {
preparation_result.map_or(0.0, |res| res.time_real + self.executor.time_overhead()); preparation_result.map_or(0.0, |res| res.time_real + self.executor.time_overhead());
// Initial timing run // Initial timing run
let (res, status) = self.executor.time_command(self.command, None)?; let (res, status) = self.executor.run_command_and_measure(self.command, None)?;
let success = status.success(); let success = status.success();
// Determine number of benchmark runs // Determine number of benchmark runs
@@ -239,7 +239,7 @@ impl<'a> Benchmark<'a> {
bar.set_message(msg.to_owned()) bar.set_message(msg.to_owned())
} }
let (res, status) = self.executor.time_command(self.command, None)?; let (res, status) = self.executor.run_command_and_measure(self.command, None)?;
let success = status.success(); let success = status.success();
times_real.push(res.time_real); times_real.push(res.time_real);
+12 -12
View File
@@ -55,12 +55,12 @@ impl<'a> Command<'a> {
pub fn get_name(&self) -> String { pub fn get_name(&self) -> String {
self.name.map_or_else( self.name.map_or_else(
|| self.get_shell_command(), || self.get_command_line(),
|name| self.replace_parameters_in(name), |name| self.replace_parameters_in(name),
) )
} }
pub fn get_shell_command(&self) -> String { pub fn get_command_line(&self) -> String {
self.replace_parameters_in(self.expression) self.replace_parameters_in(self.expression)
} }
@@ -82,7 +82,7 @@ impl<'a> Command<'a> {
// //
// hyperfine -L foo 'a,{bar}' -L bar 'baz,quux' 'echo {foo} {bar}' // hyperfine -L foo 'a,{bar}' -L bar 'baz,quux' 'echo {foo} {bar}'
// //
// should not ever run 'echo baz baz'. See `test_get_shell_command_nonoverlapping`. // should not ever run 'echo baz baz'. See `test_get_command_line_nonoverlapping`.
'outer: while let Some(head) = remaining.chars().next() { 'outer: while let Some(head) = remaining.chars().next() {
for (k, v) in &replacements { for (k, v) in &replacements {
if remaining.starts_with(k.as_str()) { if remaining.starts_with(k.as_str()) {
@@ -318,7 +318,7 @@ impl<'a> Commands<'a> {
} }
#[test] #[test]
fn test_get_shell_command_nonoverlapping() { fn test_get_command_line_nonoverlapping() {
let cmd = Command::new_parametrized( let cmd = Command::new_parametrized(
None, None,
"echo {foo} {bar}", "echo {foo} {bar}",
@@ -327,7 +327,7 @@ fn test_get_shell_command_nonoverlapping() {
("bar", ParameterValue::Text("quux".into())), ("bar", ParameterValue::Text("quux".into())),
], ],
); );
assert_eq!(cmd.get_shell_command(), "echo {bar} baz quux"); assert_eq!(cmd.get_command_line(), "echo {bar} baz quux");
} }
#[test] #[test]
@@ -345,7 +345,7 @@ fn test_get_parameterized_command_name() {
impl<'a> fmt::Display for Command<'a> { impl<'a> fmt::Display for Command<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.get_shell_command()) write!(f, "{}", self.get_command_line())
} }
} }
@@ -404,8 +404,8 @@ fn test_build_parameter_list_commands() {
assert_eq!(commands.len(), 2); assert_eq!(commands.len(), 2);
assert_eq!(commands[0].get_name(), "name-1"); assert_eq!(commands[0].get_name(), "name-1");
assert_eq!(commands[1].get_name(), "name-2"); assert_eq!(commands[1].get_name(), "name-2");
assert_eq!(commands[0].get_shell_command(), "echo 1"); assert_eq!(commands[0].get_command_line(), "echo 1");
assert_eq!(commands[1].get_shell_command(), "echo 2"); assert_eq!(commands[1].get_command_line(), "echo 2");
} }
#[test] #[test]
@@ -427,8 +427,8 @@ fn test_build_parameter_scan_commands() {
assert_eq!(commands.len(), 2); assert_eq!(commands.len(), 2);
assert_eq!(commands[0].get_name(), "name-1"); assert_eq!(commands[0].get_name(), "name-1");
assert_eq!(commands[1].get_name(), "name-2"); assert_eq!(commands[1].get_name(), "name-2");
assert_eq!(commands[0].get_shell_command(), "echo 1"); assert_eq!(commands[0].get_command_line(), "echo 1");
assert_eq!(commands[1].get_shell_command(), "echo 2"); assert_eq!(commands[1].get_command_line(), "echo 2");
} }
#[test] #[test]
@@ -444,7 +444,7 @@ fn test_parameter_scan_commands_int() {
.unwrap(); .unwrap();
assert_eq!(commands.len(), 3); assert_eq!(commands.len(), 3);
assert_eq!(commands[2].get_name(), "echo 7"); assert_eq!(commands[2].get_name(), "echo 7");
assert_eq!(commands[2].get_shell_command(), "echo 7"); assert_eq!(commands[2].get_command_line(), "echo 7");
} }
#[test] #[test]
@@ -464,7 +464,7 @@ fn test_parameter_scan_commands_decimal() {
.unwrap(); .unwrap();
assert_eq!(commands.len(), 4); assert_eq!(commands.len(), 4);
assert_eq!(commands[3].get_name(), "echo 0.99"); assert_eq!(commands[3].get_name(), "echo 0.99");
assert_eq!(commands[3].get_shell_command(), "echo 0.99"); assert_eq!(commands[3].get_command_line(), "echo 0.99");
} }
#[test] #[test]
-1
View File
@@ -18,7 +18,6 @@ pub mod options;
pub mod outlier_detection; pub mod outlier_detection;
pub mod output; pub mod output;
pub mod parameter; pub mod parameter;
pub mod shell;
pub mod timer; pub mod timer;
pub mod util; pub mod util;
+10 -1
View File
@@ -1,4 +1,4 @@
use std::process::Command; use std::process::{Command, Stdio};
use std::{cmp, fmt}; use std::{cmp, fmt};
use anyhow::ensure; use anyhow::ensure;
@@ -124,6 +124,15 @@ impl Default for CommandOutputPolicy {
} }
} }
impl CommandOutputPolicy {
pub fn get_stdout_stderr(&self) -> (Stdio, Stdio) {
match self {
CommandOutputPolicy::Discard => (Stdio::null(), Stdio::null()),
CommandOutputPolicy::Forward => (Stdio::inherit(), Stdio::inherit()),
}
}
}
pub enum ExecutorKind { pub enum ExecutorKind {
Shell(Shell), Shell(Shell),
Mock(Option<String>), Mock(Option<String>),
-87
View File
@@ -1,87 +0,0 @@
use std::process::{ExitStatus, Stdio};
use crate::options::Shell;
use anyhow::{Context, Result};
/// Used to indicate the result of running a command
#[derive(Debug, Copy, Clone)]
pub struct ExecuteResult {
/// The amount of user time the process used
pub user_time: f64,
/// The amount of cpu time the process used
pub system_time: f64,
/// The exit status of the process
pub status: ExitStatus,
}
fn randomized_environment_offset_value() -> String {
"X".repeat(rand::random::<usize>() % 4096usize)
}
/// Execute the given command and return a timing summary
#[cfg(not(windows))]
pub fn execute_and_time(
stdout: Stdio,
stderr: Stdio,
command: &str,
shell: &Shell,
) -> Result<ExecuteResult> {
let cpu_timer = crate::timer::unix_timer::CPUTimer::start();
let status = shell
.command()
.arg("-c")
.arg(command)
.env(
"HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET",
randomized_environment_offset_value(),
)
.stdin(Stdio::null())
.stdout(stdout)
.stderr(stderr)
.status()
.with_context(|| format!("Failed to run command '{}'", command))?;
let (user_time, system_time) = cpu_timer.stop();
Ok(ExecuteResult {
user_time,
system_time,
status,
})
}
/// Execute the given command and return a timing summary
#[cfg(windows)]
pub fn execute_and_time(
stdout: Stdio,
stderr: Stdio,
command: &str,
shell: &Shell,
) -> Result<ExecuteResult> {
let mut child = shell
.command()
.arg("/C")
.arg(command)
.env(
"HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET",
randomized_environment_offset_value(),
)
.stdin(Stdio::null())
.stdout(stdout)
.stderr(stderr)
.spawn()
.with_context(|| format!("Failed to run command '{}'", command))?;
let cpu_timer = crate::timer::windows_timer::CPUTimer::start_for_process(&child);
let status = child.wait()?;
let (user_time, system_time) = cpu_timer.stop();
Ok(ExecuteResult {
user_time,
system_time,
status,
})
}
+48 -11
View File
@@ -1,15 +1,20 @@
use crate::util::units::Second; mod wall_clock_timer;
pub mod wallclocktimer;
#[cfg(windows)] #[cfg(windows)]
pub mod windows_timer; mod windows_timer;
#[cfg(not(windows))] #[cfg(not(windows))]
pub mod unix_timer; mod unix_timer;
use crate::util::units::Second;
use wall_clock_timer::WallClockTimer;
use std::process::{Command, ExitStatus};
use anyhow::Result;
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct CPUTimes { struct CPUTimes {
/// Total amount of time spent executing in user mode /// Total amount of time spent executing in user mode
pub user_usec: i64, pub user_usec: i64,
@@ -17,11 +22,43 @@ pub struct CPUTimes {
pub system_usec: i64, pub system_usec: i64,
} }
/// Used to indicate the result of running a command
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct CPUInterval { pub struct TimerResult {
/// Total amount of time spent executing in user mode pub time_real: Second,
pub user: Second, pub time_user: Second,
pub time_system: Second,
/// Total amount of time spent executing in kernel mode /// The exit status of the process
pub system: Second, pub status: ExitStatus,
}
/// Execute the given command and return a timing summary
pub fn execute_and_measure(mut command: Command) -> Result<TimerResult> {
let wallclock_timer = WallClockTimer::start();
#[cfg(not(windows))]
let ((time_user, time_system), status) = {
let cpu_timer = self::unix_timer::CPUTimer::start();
let status = command.status()?;
(cpu_timer.stop(), status)
};
#[cfg(windows)]
let ((time_user, time_system), status) = {
let mut child = command.spawn()?;
let cpu_timer = self::windows_timer::CPUTimer::start_for_process(&child);
let status = child.wait()?;
(cpu_timer.stop(), status)
};
let time_real = wallclock_timer.stop();
Ok(TimerResult {
time_real,
time_user,
time_system,
status,
})
} }
+10 -1
View File
@@ -2,9 +2,18 @@
use std::mem; use std::mem;
use crate::timer::{CPUInterval, CPUTimes}; use crate::timer::CPUTimes;
use crate::util::units::Second; use crate::util::units::Second;
#[derive(Debug, Copy, Clone)]
pub struct CPUInterval {
/// Total amount of time spent executing in user mode
pub user: Second,
/// Total amount of time spent executing in kernel mode
pub system: Second,
}
pub struct CPUTimer { pub struct CPUTimer {
start_cpu: CPUTimes, start_cpu: CPUTimes,
} }
File renamed without changes.
+1
View File
@@ -1,4 +1,5 @@
pub mod exit_code; pub mod exit_code;
pub mod min_max; pub mod min_max;
pub mod number; pub mod number;
pub mod randomized_environment_offset;
pub mod units; pub mod units;
@@ -0,0 +1,8 @@
/// Returns a string with a random length. This value will be set as an environment
/// variable to account for offset effects. See [1] for more details.
///
/// [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)
}
+12
View File
@@ -147,6 +147,18 @@ fn can_run_failing_commands_with_ignore_failure_option() {
.success(); .success();
} }
#[test]
fn shows_output_of_benchmarked_command() {
hyperfine()
.arg("--runs=2")
.arg("--command-name=dummy")
.arg("--show-output")
.arg("echo 4fd47015")
.assert()
.success()
.stdout(predicate::str::contains("4fd47015").count(2));
}
#[test] #[test]
fn runs_commands_using_user_defined_shell() { fn runs_commands_using_user_defined_shell() {
hyperfine() hyperfine()