#![cfg_attr(
not(any(feature = "precise-timing", feature = "alloc-profiling")),
forbid(unsafe_code)
)]
#![cfg_attr(
any(feature = "precise-timing", feature = "alloc-profiling"),
deny(unsafe_code)
)]
#![doc = include_str!("../README.md")]
#[cfg(feature = "alloc-profiling")]
mod alloc;
pub mod baseline;
mod bench;
pub mod calibration;
#[cfg(feature = "charts")]
pub mod charts;
mod checks;
mod ci;
#[cfg(feature = "criterion-compat")]
pub mod criterion_compat;
pub mod daemon;
mod engine;
pub mod exclusive;
mod format;
mod gate;
mod html;
pub mod mcp;
pub mod platform;
pub mod quickchart;
mod report;
mod results;
mod stats;
#[cfg(feature = "precise-timing")]
mod timing;
#[cfg(feature = "wasm")]
pub mod wasm;
pub use bench::{BenchGroup, Bencher, GroupConfig, Suite, Throughput};
#[doc(hidden)]
pub fn postprocess_result(result: &SuiteResult) {
let args: Vec<String> = std::env::args().collect();
let format = args
.iter()
.find_map(|a| a.strip_prefix("--format=").map(String::from))
.or_else(|| std::env::var("ZENBENCH_FORMAT").ok());
let save_baseline: Option<String> = args
.iter()
.find_map(|a| a.strip_prefix("--save-baseline=").map(String::from));
let baseline_name: Option<String> = args
.iter()
.find_map(|a| a.strip_prefix("--baseline=").map(String::from));
let max_regression: f64 = args
.iter()
.find_map(|a| {
a.strip_prefix("--max-regression=")
.and_then(|v| v.parse().ok())
})
.unwrap_or(5.0);
let update_on_pass = args.iter().any(|a| a == "--update-on-pass");
match format.as_deref() {
Some("llm") => print!("{}", result.to_llm()),
Some("csv") => print!("{}", result.to_csv()),
Some("markdown" | "md") => print!("{}", result.to_markdown()),
Some("html") => print!("{}", result.to_html()),
Some("json") => {
if let Ok(json) = serde_json::to_string_pretty(result) {
println!("{json}");
}
}
_ => {} }
if let Some(ref name) = save_baseline {
match baseline::save_baseline(result, name) {
Ok(path) => eprintln!("[zenbench] baseline '{name}' saved to {}", path.display()),
Err(e) => {
eprintln!("[zenbench] error saving baseline '{name}': {e}");
std::process::exit(2);
}
}
}
if let Some(ref name) = baseline_name {
match baseline::load_baseline(name) {
Ok(saved) => {
let comparison = baseline::compare_against_baseline(&saved, result, max_regression);
baseline::print_comparison_report(&comparison);
if comparison.regressions > 0 {
eprintln!(
"\n[zenbench] FAIL: {} regression(s) exceed {max_regression}% threshold",
comparison.regressions,
);
std::process::exit(1);
} else {
eprintln!(
"\n[zenbench] PASS: no regressions exceed {max_regression}% threshold"
);
if update_on_pass {
match baseline::save_baseline(result, name) {
Ok(path) => eprintln!(
"[zenbench] baseline '{name}' updated (--update-on-pass) → {}",
path.display()
),
Err(e) => {
eprintln!("[zenbench] warning: failed to update baseline: {e}");
}
}
}
}
}
Err(e) => {
eprintln!("[zenbench] {e}");
std::process::exit(2);
}
}
}
if let Some(path) = daemon::result_path_from_env() {
if let Err(e) = result.save(&path) {
eprintln!("[zenbench] error saving results: {e}");
}
}
}
#[cfg(feature = "alloc-profiling")]
pub use alloc::{AllocProfiler, AllocStats};
#[doc(hidden)]
pub fn engine_new(suite: Suite) -> engine::Engine {
engine::Engine::new(suite)
}
pub use format::format_ns;
pub use gate::GateConfig;
pub use platform::Testbed;
pub use results::{BenchmarkResult, ComparisonResult, RunId, SuiteResult};
pub use stats::{MeanCi, PairedAnalysis, Summary};
#[inline(always)]
pub fn black_box<T>(x: T) -> T {
std::hint::black_box(x)
}
pub mod prelude {
pub use crate::bench::{BenchGroup, Bencher, GroupConfig, Suite, Throughput};
pub use crate::black_box;
pub use crate::gate::GateConfig;
pub use crate::results::SuiteResult;
pub use crate::stats::{MeanCi, PairedAnalysis, Summary};
}
pub fn run<F: FnOnce(&mut Suite)>(f: F) -> SuiteResult {
let mut suite = Suite::new();
f(&mut suite);
let engine = engine::Engine::new(suite);
engine.run()
}
#[derive(Clone, Copy, Debug)]
pub enum Aggregation {
Best,
Mean,
Median,
}
pub fn run_passes<F: FnMut(&mut Suite)>(
passes: usize,
policy: Aggregation,
mut f: F,
) -> SuiteResult {
if passes <= 1 {
let mut suite = Suite::new();
f(&mut suite);
let engine = engine::Engine::new(suite);
return engine.run();
}
let mut all_results: Vec<SuiteResult> = Vec::with_capacity(passes);
for i in 0..passes {
eprintln!("[zenbench] pass {}/{}", i + 1, passes);
let mut suite = Suite::new();
f(&mut suite);
let engine = engine::Engine::new(suite).quiet(true);
all_results.push(engine.run());
}
let aggregated = aggregate_results(all_results, policy);
report::print_header(
&aggregated.run_id,
aggregated.git_hash.as_deref(),
aggregated.ci_environment.as_deref(),
);
let banner = match policy {
Aggregation::Best => {
format!("[zenbench] best of {passes} passes (min pass mean; within-run mad preserved)")
}
Aggregation::Mean => {
format!("[zenbench] mean of {passes} passes (mad = inter-pass spread)")
}
Aggregation::Median => {
format!("[zenbench] median of {passes} passes (mad = inter-pass spread)")
}
};
eprintln!("{banner}");
for cmp in &aggregated.comparisons {
report::print_group(cmp, aggregated.timer_resolution_ns);
}
report::print_footer(
aggregated.total_time,
aggregated.gate_waits,
aggregated.gate_wait_time,
aggregated.unreliable,
);
aggregated
}
pub fn aggregate_results(runs: Vec<SuiteResult>, policy: Aggregation) -> SuiteResult {
use std::collections::HashMap;
if runs.is_empty() {
return SuiteResult::default();
}
if runs.len() == 1 {
return runs.into_iter().next().unwrap();
}
let mut means: HashMap<(String, String), Vec<f64>> = HashMap::new();
for result in &runs {
for cmp in &result.comparisons {
for bench in &cmp.benchmarks {
let key = (cmp.group_name.clone(), bench.name.clone());
means.entry(key).or_default().push(bench.summary.mean);
}
}
}
let winners: HashMap<(String, String), usize> =
runs.iter()
.enumerate()
.fold(HashMap::new(), |mut acc, (ri, result)| {
for cmp in &result.comparisons {
for bench in &cmp.benchmarks {
let key = (cmp.group_name.clone(), bench.name.clone());
let this_mean = bench.summary.mean;
acc.entry(key)
.and_modify(|best: &mut usize| {
let prev_mean =
run_mean(&runs, *best, &cmp.group_name, &bench.name);
if this_mean < prev_mean {
*best = ri;
}
})
.or_insert(ri);
}
}
acc
});
let mut out = runs[0].clone();
for cmp in out.comparisons.iter_mut() {
for bench in cmp.benchmarks.iter_mut() {
let key = (cmp.group_name.clone(), bench.name.clone());
let samples = match means.get(&key) {
Some(s) => s,
None => continue,
};
match policy {
Aggregation::Best => {
if let Some(&best_ri) = winners.get(&key) {
if let Some(best_bench) = find_bench(&runs[best_ri], &key.0, &key.1) {
bench.summary = best_bench.summary.clone();
}
}
}
Aggregation::Mean => {
bench.summary = crate::stats::Summary::from_slice(samples);
}
Aggregation::Median => {
let fresh = crate::stats::Summary::from_slice(samples);
bench.summary = fresh.clone();
bench.summary.mean = fresh.median;
}
}
bench.mean_ci = None;
}
}
out
}
fn find_bench<'a>(
result: &'a SuiteResult,
group: &str,
bench_name: &str,
) -> Option<&'a results::BenchmarkResult> {
result
.comparisons
.iter()
.find(|c| c.group_name == group)?
.benchmarks
.iter()
.find(|b| b.name == bench_name)
}
fn run_mean(results: &[SuiteResult], idx: usize, group: &str, bench_name: &str) -> f64 {
find_bench(&results[idx], group, bench_name)
.map(|b| b.summary.mean)
.unwrap_or(f64::INFINITY)
}
pub fn run_gated<F: FnOnce(&mut Suite)>(gate: GateConfig, f: F) -> SuiteResult {
let mut suite = Suite::new();
f(&mut suite);
let engine = engine::Engine::with_gate(suite, gate);
engine.run()
}
pub fn run_and_save<F: FnOnce(&mut Suite)>(f: F) -> SuiteResult {
let result = run(f);
let path = daemon::result_path_from_env().unwrap_or_else(|| {
let name = format!("zenbench-{}.json", result.run_id);
std::path::PathBuf::from(name)
});
if let Err(e) = result.save(&path) {
eprintln!("[zenbench] error saving results to {}: {e}", path.display());
} else {
eprintln!("[zenbench] results saved to {}", path.display());
}
result
}
#[doc(hidden)]
pub fn parse_pass_args() -> Option<(usize, Aggregation)> {
let mut found: Option<(usize, Aggregation, &'static str)> = None;
let flags: &[(&str, Aggregation, &str)] = &[
("--best-of-passes=", Aggregation::Best, "--best-of-passes"),
("--mean-of-passes=", Aggregation::Mean, "--mean-of-passes"),
(
"--median-of-passes=",
Aggregation::Median,
"--median-of-passes",
),
];
for arg in std::env::args() {
let parsed: Option<(usize, Aggregation, &'static str)> =
flags.iter().find_map(|(prefix, policy, name)| {
arg.strip_prefix(prefix)
.and_then(|v| v.parse().ok())
.map(|n: usize| (n, *policy, *name))
});
if let Some((n, p, name)) = parsed {
if let Some((_, _, prev_name)) = found {
eprintln!("[zenbench] error: {prev_name} and {name} are mutually exclusive");
std::process::exit(2);
}
if n == 0 {
eprintln!("[zenbench] error: {name}=0 is not meaningful");
std::process::exit(2);
}
found = Some((n, p, name));
}
}
found.map(|(n, p, _)| (n, p))
}
#[doc(hidden)]
pub fn parse_process_args() -> Option<(usize, Aggregation)> {
if std::env::var("ZENBENCH_SUBPROCESS").as_deref() == Ok("1") {
return None;
}
let mut found: Option<(usize, Aggregation, &'static str)> = None;
let flags: &[(&str, Aggregation, &str)] = &[
(
"--best-of-processes=",
Aggregation::Best,
"--best-of-processes",
),
(
"--mean-of-processes=",
Aggregation::Mean,
"--mean-of-processes",
),
(
"--median-of-processes=",
Aggregation::Median,
"--median-of-processes",
),
];
for arg in std::env::args() {
let parsed: Option<(usize, Aggregation, &'static str)> =
flags.iter().find_map(|(prefix, policy, name)| {
arg.strip_prefix(prefix)
.and_then(|v| v.parse().ok())
.map(|n: usize| (n, *policy, *name))
});
if let Some((n, p, name)) = parsed {
if let Some((_, _, prev_name)) = found {
eprintln!("[zenbench] error: {prev_name} and {name} are mutually exclusive");
std::process::exit(2);
}
if n == 0 {
eprintln!("[zenbench] error: {name}=0 is not meaningful");
std::process::exit(2);
}
found = Some((n, p, name));
}
}
found.map(|(n, p, _)| (n, p))
}
pub fn run_processes(processes: usize, policy: Aggregation) -> SuiteResult {
let exe = std::env::current_exe().unwrap_or_else(|e| {
eprintln!("[zenbench] error: cannot determine current executable: {e}");
std::process::exit(2);
});
let child_args: Vec<String> = std::env::args()
.skip(1) .filter(|a| {
!a.starts_with("--best-of-processes=")
&& !a.starts_with("--mean-of-processes=")
&& !a.starts_with("--median-of-processes=")
&& !a.starts_with("--format=")
&& !a.starts_with("--save-baseline=")
&& !a.starts_with("--baseline=")
&& !a.starts_with("--max-regression=")
&& a != "--update-on-pass"
})
.collect();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let pid = std::process::id();
let run_id = format!("{now:x}-{pid:x}");
let launcher_pids = match std::env::var("ZENBENCH_LAUNCHER_PIDS") {
Ok(existing) => format!("{existing},{pid}"),
Err(_) => pid.to_string(),
};
let temp_dir = std::env::temp_dir();
let temp_paths: Vec<std::path::PathBuf> = (0..processes)
.map(|i| temp_dir.join(format!("zenbench-proc-{run_id}-{i}.json")))
.collect();
let mut results: Vec<SuiteResult> = Vec::with_capacity(processes);
for (i, path) in temp_paths.iter().enumerate() {
eprintln!("[zenbench] process {}/{processes}", i + 1);
let status = std::process::Command::new(&exe)
.args(&child_args)
.env("ZENBENCH_SUBPROCESS", "1")
.env("ZENBENCH_RESULT_PATH", path)
.env("ZENBENCH_LAUNCHER_PIDS", &launcher_pids)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::inherit())
.status();
match status {
Ok(s) if s.success() => {}
Ok(s) => {
eprintln!("[zenbench] process {} exited with {s}", i + 1);
cleanup_temp(&temp_paths);
std::process::exit(1);
}
Err(e) => {
eprintln!("[zenbench] failed to spawn process {}: {e}", i + 1);
cleanup_temp(&temp_paths);
std::process::exit(1);
}
}
match SuiteResult::load(path) {
Ok(r) => results.push(r),
Err(e) => {
eprintln!("[zenbench] process {} produced no results: {e}", i + 1);
cleanup_temp(&temp_paths);
std::process::exit(1);
}
}
}
cleanup_temp(&temp_paths);
let aggregated = aggregate_results(results, policy);
report::print_header(
&aggregated.run_id,
aggregated.git_hash.as_deref(),
aggregated.ci_environment.as_deref(),
);
let policy_name = match policy {
Aggregation::Best => "best",
Aggregation::Mean => "mean",
Aggregation::Median => "median",
};
eprintln!("[zenbench] {policy_name} of {processes} processes (cross-OS-process isolation)");
for cmp in &aggregated.comparisons {
report::print_group(cmp, aggregated.timer_resolution_ns);
}
report::print_footer(
aggregated.total_time,
aggregated.gate_waits,
aggregated.gate_wait_time,
aggregated.unreliable,
);
aggregated
}
fn cleanup_temp(paths: &[std::path::PathBuf]) {
for p in paths {
let _ = std::fs::remove_file(p);
}
}
#[macro_export]
macro_rules! main {
($($func:path),+ $(,)?) => {
fn main() {
if let Some((n, policy)) = $crate::parse_process_args() {
let result = $crate::run_processes(n, policy);
$crate::postprocess_result(&result);
return;
}
let group_filter: Option<String> = std::env::args()
.find_map(|a| a.strip_prefix("--group=").map(String::from));
let passes = $crate::parse_pass_args();
let closure = |suite: &mut $crate::Suite| {
if let Some(ref filter) = group_filter {
suite.set_group_filter(filter.clone());
}
$( $func(suite); )+
};
let result = match passes {
Some((n, policy)) => $crate::run_passes(n, policy, closure),
None => $crate::run(closure),
};
$crate::postprocess_result(&result);
}
};
(|$suite:ident| $body:block) => {
fn main() {
if let Some((n, policy)) = $crate::parse_process_args() {
let result = $crate::run_processes(n, policy);
$crate::postprocess_result(&result);
return;
}
let group_filter: Option<String> = std::env::args()
.find_map(|a| a.strip_prefix("--group=").map(String::from));
let passes = $crate::parse_pass_args();
let closure = |$suite: &mut $crate::Suite| {
if let Some(ref filter) = group_filter {
$suite.set_group_filter(filter.clone());
}
$body
};
let result = match passes {
Some((n, policy)) => $crate::run_passes(n, policy, closure),
None => $crate::run(closure),
};
$crate::postprocess_result(&result);
}
};
}