#[cfg(target_os = "linux")]
mod cgroup;
mod measure;
mod progress;
mod report;
mod sampler;
mod stats;
use anyhow::{Context, Result};
use clap::Parser;
use std::process::{Command, Stdio};
use std::time::Duration;
use progress::Progress;
use stats::BenchResult;
#[derive(Parser, Debug)]
#[command(
name = "hyperfoot",
version,
about = "Benchmark the resource footprint of commands"
)]
struct Cli {
#[arg(required = true, num_args = 1..)]
commands: Vec<String>,
#[arg(short = 'r', long, default_value_t = 10)]
runs: u32,
#[arg(short = 'w', long, default_value_t = 0)]
warmup: u32,
#[arg(long)]
prepare: Option<String>,
#[arg(long, default_value = "sh")]
shell: String,
#[arg(long)]
export_json: Option<String>,
#[arg(long)]
export_csv: Option<String>,
#[arg(long)]
export_markdown: Option<String>,
}
fn main() -> Result<()> {
restore_default_sigpipe();
let cli = Cli::parse();
run(cli)
}
fn restore_default_sigpipe() {
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
}
fn run(cli: Cli) -> Result<()> {
let mut results = Vec::with_capacity(cli.commands.len());
let progress = Progress::new(&cli.commands);
let total_commands = cli.commands.len();
for (command_index, command) in cli.commands.iter().enumerate() {
let position = (command_index, total_commands);
for w in 0..cli.warmup {
progress.warmup(command, position, w + 1, cli.warmup);
run_prepare(&cli.shell, cli.prepare.as_deref())?;
measure::measure_once(&cli.shell, command)
.with_context(|| format!("failed to run warmup for `{command}`"))?;
}
let mut runs = Vec::with_capacity(cli.runs as usize);
let mut total_wall = Duration::ZERO;
for i in 0..cli.runs {
let mean_so_far = (i > 0).then(|| total_wall / i);
progress.run(command, position, i + 1, cli.runs, mean_so_far);
run_prepare(&cli.shell, cli.prepare.as_deref())?;
let stats = measure::measure_once(&cli.shell, command)
.with_context(|| format!("failed to run `{command}`"))?;
total_wall += stats.wall_time;
runs.push(stats);
}
results.push(BenchResult {
command: command.clone(),
runs,
});
}
progress.finish();
if results.len() == 1 {
report::print_single(&results[0]);
} else {
report::print_comparison(&results);
}
if let Some(path) = &cli.export_json {
report::export_json(&results, path).with_context(|| format!("failed to write {path}"))?;
}
if let Some(path) = &cli.export_csv {
report::export_csv(&results, path).with_context(|| format!("failed to write {path}"))?;
}
if let Some(path) = &cli.export_markdown {
report::export_markdown(&results, path)
.with_context(|| format!("failed to write {path}"))?;
}
Ok(())
}
fn run_prepare(shell: &str, prepare: Option<&str>) -> Result<()> {
let Some(prepare) = prepare else {
return Ok(());
};
let status = Command::new(shell)
.arg("-c")
.arg(prepare)
.stdin(Stdio::null())
.status()
.with_context(|| format!("failed to run --prepare command `{prepare}`"))?;
if !status.success() {
anyhow::bail!("--prepare command `{prepare}` exited with {status}");
}
Ok(())
}