Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ windows-sys = { version = "0.59", features = [
once_cell = "1.19"

[target.'cfg(target_os="linux")'.dependencies]
nix = { version = "0.29", features = ["zerocopy"] }
nix = { version = "0.29", features = ["zerocopy", "signal"] }

[dependencies.clap]
version = "4"
Expand Down
16 changes: 15 additions & 1 deletion src/benchmark/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ impl BenchmarkIteration {
BenchmarkIteration::Benchmark(i) => Some(format!("{}", i)),
}
}

/// Returns `true` if the benchmark iteration is [`NonBenchmarkRun`].
///
/// [`NonBenchmarkRun`]: BenchmarkIteration::NonBenchmarkRun
#[must_use]
pub fn is_non_benchmark_run(&self) -> bool {
matches!(self, Self::NonBenchmarkRun)
}
}

pub trait Executor {
Expand Down Expand Up @@ -65,6 +73,12 @@ fn run_command_and_measure_common(
) -> Result<TimerResult> {
let stdin = command_input_policy.get_stdin()?;
let (stdout, stderr) = command_output_policy.get_stdout_stderr()?;
let until_text = if iteration.is_non_benchmark_run() {
None
} else {
command_output_policy.get_until_text()
};

command.stdin(stdin).stdout(stdout).stderr(stderr);

command.env(
Expand All @@ -76,7 +90,7 @@ fn run_command_and_measure_common(
command.env("HYPERFINE_ITERATION", value);
}

let result = execute_and_measure(command)
let result = execute_and_measure(command, until_text)
.with_context(|| format!("Failed to run command '{command_name}'"))?;

if command_failure_action == CmdFailureAction::RaiseError && !result.status.success() {
Expand Down
12 changes: 11 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ fn build_command() -> Command {
.arg(
Arg::new("output")
.long("output")
.conflicts_with("show-output")
.conflicts_with_all(["show-output", "until"])
.action(ArgAction::Append)
.value_name("WHERE")
.help(
Expand All @@ -350,6 +350,16 @@ fn build_command() -> Command {
hyperfine 'my-command > output-${HYPERFINE_ITERATION}.log'\n\n",
),
)
.arg(
Arg::new("until")
.long("until")
.action(ArgAction::Set)
.conflicts_with_all(["show-output", "output"])
.help(
"Run the command until it prints the given output in stdout"
),

)
.arg(
Arg::new("input")
.long("input")
Expand Down
15 changes: 15 additions & 0 deletions src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ pub enum CommandOutputPolicy {

/// Show command output on the terminal
Inherit,

/// Read until the given text is matched and then exit.
Until(Vec<u8>),
}

impl CommandOutputPolicy {
Expand All @@ -172,10 +175,20 @@ impl CommandOutputPolicy {
}

CommandOutputPolicy::Inherit => (Stdio::inherit(), Stdio::inherit()),

CommandOutputPolicy::Until(_) => (Stdio::piped(), Stdio::null()),
};

Ok(streams)
}

pub fn get_until_text(&self) -> Option<&[u8]> {
if let CommandOutputPolicy::Until(v) = self {
Some(v.as_slice())
} else {
None
}
}
}

#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -341,6 +354,8 @@ impl Options {
policies.push(policy);
}
policies
} else if let Some(text) = matches.get_one::<String>("until") {
vec![CommandOutputPolicy::Until(text.as_bytes().to_vec())]
} else {
vec![CommandOutputPolicy::Null]
};
Expand Down
126 changes: 125 additions & 1 deletion src/timer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use nix::fcntl::{splice, SpliceFFlags};
use std::fs::File;
#[cfg(target_os = "linux")]
use std::os::fd::AsFd;
use std::os::unix::process::ExitStatusExt;

#[cfg(target_os = "windows")]
use windows_sys::Win32::System::Threading::CREATE_SUSPENDED;
Expand Down Expand Up @@ -79,8 +80,91 @@ fn discard(output: ChildStdout) {
}
}

fn discard_until(output: ChildStdout, ptn: &[u8]) -> Result<bool> {
const CHUNK_SIZE: usize = 64 << 10;

let mut output = output;
let mut buf = [0; CHUNK_SIZE];

let ptn_len = ptn.len();
let lps = compute_lps_array(ptn);
let mut j = 0; // position of the character in ptn
let mut read_more = false;

loop {
let n = output.read(&mut buf)?;

if n == 0 {
return Ok(false);
}

let mut i = 0; // position of the character in buf
if read_more && ptn[j] != buf[i] {
if j != 0 {
j = lps[j - 1];
} else {
i += 1;
}
}
read_more = false;

while i < n {
if ptn[j] == buf[i] {
i += 1;
j += 1;
}

if j == ptn_len {
return Ok(true);
}

if i == n {
read_more = true;
break;
}

if ptn[j] == buf[i] {
continue;
}

if j != 0 {
j = lps[j - 1];
} else {
i += 1;
}
}
}
}

#[inline(always)]
fn compute_lps_array(pattern: &[u8]) -> Vec<usize> {
let ptn_len = pattern.len();
let mut lps = vec![0; ptn_len];

// length of the previous longest prefix suffix
let mut len = 0;
lps[0] = 0;

// the loop calculates lps[i] for i = 1 to ptn_len-1
let mut i = 1;
while i < ptn_len {
if pattern[i] == pattern[len] {
len += 1;
lps[i] = len;
i += 1;
} else if len != 0 {
len = lps[len - 1];
} else {
lps[i] = 0;
i += 1;
}
}

lps
}

/// Execute the given command and return a timing summary
pub fn execute_and_measure(mut command: Command) -> Result<TimerResult> {
pub fn execute_and_measure(mut command: Command, until: Option<&[u8]>) -> Result<TimerResult> {
#[cfg(not(windows))]
let cpu_timer = self::unix_timer::CPUTimer::start();

Expand All @@ -101,6 +185,46 @@ pub fn execute_and_measure(mut command: Command) -> Result<TimerResult> {
unsafe { self::windows_timer::CPUTimer::start_suspended_process(&child) }
};

if let Some(ptn) = until {
// Handle CommandOutputPolicy::Until
let output = child
.stdout
.take()
.expect("Expected a pipe when until text is present.");

let status = if discard_until(output, ptn)? {
ExitStatus::from_raw(0)
} else {
ExitStatus::from_raw(-1)
};

let time_real = wallclock_timer.stop();
let (time_user, time_system, memory_usage_byte) = cpu_timer.stop();

#[cfg(unix)]
{
// child.kill() sends SIGKILL we don't really want that.
use nix::sys::signal::{self, Signal};
use nix::unistd::Pid;
signal::kill(Pid::from_raw(child.id() as i32), Signal::SIGTERM)?;
}

#[cfg(not(unix))]
{
child.kill()?;
}

child.wait()?;

return Ok(TimerResult {
time_real,
time_user,
time_system,
memory_usage_byte,
status,
});
}

if let Some(output) = child.stdout.take() {
// Handle CommandOutputPolicy::Pipe
discard(output);
Expand Down
Loading