use clap::Parser;
use std::process::{Command, ExitStatus, Stdio};
const STDOUT: &str = "out.chain";
const SORTING: [&str; 2] = [
"target/release/chaintools sort -c {chain} -o out.chain --sort-by score --threads 16",
"./bench/chainSort {chain} out.chain",
];
const FILTER: [&str; 2] = [
"target/release/chaintools filter -c {chain} -o out.chain --min-score 15000 --min-target-start 12400 --threads 16",
"./bench/chainFilter {chain} -minScore=15000 -tStartMin=12400 > out.chain",
];
const MERGE_SORT: [&str; 2] = [
"target/release/chaintools split -c {chain} --files 10 -o . && target/release/chaintools merge -c chains/* -o out.chain --threads 16 -S score && rm -rf chains",
"target/release/chaintools split -c {chain} --files 10 -o . && ./bench/chainMergeSort chains/* > out.chain && rm -rf chains",
];
const SWAP: [&str; 2] = [
"target/release/chaintools swap -c {chain} -o out.chain",
"./bench/chainSwap {chain} out.chain",
];
const LIFTOVER: [&str; 2] = [
"target/release/chaintools liftover -c {chain} -b bench.bed --type 3 -o out.bed -u unmapped.bed",
"./bench/liftOver bench.bed {chain} out.bed unmapped.bed",
];
const LIFTOVER_BED: &str = "bench.bed";
const LIFTOVER_BED_SOURCE: &str = "bench.bed.source";
const LIFTOVER_BED_STRIDE: u64 = 10;
#[derive(Debug, Parser)]
pub struct Args {
#[clap(short = 'c', long = "chain", help = "Path to chain file")]
chain: String,
#[clap(short = 'a',
value_delimiter = ',',
num_args = 1..,
help = "Extra arguments to pass to hyperfine"
)]
hyperfine_args: Vec<String>,
#[clap(
long = "only",
value_delimiter = ',',
num_args = 1..,
help = "Only run the named benchmarks (sorting, filter, merge_sort, swap, liftover)"
)]
only: Vec<String>,
}
pub struct HyperfineCall {
pub warmup: u32,
pub min_runs: u32,
pub max_runs: Option<u32>,
pub export_csv: Option<String>,
pub export_markdown: Option<String>,
pub parameters: Vec<(String, Vec<String>)>,
pub setup: Option<String>,
pub cleanup: Option<String>,
pub commands: Vec<String>,
pub extras: Vec<String>,
}
impl Default for HyperfineCall {
fn default() -> Self {
Self {
warmup: 3,
min_runs: 5,
max_runs: None,
export_csv: None,
export_markdown: None,
parameters: Vec::new(),
setup: None,
cleanup: None,
commands: Vec::new(),
extras: Vec::new(),
}
}
}
impl HyperfineCall {
pub fn invoke(&self) -> ExitStatus {
let mut command = Command::new("hyperfine");
command
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.stdin(Stdio::null());
command.arg("--warmup").arg(self.warmup.to_string());
command.arg("--min-runs").arg(self.min_runs.to_string());
if let Some(export_csv) = &self.export_csv {
command.arg("--export-csv").arg(export_csv);
}
if let Some(export_markdown) = &self.export_markdown {
command.arg("--export-markdown").arg(export_markdown);
}
for (flag, values) in &self.parameters {
command.arg("-L").arg(flag).arg(values.join(","));
}
if let Some(setup) = &self.setup {
command.arg("--setup").arg(setup);
}
if let Some(cleanup) = &self.cleanup {
command.arg("--cleanup").arg(cleanup);
}
if let Some(max_runs) = self.max_runs {
command.arg("--max-runs").arg(max_runs.to_string());
}
if !self.extras.is_empty() {
command.args(&self.extras);
}
for cmd in &self.commands {
command.arg(cmd);
}
command.status().expect("Failed to run hyperfine")
}
}
fn benchmark() {
let args = Args::parse();
std::fs::create_dir_all("runs").unwrap_or_else(|e| panic!("{}", e));
prepare_liftover_bed(&args.chain);
let triplets = vec![
(&args.chain, SORTING.to_vec(), "sorting", 3, 3, 10),
(&args.chain, FILTER.to_vec(), "filter", 3, 3, 10),
(&args.chain, MERGE_SORT.to_vec(), "merge_sort", 3, 3, 10),
(&args.chain, SWAP.to_vec(), "swap", 3, 3, 10),
(&args.chain, LIFTOVER.to_vec(), "liftover", 3, 3, 10),
];
for (chain, tools, run_name, warmup, min_runs, max_runs) in triplets {
if !args.only.is_empty() && !args.only.iter().any(|name| name == run_name) {
continue;
}
let csv = format!("bench_{}.csv", run_name);
let md = format!("bench_{}.md", run_name);
#[allow(clippy::needless_update)]
let code = HyperfineCall {
warmup,
min_runs,
max_runs: Some(max_runs),
export_csv: Some(format!("runs/{}", csv).to_string()),
export_markdown: Some(format!("runs/{}", md).to_string()),
parameters: vec![("chain".to_string(), vec![chain.to_string()])],
setup: Some("cargo build --release --all-features".to_string()),
cleanup: Some(format!(
"rm -rf output {} chains out.bed unmapped.bed",
STDOUT
)),
commands: tools
.iter()
.map(|cmd| cmd.to_string())
.collect::<Vec<String>>(),
extras: args
.hyperfine_args
.iter()
.map(|s| format!("--{}", s))
.collect(),
..Default::default()
}
.invoke()
.code()
.expect("Benchmark terminated unexpectedly");
if code != 0 {
eprintln!("Benchmark failed with exit code {}", code);
}
}
}
fn prepare_liftover_bed(chain: &str) {
let built_from_this_chain = std::fs::read_to_string(LIFTOVER_BED_SOURCE)
.is_ok_and(|source| source.trim() == chain.trim());
if std::path::Path::new(LIFTOVER_BED).exists() && built_from_this_chain {
eprintln!("reusing existing {LIFTOVER_BED}");
return;
}
build_release();
eprintln!("building {LIFTOVER_BED} from {chain}");
let script = format!(
"target/release/chaintools --level off bed -c {chain} --side reference --type 12 \
| awk 'BEGIN{{OFS=\"\\t\"}} \
{{n=split($11,sz,\",\"); split($12,st,\",\"); \
for(i=1;i<=n;i++) if(sz[i]!=\"\") {{k++; if(k%{LIFTOVER_BED_STRIDE}==0) \
print $1, $2+st[i], $2+st[i]+sz[i]}}}}' > {LIFTOVER_BED}"
);
let status = Command::new("sh")
.arg("-c")
.arg(&script)
.status()
.expect("Failed to build the liftover BED workload");
assert!(status.success(), "building {LIFTOVER_BED} failed");
std::fs::write(LIFTOVER_BED_SOURCE, chain).expect("Failed to record the BED source chain");
}
fn build_release() {
let status = Command::new("cargo")
.args(["build", "--release", "--all-features"])
.status()
.expect("Failed to run cargo build");
assert!(
status.success(),
"cargo build --release --all-features failed"
);
}
fn main() {
benchmark();
}