#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
use std::time::{Duration, Instant};
use dev_report::{CheckResult, Severity};
pub struct Benchmark {
name: String,
samples: Vec<Duration>,
}
impl Benchmark {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
samples: Vec::new(),
}
}
pub fn iter<F, R>(&mut self, f: F) -> R
where
F: FnOnce() -> R,
{
let start = Instant::now();
let r = f();
let elapsed = start.elapsed();
self.samples.push(elapsed);
r
}
pub fn finish(self) -> BenchmarkResult {
let n = self.samples.len();
let mean = if n == 0 {
Duration::ZERO
} else {
let total: Duration = self.samples.iter().copied().sum();
total / n as u32
};
let mut sorted = self.samples.clone();
sorted.sort();
let p50 = sorted.get(n / 2).copied().unwrap_or(Duration::ZERO);
let p99 = sorted
.get((n as f64 * 0.99).floor() as usize)
.copied()
.unwrap_or(Duration::ZERO);
BenchmarkResult {
name: self.name,
samples: self.samples,
mean,
p50,
p99,
}
}
}
#[derive(Debug, Clone)]
pub struct BenchmarkResult {
pub name: String,
pub samples: Vec<Duration>,
pub mean: Duration,
pub p50: Duration,
pub p99: Duration,
}
#[derive(Debug, Clone, Copy)]
pub enum Threshold {
RegressionPct(f64),
RegressionAbsoluteNs(u128),
}
impl Threshold {
pub fn regression_pct(pct: f64) -> Self {
Threshold::RegressionPct(pct)
}
pub fn regression_abs_ns(nanos: u128) -> Self {
Threshold::RegressionAbsoluteNs(nanos)
}
}
impl BenchmarkResult {
pub fn compare_against_baseline(
&self,
baseline_mean: Option<Duration>,
threshold: Threshold,
) -> CheckResult {
let Some(baseline) = baseline_mean else {
return CheckResult::skip(format!("bench::{}", self.name))
.with_detail("no baseline available");
};
let current_ns = self.mean.as_nanos();
let baseline_ns = baseline.as_nanos();
let regressed = match threshold {
Threshold::RegressionPct(pct) => {
let allowed = baseline_ns as f64 * (1.0 + pct / 100.0);
current_ns as f64 > allowed
}
Threshold::RegressionAbsoluteNs(abs) => {
current_ns.saturating_sub(baseline_ns) > abs
}
};
let detail = format!(
"current mean {} ns, baseline {} ns",
current_ns, baseline_ns
);
let name = format!("bench::{}", self.name);
if regressed {
CheckResult::fail(name, Severity::Warning).with_detail(detail)
} else {
CheckResult::pass(name).with_detail(detail)
}
}
}
pub trait Bench {
fn run(&mut self) -> BenchmarkResult;
}
#[cfg(test)]
mod tests {
use super::*;
use dev_report::Verdict;
#[test]
fn benchmark_runs_and_finishes() {
let mut b = Benchmark::new("noop");
for _ in 0..10 {
b.iter(|| std::hint::black_box(42));
}
let r = b.finish();
assert_eq!(r.samples.len(), 10);
assert!(r.mean > Duration::ZERO);
}
#[test]
fn comparison_without_baseline_is_skip() {
let mut b = Benchmark::new("x");
b.iter(|| ());
let r = b.finish();
let v = r.compare_against_baseline(None, Threshold::regression_pct(5.0));
assert_eq!(v.verdict, Verdict::Skip);
}
#[test]
fn small_regression_under_threshold_passes() {
let mut b = Benchmark::new("x");
for _ in 0..5 {
b.iter(|| std::thread::sleep(Duration::from_micros(1)));
}
let r = b.finish();
let baseline = r.mean;
let v = r.compare_against_baseline(Some(baseline), Threshold::regression_pct(50.0));
assert_eq!(v.verdict, Verdict::Pass);
}
}