use alux_bench::{
BenchAlg, BenchCase, BenchCases, BenchGroup, BenchRounds, BenchRoutine, BenchSampling, BenchSentRoutine,
BenchStated, MeasureBenchAlg,
};
use core::time::Duration;
use derive_new::new as New;
use std::io::{IsTerminal, Write as _, stderr};
use std::thread;
use std::time::Instant;
const LEAST: BenchRounds = BenchRounds::new(1);
const COLUMN: usize = 32;
const SAID: usize = 96;
const SAYING: &str = "\x1b[36m";
const PLAIN: &str = "\x1b[0m";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BenchMeasured {
case: BenchCase,
rounds: BenchRounds,
samples: Vec<Duration>,
}
impl BenchMeasured {
pub const fn case(&self) -> BenchCase {
self.case
}
pub fn said(&self) {
say_no_more();
if let Some(line) = line(self, COLUMN) {
println!("{line}");
}
}
pub const fn rounds(&self) -> BenchRounds {
self.rounds
}
pub fn samples(&self) -> &[Duration] {
&self.samples
}
pub fn spread(&self) -> Option<(Duration, Duration, Duration)> {
let mut sorted = self.samples.iter().map(|sample| *sample / self.per_round()).collect::<Vec<_>>();
sorted.sort_unstable();
let shortest = *sorted.first()?;
let longest = *sorted.last()?;
Some((shortest, sorted[sorted.len() / 2], longest))
}
fn per_round(&self) -> u32 {
u32::try_from(self.rounds.count()).unwrap_or(u32::MAX).max(1)
}
}
#[derive(Clone, Debug, New)]
pub struct DirectBench {
sampling: BenchSampling,
#[new(default)]
only: Option<String>,
}
impl DirectBench {
pub fn from_args(sampling: BenchSampling) -> Self {
let only = std::env::args().skip(1).find(|argument| !argument.starts_with('-'));
only.map_or_else(|| Self::new(sampling), |only| Self::new(sampling).only(only))
}
#[must_use]
pub fn only(mut self, only: impl Into<String>) -> Self {
self.only = Some(only.into());
self
}
fn measuring(&self, case: BenchCase, routine: &mut dyn FnMut(BenchRounds) -> Duration) -> Option<BenchMeasured> {
if !self.stating(case) {
return None;
}
let each = self.rounds_of_a_sample(routine);
let samples = (0..self.sampling.samples()).map(|_| routine(each)).collect();
Some(BenchMeasured { case, rounds: each, samples })
}
fn stating(&self, case: BenchCase) -> bool {
self.only.as_ref().is_none_or(|only| name(case).contains(only.as_str()))
}
fn rounds_of_a_sample(&self, routine: &mut dyn FnMut(BenchRounds) -> Duration) -> BenchRounds {
let Some(spend) = self.sampling.spend() else {
return LEAST;
};
let warming = Instant::now();
let _ = routine(LEAST);
let cost = warming.elapsed();
let Ok(fits) = u64::try_from(spend.as_nanos() / cost.as_nanos().max(1)) else {
return LEAST;
};
BenchRounds::new(fits.max(1))
}
}
impl BenchAlg for DirectBench {
type Bench = BenchStated<'static>;
fn nothing(&self) -> Self::Bench {
BenchStated::new()
}
fn group(&self, group: &'static str, cases: BenchCases<'static>) -> Self::Bench {
BenchStated::new().group(BenchGroup::new(group, cases))
}
fn then(&self, first: Self::Bench, next: Self::Bench) -> Self::Bench {
first.then(next)
}
}
impl MeasureBenchAlg for DirectBench {
type Bench = BenchStated<'static>;
fn measure(&mut self, bench: Self::Bench) {
for group in bench.into_groups() {
let named = group.group();
match group.cases() {
BenchCases::OneAtATime(cases) => self.one_at_a_time(named, cases),
BenchCases::Together(cases) => self.together(named, cases),
}
}
say_no_more();
}
}
impl DirectBench {
fn one_at_a_time(&self, group: &'static str, cases: Vec<(&'static str, BenchRoutine<'static>)>) {
for (subject, mut routine) in cases {
let case = BenchCase::new(group, subject);
if !self.stating(case) {
continue;
}
say(&format!("measuring {}", name(case)));
if let Some(measured) = self.measuring(case, &mut routine) {
measured.said();
}
}
}
fn together(&self, group: &'static str, cases: Vec<(&'static str, BenchSentRoutine)>) {
let cases = cases
.into_iter()
.map(|(subject, routine)| (BenchCase::new(group, subject), routine))
.filter(|(case, _)| self.stating(*case))
.collect::<Vec<_>>();
if cases.is_empty() {
return;
}
let stated = cases.len();
say(&measuring_at_once(stated, group));
thread::scope(|running| {
let running = cases
.into_iter()
.map(|(case, mut routine)| running.spawn(move || self.measuring(case, &mut routine)))
.collect::<Vec<_>>();
for (done, case) in running.into_iter().enumerate() {
if let Some(measured) = case.join().expect("the case measured") {
measured.said();
}
let waiting = stated - done - 1;
if waiting > 0 {
say(&measuring_at_once(waiting, group));
}
}
});
}
}
fn measuring_at_once(cases: usize, group: &str) -> String {
let many = if cases == 1 { "case" } else { "cases" };
format!("measuring {cases} {many} of {group} at the same time")
}
fn say(saying: &str) {
if stderr().is_terminal() {
eprint!("\r{SAYING}{saying:SAID$}{PLAIN}\r");
let _ = stderr().flush();
} else {
eprintln!("{saying}");
}
}
fn say_no_more() {
if stderr().is_terminal() {
eprint!("\r{:SAID$}\r", "");
let _ = stderr().flush();
}
}
fn line(measured: &BenchMeasured, width: usize) -> Option<String> {
let (shortest, median, longest) = measured.spread()?;
let samples = measured.samples().len();
let each = measured.rounds().count();
let case = name(measured.case());
Some(format!("{case:width$} {samples:>3} x {each:<6} rounds {median:>12.4?} [{shortest:.4?} {longest:.4?}]"))
}
fn name(case: BenchCase) -> String {
format!("{}/{}", case.group(), case.subject())
}