use itertools::Itertools;
use std::{
env::current_dir,
io,
io::Write,
path::PathBuf,
process::{Command, Output, Stdio},
sync::mpsc::{channel, Receiver},
thread::JoinHandle,
time::{Duration, Instant},
};
pub struct Runner {
pub binary: PathBuf,
pub cli_args: Vec<String>,
pub runs: usize,
pub stop_rx: Option<Receiver<()>>,
pub warmup: bool,
pub print_initial: bool,
}
const CHUNK_SIZE: usize = 5;
pub const DEFAULT_RUNS: usize = 1_000;
impl Runner {
#[must_use]
pub fn new(
binary: PathBuf,
cli_args: Vec<String>,
runs: usize,
stop_rx: Option<Receiver<()>>,
warmup: bool,
print_initial: bool,
) -> Self {
Self {
binary,
cli_args,
runs,
stop_rx,
warmup,
print_initial,
}
}
#[must_use]
#[instrument(skip(self))]
pub fn start(self) -> (JoinHandle<io::Result<()>>, Receiver<Duration>) {
let Self {
runs,
binary,
cli_args,
stop_rx,
warmup,
print_initial,
} = self; let runs = runs - usize::from(!warmup);
let (duration_sender, duration_receiver) = channel();
let handle = std::thread::Builder::new()
.name("benchmark_runner".into()) .spawn(move || {
info!(%runs, ?binary, ?cli_args, ?warmup, "Starting benching.");
let mut command = Command::new(binary);
command.args(cli_args);
if let Ok(cd) = current_dir() {
command.current_dir(cd); }
let mut start = Instant::now();
{
let Output {
status,
stdout,
stderr,
} = command.output()?;
if !warmup {
let elapsed = start.elapsed();
duration_sender.send(elapsed).expect("error sending time");
}
if !status.success() {
error!(?status, "Initial Command failed");
return Ok(());
}
if !stdout.is_empty() && print_initial {
io::stdout().lock().write_all(&stdout)?;
}
if !stderr.is_empty() {
io::stderr().lock().write_all(&stderr)?;
}
}
command.stdout(Stdio::null()).stderr(Stdio::null());
for chunk_size in (0..runs)
.chunks(CHUNK_SIZE)
.into_iter()
.map(Iterator::count)
{
if stop_rx
.as_ref()
.map_or(true, |stop_recv| stop_recv.try_recv().is_err())
{
trace!(%chunk_size, "Starting batch.");
for _ in 0..chunk_size {
start = Instant::now();
let status = command.status()?;
duration_sender
.send(start.elapsed())
.expect("Error sending result");
if status.success() {
trace!(?status, "Finished command");
} else {
warn!(?status, "Command failed");
}
}
} else {
break; }
}
Ok(())
})
.expect("error creating thread");
(handle, duration_receiver)
}
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn calculate_mean_standard_deviation(runs: &[u128]) -> Option<(Duration, Duration)> {
if runs.is_empty() {
return None;
}
let len = runs.len() as f64;
let mut sum = 0;
let mut sum_of_squares = 0;
for item in runs {
sum += item;
sum_of_squares += item.pow(2);
}
let mean = (sum as f64) / len;
let variance = mean.mul_add(-mean, (sum_of_squares as f64) / len);
Some((
Duration::from_secs_f64(mean / 1_000_000.0),
Duration::from_secs_f64(variance.sqrt() / 1_000_000.0),
)) }