use std::path::PathBuf;
use fastrace::{Span, collector::SpanContext};
use crate::report::print_group;
use crate::{collector, report::BenchReport};
pub(crate) const BENCH_ROOT: &str = "__bencher_root__";
pub struct Bench {
name: Option<String>,
group: Option<String>,
pub(crate) iterations: usize,
pub(crate) min_run_seconds: usize,
pub(crate) warmup_seconds: usize,
pub(crate) auto_save: bool,
}
impl Bench {
pub fn new(name: impl Into<String>) -> Self {
Bench {
name: Some(name.into()),
group: None,
iterations: 1000,
min_run_seconds: 3,
warmup_seconds: 2,
auto_save: true,
}
}
pub fn group(&mut self, group: impl Into<String>) -> BenchGroup<'_> {
BenchGroup {
bench: self,
group_name: group.into(),
entries: Vec::new(),
}
}
pub fn iterations(&mut self, n: usize) -> &mut Self {
self.iterations = n;
self
}
pub fn run_seconds(&mut self, n: usize) -> &mut Self {
self.min_run_seconds = n;
self
}
pub fn warmup(&mut self, n: usize) -> &mut Self {
self.warmup_seconds = n;
self
}
pub fn no_auto_save(&mut self) -> &mut Self {
self.auto_save = false;
self
}
pub fn name(&mut self, name: impl Into<String>) -> &mut Self {
self.name = Some(name.into());
self
}
pub fn run<F, R>(&mut self, f: F) -> BenchReport
where
F: FnMut() -> R,
{
let name = self.name.take().expect(
"bencher: called run() twice on the same Bench, \
call `name()` to create a new benchmark with the same params.",
);
let report = self.run_inner(&name, f);
if self.auto_save {
let path = self.baseline_path(&name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok();
}
if let Ok(previous) = BenchReport::load(&path) {
report.compare(&previous).print();
} else {
report.print();
}
report.save(&path).ok();
}
report
}
pub fn baseline_path(&self, name: &str) -> PathBuf {
let base = cargo_target_directory().unwrap();
let mut path = base.join("bencher");
if let Some(ref group) = self.group {
path = path.join(group);
}
path.join(name).join("baseline.json")
}
pub(crate) fn run_inner<F, R>(&self, name: &str, mut f: F) -> BenchReport
where
F: FnMut() -> R,
{
assert!(self.iterations > 0, "bencher: iterations must be > 0");
collector::init();
let warmup_end =
std::time::Instant::now() + std::time::Duration::from_secs(self.warmup_seconds as u64);
while std::time::Instant::now() < warmup_end {
run_iter(&mut f);
collector::drain();
}
let end_run =
std::time::Instant::now() + std::time::Duration::from_secs(self.min_run_seconds as u64);
let mut results = Vec::new();
for iter in 0.. {
run_iter(&mut f);
results.push(collector::drain());
if iter > self.iterations && std::time::Instant::now() >= end_run {
break;
}
}
BenchReport::from_iters(name.to_owned(), results)
}
}
pub struct BenchGroup<'a> {
bench: &'a mut Bench,
group_name: String,
entries: Vec<GroupEntry>,
}
struct GroupEntry {
report: BenchReport,
baseline: Option<BenchReport>,
path: PathBuf,
}
impl BenchGroup<'_> {
pub fn name(&mut self, name: impl Into<String>) -> &mut Self {
self.bench.name = Some(name.into());
self
}
pub fn iterations(&mut self, n: usize) -> &mut Self {
self.bench.iterations = n;
self
}
pub fn run<F, R>(&mut self, f: F) -> BenchReport
where
F: FnMut() -> R,
{
let name = self
.bench
.name
.take()
.expect("bencher: call name() before each run() on a BenchGroup");
let report = self.bench.run_inner(&name, f);
let path = self.baseline_path(&name);
let baseline = BenchReport::load(&path).ok();
self.entries.push(GroupEntry {
report: report.clone(),
baseline,
path,
});
report
}
fn baseline_path(&self, name: &str) -> PathBuf {
let base = std::env::current_dir().unwrap_or_default();
base.join("target")
.join("bencher")
.join(&self.group_name)
.join(name)
.join("baseline.json")
}
}
impl Drop for BenchGroup<'_> {
fn drop(&mut self) {
if self.bench.auto_save {
for entry in &self.entries {
if let Some(parent) = entry.path.parent() {
std::fs::create_dir_all(parent).ok();
}
entry.report.save(&entry.path).ok();
}
let pairs: Vec<(&BenchReport, Option<&BenchReport>)> = self
.entries
.iter()
.map(|e| (&e.report, e.baseline.as_ref()))
.collect();
print_group(&pairs, &self.group_name);
}
}
}
fn run_iter<F, R>(f: &mut F)
where
F: FnMut() -> R,
{
let root = Span::root(BENCH_ROOT, SpanContext::random());
let _guard = root.set_local_parent();
f();
drop(_guard);
drop(root);
}
fn cargo_target_directory() -> Option<PathBuf> {
#[derive(serde::Deserialize)]
struct Metadata {
target_directory: PathBuf,
}
std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.or_else(|| {
let output =
std::process::Command::new(std::env::var_os("CARGO").unwrap_or("cargo".into()))
.args(["metadata", "--format-version", "1"])
.output()
.ok()?;
let metadata: Metadata = serde_json::from_slice(&output.stdout).ok()?;
Some(metadata.target_directory)
})
}