use crate::{
BrunchError,
History,
MIN_SAMPLES,
Stats,
util,
};
use dactyl::{
NiceU32,
traits::SaturatingFrom,
};
use std::{
fmt,
hint::black_box,
num::NonZeroU32,
time::{
Duration,
Instant,
},
};
const DEFAULT_SAMPLES: NonZeroU32 = NonZeroU32::new(2500).unwrap();
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
const NO_CHANGE: &str = "\x1b[2m---\x1b[0m";
#[derive(Debug, Default)]
pub struct Benches(Vec<Bench>);
impl Extend<Bench> for Benches {
fn extend<T: IntoIterator<Item=Bench>>(&mut self, iter: T) {
for b in iter { self.push(b); }
}
}
impl Benches {
pub fn push(&mut self, mut b: Bench) {
if ! b.is_spacer() && self.has_name(&b.name) {
b.stats.replace(Err(BrunchError::DupeName));
}
self.0.push(b);
}
pub fn finish(&self) {
if self.0.is_empty() {
eprintln!(
"\x1b[1;91mError:\x1b[0m {}",
BrunchError::NoBench
);
return;
}
let mut history = History::default();
let mut summary = Table::default();
let names: Vec<Vec<char>> = self.0.iter()
.filter_map(|b|
if b.is_spacer() { None }
else { Some(b.name.chars().collect()) }
)
.collect();
for b in &self.0 {
summary.push(b, &names, &history);
}
self.finish_history(&mut history);
eprintln!("{summary}");
}
fn finish_history(&self, history: &mut History) {
for b in &self.0 {
if let Some(Ok(s)) = b.stats {
history.insert(&b.name, s);
}
}
history.save();
}
}
impl Benches {
fn has_name(&self, name: &str) -> bool {
self.0.iter().any(|b| b.name == name)
}
}
#[derive(Debug)]
pub struct Bench {
name: String,
samples: NonZeroU32,
timeout: Duration,
stats: Option<Result<Stats, BrunchError>>,
}
impl Bench {
#[must_use]
pub fn new<S>(name: S) -> Self
where S: AsRef<str> {
let name = name.as_ref().trim();
assert!(! name.is_empty(), "Name is required.");
let mut ws = false;
let name: String = name.chars()
.filter_map(|c|
if c.is_whitespace() {
if ws { None }
else {
ws = true;
Some(' ')
}
}
else {
ws = false;
Some(c)
}
)
.collect();
assert!(name.len() <= 65535, "Names cannot be longer than 65,535.");
Self {
name,
samples: DEFAULT_SAMPLES,
timeout: DEFAULT_TIMEOUT,
stats: None,
}
}
#[must_use]
pub const fn spacer() -> Self {
Self {
name: String::new(),
samples: DEFAULT_SAMPLES,
timeout: DEFAULT_TIMEOUT,
stats: None,
}
}
const fn is_spacer(&self) -> bool { self.name.is_empty() }
#[must_use]
pub const fn with_timeout(mut self, timeout: Duration) -> Self {
if timeout.as_millis() < 500 {
self.timeout = Duration::from_millis(500);
}
else { self.timeout = timeout; }
self
}
#[expect(clippy::missing_panics_doc, reason = "Value is checked.")]
#[must_use]
pub const fn with_samples(mut self, samples: u32) -> Self {
if samples < MIN_SAMPLES.get() { self.samples = MIN_SAMPLES; }
else {
self.samples = NonZeroU32::new(samples).unwrap();
}
self
}
}
impl Bench {
#[must_use]
pub fn run<F, O>(mut self, mut cb: F) -> Self
where F: FnMut() -> O {
if self.is_spacer() { return self; }
let mut times: Vec<Duration> = Vec::with_capacity(usize::saturating_from(self.samples.get()));
let now = Instant::now();
for _ in 0..self.samples.get() {
let now2 = Instant::now();
let _res = black_box(cb());
times.push(now2.elapsed());
if self.timeout <= now.elapsed() { break; }
}
self.stats.replace(Stats::try_from(times));
self
}
#[must_use]
pub fn run_seeded<F, I, O>(mut self, seed: I, mut cb: F) -> Self
where F: FnMut(I) -> O, I: Clone {
if self.is_spacer() { return self; }
let mut times: Vec<Duration> = Vec::with_capacity(usize::saturating_from(self.samples.get()));
let now = Instant::now();
for _ in 0..self.samples.get() {
let seed2 = seed.clone();
let now2 = Instant::now();
let _res = black_box(cb(seed2));
times.push(now2.elapsed());
if self.timeout <= now.elapsed() { break; }
}
self.stats.replace(Stats::try_from(times));
self
}
#[must_use]
pub fn run_seeded_with<F1, F2, I, O>(mut self, mut seed: F1, mut cb: F2) -> Self
where F1: FnMut() -> I, F2: FnMut(I) -> O {
if self.is_spacer() { return self; }
let mut times: Vec<Duration> = Vec::with_capacity(usize::saturating_from(self.samples.get()));
let now = Instant::now();
for _ in 0..self.samples.get() {
let seed2 = seed();
let now2 = Instant::now();
let _res = black_box(cb(seed2));
times.push(now2.elapsed());
if self.timeout <= now.elapsed() { break; }
}
self.stats.replace(Stats::try_from(times));
self
}
}
#[derive(Debug, Clone)]
struct Table(Vec<TableRow>);
impl Default for Table {
fn default() -> Self {
Self(vec![
TableRow::Normal(
"\x1b[1;95mMethod".to_owned(),
"Mean".to_owned(),
"Samples\x1b[0m".to_owned(),
"\x1b[1;95mChange\x1b[0m".to_owned(),
),
TableRow::Spacer,
])
}
}
impl fmt::Display for Table {
#[expect(clippy::many_single_char_names, reason = "Consistency is preferred.")]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (w1, w2, w3, mut w4) = self.lens();
let changes = self.show_changes();
let width =
if changes { w1 + w2 + w3 + w4 + 12 }
else {
w4 = 0;
w1 + w2 + w3 + 8
};
let pad_len = w1.max(w2).max(w3).max(w4);
let mut pad = String::with_capacity(pad_len);
for _ in 0..pad_len { pad.push(' '); }
let mut spacer = String::with_capacity(10 + width);
spacer.push_str("\x1b[35m");
for _ in 0..width { spacer.push('-'); }
spacer.push_str("\x1b[0m\n");
for v in &self.0 {
let (c1, c2, c3, c4) = v.lens();
match v {
TableRow::Normal(a, b, c, d) if changes => writeln!(
f, "{}{} {}{} {}{} {}{}",
a, &pad[..w1 - c1],
&pad[..w2 - c2], b,
&pad[..w3 - c3], c,
&pad[..w4 - c4], d,
)?,
TableRow::Normal(a, b, c, _) => writeln!(
f, "{}{} {}{} {}{}",
a, &pad[..w1 - c1],
&pad[..w2 - c2], b,
&pad[..w3 - c3], c,
)?,
TableRow::Error(a, b) => writeln!(
f,
"{}{} \x1b[38;5;208m{}\x1b[0m",
a,
&pad[..w1 - c1],
b,
)?,
TableRow::Spacer => f.write_str(&spacer)?,
}
}
Ok(())
}
}
impl Table {
fn push(&mut self, src: &Bench, names: &[Vec<char>], history: &History) {
if src.is_spacer() { self.0.push(TableRow::Spacer); }
else {
let name = format_name(src.name.chars().collect(), names);
match src.stats.unwrap_or(Err(BrunchError::NoRun)) {
Ok(s) => {
let time = s.nice_mean();
let diff = history.get(&src.name)
.and_then(|h| s.is_deviant(h))
.unwrap_or_else(|| NO_CHANGE.to_owned());
let (valid, total) = s.samples();
let samples = format!(
"\x1b[2m{}\x1b[0;35m/\x1b[0;2m{}\x1b[0m",
NiceU32::from(valid),
NiceU32::from(total),
);
self.0.push(TableRow::Normal(name, time, samples, diff));
},
Err(e) => {
self.0.push(TableRow::Error(name, e));
}
}
}
}
fn show_changes(&self) -> bool {
self.0.iter().skip(2).any(|v|
if let TableRow::Normal(_, _, _, c) = v { c != NO_CHANGE }
else { false }
)
}
fn lens(&self) -> (usize, usize, usize, usize) {
self.0.iter()
.fold((0, 0, 0, 0), |acc, v| {
let v = v.lens();
(
acc.0.max(v.0),
acc.1.max(v.1),
acc.2.max(v.2),
acc.3.max(v.3),
)
})
}
}
#[derive(Debug, Clone)]
enum TableRow {
Normal(String, String, String, String),
Error(String, BrunchError),
Spacer,
}
impl TableRow {
fn lens(&self) -> (usize, usize, usize, usize) {
match self {
Self::Normal(a, b, c, d) => (
util::width(a),
util::width(b),
util::width(c),
util::width(d),
),
Self::Error(a, _) => (util::width(a), 0, 0, 0),
Self::Spacer => (0, 0, 0, 0),
}
}
}
fn format_name(mut name: Vec<char>, names: &[Vec<char>]) -> String {
let len = name.len();
let mut pos: usize = names.iter()
.filter_map(|other|
if name.eq(other) { None }
else {
name.iter()
.zip(other.iter())
.position(|(l, r)| l != r)
.or_else(|| Some(len.min(other.len())))
}
)
.max()
.unwrap_or_default();
if 0 < pos && pos < len && ! matches!(name[pos], ':' | '(') {
if let Some(pos2) = name[..pos].iter().rposition(|c| matches!(c, ':' | '(')) {
pos = name[..pos2].iter()
.rposition(|c| ! matches!(c, ':' | '('))
.map_or(0, |p| p + 1);
}
else if let Some(pos2) = name[..pos].iter().rposition(|c| matches!(c, '_' | ' ')) {
pos = name[..pos2].iter()
.rposition(|c| ! matches!(c, '_' | ' '))
.map_or(0, |p| p + 1);
}
else { pos = 0; }
}
if pos == 0 {
"\x1b[94m".chars()
.chain(name)
.chain("\x1b[0m".chars())
.collect()
}
else if pos == len {
"\x1b[34m".chars()
.chain(name)
.chain("\x1b[0m".chars())
.collect()
}
else {
let b = name.split_off(pos);
"\x1b[34m".chars()
.chain(name)
.chain("\x1b[94m".chars())
.chain(b)
.chain("\x1b[0m".chars())
.collect()
}
}