use std::cmp::min;
use std::collections::HashMap;
use std::error::Error as StdError;
use std::ffi::OsStr;
use std::fmt::{self, Display, Formatter};
use std::io::{self, StdoutLock, Write};
use std::iter;
use std::path::PathBuf;
use std::process::Command;
use std::time::{Duration, Instant};
use walkdir::WalkDir;
#[derive(Debug)]
pub enum Error {
Empty,
}
impl StdError for Error {}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Error::Empty => write!(f, "empty benchmark"),
}
}
}
pub fn find_benchmarks(paths: &[PathBuf]) -> Vec<BenchmarkLoader> {
let mut benchmarks: HashMap<String, (Option<PathBuf>, Option<PathBuf>)> = HashMap::new();
for path in paths {
for entry in WalkDir::new(path)
.follow_links(true)
.into_iter()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_type().is_file())
.filter(|entry| entry.file_name() == "setup" || entry.file_name() == "benchmark")
{
let file_name = entry.file_name().to_string_lossy().into_owned();
let path = entry.into_path();
if let Some(name) = path.parent().and_then(|path| path.file_name()) {
let name = name.to_string_lossy().to_string();
let bench = benchmarks.entry(name).or_default();
match file_name.as_str() {
"setup" => bench.0 = Some(path),
"benchmark" => bench.1 = Some(path),
_ => unreachable!(),
}
}
}
}
let mut benchmarks: Vec<BenchmarkLoader> = benchmarks
.drain()
.filter_map(|(name, (setup_path, bench_path))| {
bench_path.map(|bench_path| BenchmarkLoader { name, setup_path, bench_path })
})
.collect();
benchmarks.sort_unstable_by(|a, b| a.name.cmp(&b.name));
benchmarks
}
pub struct BenchmarkLoader {
setup_path: Option<PathBuf>,
bench_path: PathBuf,
name: String,
}
impl BenchmarkLoader {
pub fn load(self, min_bytes: usize) -> Result<Benchmark, Error> {
println!("Loading {}...", self.name);
Benchmark::new(self.name, self.setup_path.as_ref(), self.bench_path, min_bytes)
}
}
pub struct Benchmark {
setup: Vec<u8>,
benchmark: Vec<u8>,
name: String,
}
impl Benchmark {
pub fn new<SP, BP>(
name: String,
setup_path: Option<SP>,
bench_path: BP,
min_bytes: usize,
) -> Result<Self, Error>
where
SP: AsRef<OsStr>,
BP: AsRef<OsStr>,
{
let setup = match setup_path {
Some(path) => Command::new(path).output().map(|out| out.stdout).unwrap_or_default(),
None => Vec::new(),
};
let bench = Command::new(bench_path).output().map(|out| out.stdout).unwrap_or_default();
if min_bytes == 0 || bench.is_empty() {
return Err(Error::Empty);
}
let count = (min_bytes - 1) / bench.len() + 1;
let bytes = iter::repeat(bench).take(count).flatten().collect();
Ok(Self { benchmark: bytes, setup, name })
}
pub fn run(&self, warmup_runs: usize, max_secs: u64, max_samples: Option<usize>) -> Results {
let stdout = io::stdout();
let mut stdout = stdout.lock();
for _ in 0..warmup_runs {
self.run_sample(&mut stdout);
}
let mut samples = Vec::new();
let max_samples = max_samples.unwrap_or(usize::max_value());
let end = Instant::now() + Duration::from_secs(max_secs);
for _ in (0..max_samples).take_while(|_| Instant::now() < end) {
let duration = self.run_sample(&mut stdout);
samples.push(duration.as_millis() as usize);
}
let _ = stdout.write_all(b"\x1bc");
let _ = stdout.flush();
Results::new(self.name.clone(), self.benchmark.len(), samples)
}
fn run_sample(&self, stdout: &mut StdoutLock) -> Duration {
let _ = stdout.write_all(b"\x1bc");
let _ = stdout.flush();
let _ = stdout.write_all(&self.setup);
let start = Instant::now();
let _ = stdout.write_all(&self.benchmark);
let _ = stdout.flush();
Instant::now() - start
}
}
pub struct Results {
sorted_samples: Vec<usize>,
samples: Vec<usize>,
bench_size: usize,
name: String,
}
#[allow(unused)]
impl Results {
pub fn new(name: String, bench_size: usize, mut samples: Vec<usize>) -> Self {
if samples.is_empty() {
samples.push(0);
}
let mut sorted_samples = samples.clone();
sorted_samples.sort_unstable();
Results { sorted_samples, samples, bench_size, name }
}
pub fn name(&self) -> &str {
&self.name
}
pub fn samples(&self) -> &[usize] {
self.samples.as_slice()
}
pub fn bench_size(&self) -> usize {
self.bench_size
}
pub fn sample_count(&self) -> usize {
self.samples.len()
}
pub fn min(&self) -> usize {
self.samples.iter().min().copied().unwrap_or_default()
}
pub fn max(&self) -> usize {
self.samples.iter().max().copied().unwrap_or_default()
}
pub fn mean(&self) -> f64 {
self.samples.iter().sum::<usize>() as f64 / self.samples.len() as f64
}
pub fn median(&self) -> f64 {
let len = self.samples.len();
(self.sorted_samples[(len - 1) / 2] as f64 + self.sorted_samples[len / 2] as f64) / 2.
}
pub fn variance(&self) -> f64 {
if self.samples.len() < 2 {
return 0.;
}
let mean = self.mean() as f64;
let len = self.samples.len();
self.samples.iter().map(|&s| f64::powi(s as f64 - mean, 2)).sum::<f64>() / (len - 1) as f64
}
pub fn stddev(&self) -> f64 {
self.variance().sqrt()
}
pub fn percentile(&self, mut percentile: usize) -> usize {
percentile = min(percentile, 100);
let index = ((self.samples.len() * percentile + 99) / 100).saturating_sub(1);
self.sorted_samples[index]
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64;
#[test]
fn max() {
let results = new_results(vec![6, 3, 1, 9, 8]);
assert_eq!(results.max(), 9);
}
#[test]
fn min() {
let results = new_results(vec![6, 3, 1, 9, 8]);
assert_eq!(results.min(), 1);
}
#[test]
fn mean() {
let results = new_results(vec![20, 30, 40, 80, 100, 100]);
float_eq(results.mean(), 61.666666666666664);
}
#[test]
fn median() {
let results = new_results(vec![20, 30, 55, 60, 100, 100]);
float_eq(results.median(), 57.5);
let results = new_results(vec![20, 30, 40, 60, 80, 100, 100]);
float_eq(results.median(), 60.);
}
#[test]
fn variance() {
let results = new_results(vec![4, 8, 8, 8, 10, 10]);
float_eq(results.variance(), 4.8);
let results = new_results(vec![3]);
float_eq(results.variance(), 0.);
}
#[test]
fn stddev() {
let results = new_results(vec![4, 8, 8, 8, 10, 10]);
float_eq(results.stddev(), 2.1908902300206643);
let results = new_results(vec![3]);
float_eq(results.stddev(), 0.);
}
#[test]
fn percentile() {
let results = new_results(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
assert_eq!(results.percentile(110), 10);
assert_eq!(results.percentile(100), 10);
assert_eq!(results.percentile(90), 9);
assert_eq!(results.percentile(80), 8);
assert_eq!(results.percentile(70), 7);
assert_eq!(results.percentile(60), 6);
assert_eq!(results.percentile(50), 5);
assert_eq!(results.percentile(40), 4);
assert_eq!(results.percentile(30), 3);
assert_eq!(results.percentile(20), 2);
assert_eq!(results.percentile(10), 1);
assert_eq!(results.percentile(0), 1);
}
fn new_results(samples: Vec<usize>) -> Results {
Results { sorted_samples: samples.clone(), samples, bench_size: 0, name: String::new() }
}
fn float_eq(f1: f64, f2: f64) {
if (f1 - f2).abs() >= f64::EPSILON {
panic!("float assertion failed: {} != {}", f1, f2);
}
}
}