use mdtable::{Builder, Table};
use std::{
fmt::Display,
time::{Duration, Instant},
};
mod histogram;
use histogram::Histogram;
#[macro_export]
macro_rules! agg {
($x:ident) => {
$x.aggregate::<Time>()
};
}
#[macro_export]
macro_rules! compare {
($($x:ident),* $(,)?) => {
Comparison::from([ $(( stringify!($x), $x )),* ])
};
}
#[macro_export]
macro_rules! agg_and_cmp {
($($x:ident),* $(,)?) => {
Comparison::from([ $(( stringify!($x), agg!($x) )),* ])
};
}
#[derive(Debug, Clone)]
struct Timer {
start: Instant,
}
impl Timer {
fn start() -> Self {
Self {
start: Instant::now(),
}
}
fn stop(&self) -> Duration {
Instant::now() - self.start
}
fn time<T>(f: impl FnOnce() -> T) -> Duration {
let timer = Self::start();
f();
timer.stop()
}
}
#[derive(Debug, Clone)]
pub struct Measurements {
durations: Vec<Duration>,
is_sorted: bool,
}
impl Measurements {
fn new() -> Self {
Self {
durations: Vec::new(),
is_sorted: true,
}
}
fn accept(&mut self, duration: Duration) {
self.durations.push(duration);
self.is_sorted = false;
}
pub fn len(&self) -> usize {
self.durations.len()
}
pub fn is_empty(&self) -> bool {
self.durations.is_empty()
}
pub fn min(&self) -> Duration {
assert!(self.is_sorted);
self.durations[0]
}
pub fn max(&self) -> Duration {
assert!(self.is_sorted);
self.durations[self.len() - 1]
}
pub fn arith_mean(&self) -> Duration {
let sum: Duration = self.durations.iter().sum();
sum / self.len() as u32
}
pub fn median(&self) -> Duration {
assert!(self.is_sorted);
*self.durations.iter().take(self.len() / 2).last().unwrap()
}
pub fn variance(&self) -> Duration {
let mean = self.arith_mean();
Duration::from_secs_f64(self.durations.iter().fold(0.0, |mut acc, duration| {
let diff = duration.as_secs_f64() - mean.as_secs_f64();
acc += diff * diff;
acc
}))
}
pub fn sort(&mut self) {
self.durations.sort_unstable();
self.is_sorted = true;
}
pub fn aggregate<T>(&self) -> Aggregate<T>
where
Duration: Into<T>,
{
Aggregate {
min: self.min().into(),
max: self.max().into(),
arith_mean: self.arith_mean().into(),
median: self.median().into(),
variance: self.variance().into(),
}
}
pub fn histogram<const N: usize>(&self) -> Histogram<N> {
Histogram::make_with(&self.durations[..])
}
}
#[derive(Debug, Clone)]
pub struct Aggregate<T> {
min: T,
max: T,
arith_mean: T,
median: T,
variance: T,
}
impl<T> Aggregate<T> {
const GET_DATA: [(&'static str, fn(&Self) -> &T); 5] = [
("min", Self::min),
("max", Self::max),
("arith_mean", Self::arith_mean),
("median", Self::median),
("variance", Self::variance),
];
pub fn min(&self) -> &T {
&self.min
}
pub fn max(&self) -> &T {
&self.max
}
pub fn arith_mean(&self) -> &T {
&self.arith_mean
}
pub fn median(&self) -> &T {
&self.median
}
pub fn variance(&self) -> &T {
&self.variance
}
pub fn all(&self) -> [(&'static str, &T); 5] {
Self::GET_DATA.map(|(name, getter)| (name, getter(self)))
}
}
impl From<Aggregate<Duration>> for Aggregate<Time> {
fn from(value: Aggregate<Duration>) -> Self {
Self {
min: value.min.into(),
max: value.max.into(),
arith_mean: value.arith_mean.into(),
median: value.median.into(),
variance: value.variance.into(),
}
}
}
impl<T: Display> Display for Aggregate<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{{")?;
for (name, value) in self.all() {
writeln!(f, " {}: {}", name, value)?;
}
write!(f, "}}")?;
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct Time {
hours: u64,
minutes: u8,
secs: u8,
millis: u16,
micros: u16,
nanos: u16,
}
impl Time {
const GET_UNITS: [(fn(&Self) -> u64, &'static str); 6] = [
(Self::hours::<u64>, "h"),
(Self::minutes::<u64>, "m"),
(Self::secs::<u64>, "s"),
(Self::millis::<u64>, "ms"),
(Self::micros::<u64>, "µs"),
(Self::nanos::<u64>, "ns"),
];
pub fn hours<T: From<u64>>(&self) -> T {
self.hours.into()
}
pub fn minutes<T: From<u8>>(&self) -> T {
self.minutes.into()
}
pub fn secs<T: From<u8>>(&self) -> T {
self.secs.into()
}
pub fn millis<T: From<u16>>(&self) -> T {
self.millis.into()
}
pub fn micros<T: From<u16>>(&self) -> T {
self.micros.into()
}
pub fn nanos<T: From<u16>>(&self) -> T {
self.nanos.into()
}
pub fn total_nanos_f64(&self) -> f64 {
let mut total = 0.0;
total += self.hours::<u128>() as f64;
total *= 60.0;
total += self.minutes::<f64>();
total *= 60.0;
total += self.secs::<f64>();
total *= 1000.0;
total += self.millis::<f64>();
total *= 1000.0;
total += self.micros::<f64>();
total *= 1000.0;
total += self.nanos::<f64>();
total
}
}
impl From<Duration> for Time {
fn from(value: Duration) -> Self {
Self::from(&value)
}
}
impl From<&Duration> for Time {
fn from(value: &Duration) -> Self {
let secs = value.as_secs();
let hours = secs / 3600;
let minutes = (secs % 3600) / 60;
let secs = secs % 60;
let nanos = value.subsec_nanos();
let millis = nanos / 1_000_000;
let micros = (nanos % 1_000_000) / 1_000;
let nanos = nanos % 1_000;
Self {
hours,
minutes: minutes as u8,
secs: secs as u8,
millis: millis as u16,
micros: micros as u16,
nanos: nanos as u16,
}
}
}
impl<'a> From<&'a Time> for Time {
fn from(value: &'a Time) -> Self {
value.clone()
}
}
impl Display for Time {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut anything = false;
for (getter, unit) in Self::GET_UNITS {
let value = getter(self);
if value > 0 {
anything = true;
write!(f, "{}{}", value, unit)?;
}
}
if !anything {
write!(f, "0s")?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Comparison<A, T, const N: usize>([(A, Aggregate<T>); N]);
impl<A, const N: usize> From<[(A, Measurements); N]> for Comparison<A, Duration, N>
where
A: AsRef<str>,
{
fn from(value: [(A, Measurements); N]) -> Self {
Self(value.map(|(name, measurements)| (name, measurements.aggregate())))
}
}
impl<A, T, X, const N: usize> From<[(A, Aggregate<T>); N]> for Comparison<A, X, N>
where
A: AsRef<str>,
Aggregate<T>: Into<Aggregate<X>>,
{
fn from(value: [(A, Aggregate<T>); N]) -> Self {
Self(value.map(|(name, agg)| (name, agg.into())))
}
}
impl<A, T, const N: usize> Display for Comparison<A, T, N>
where
A: AsRef<str>,
for<'a> &'a T: Into<Time>,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
static DECIMALS: i32 = 0;
write!(f, "metric")?;
for (name, _) in &self.0 {
write!(f, " | {}", name.as_ref())?;
}
writeln!(f)?;
write!(f, "---")?;
for _ in 0..N {
write!(f, " | ---:")?;
}
writeln!(f)?;
for (name, getter) in Aggregate::GET_DATA {
write!(f, "{}", name)?;
let mut baseline: Option<Time> = None;
for (_, agg) in &self.0 {
let time: Time = getter(agg).into();
write!(f, " | {}", time)?;
if let Some(baseline) = baseline {
let diff_percent = {
let mut diff = time.total_nanos_f64() / baseline.total_nanos_f64();
diff -= 1.0;
diff *= 100.0; diff *= f64::powi(10.0, DECIMALS);
diff = diff.floor();
diff *= f64::powi(10.0, -DECIMALS);
diff
};
write!(f, " & ({}%)", diff_percent)?;
} else {
baseline = Some(time);
}
}
writeln!(f)?;
}
Ok(())
}
}
impl<A, T, const N: usize> Comparison<A, T, N>
where
A: AsRef<str>,
for<'a> &'a T: Into<Time>,
{
pub fn table<'x>(&'x self) -> Table<&'static str, &'x str, String, N> {
let mut builder = Builder::new();
builder.header(("", {
let mut refs = [""; N];
for i in 0..N {
refs[i] = self.0[i].0.as_ref();
}
refs
}));
builder.default_alignments();
for (name, getter) in Aggregate::GET_DATA {
let mut content = Vec::with_capacity(N);
let mut baseline: Option<Time> = None;
for (_, agg) in &self.0 {
let duration = getter(agg);
let time: Time = duration.into();
if let Some(baseline) = baseline {
let diff_percent = {
let mut diff = time.total_nanos_f64() / baseline.total_nanos_f64();
diff -= 1.0;
diff *= 100.0; diff.trunc()
};
content.push(format!("{} ({:>3}%)", time, diff_percent));
} else {
content.push(time.to_string());
baseline = Some(time);
}
}
builder.row((name, content.try_into().ok().unwrap()));
}
builder.finish()
}
}
pub fn chair<Return>(runs: usize, f: impl Fn() -> Return) -> Measurements {
chair_prepare(runs, |_| (), |_| f())
}
pub fn chair_prepare<Data, Return>(
runs: usize,
prepare: impl Fn(usize) -> Data,
f: impl Fn(Data) -> Return,
) -> Measurements {
let mut measurements = Measurements::new();
for i in 0..runs {
let data = prepare(i);
let f = &f;
measurements.accept(Timer::time(move || f(data)));
}
measurements.sort();
measurements
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table() {
fn bubblesort(data: &mut Vec<u32>) {
let len = data.len();
let mut sorted = false;
while !sorted {
sorted = true;
for i in 1..len {
let j = i - 1;
if data[j] < data[i] {
data.swap(i, j);
sorted = false;
}
}
}
}
const RUNS: usize = 1_000;
let prepare = |_| (0u32..100).collect();
let bubblesort = chair_prepare(RUNS, prepare, |mut data| bubblesort(&mut data));
println!("bubblesort:\n{}", bubblesort.histogram::<50>());
let std = chair_prepare(RUNS, prepare, |mut data| data.sort());
println!("std:\n{}", std.histogram::<10>());
let compare = agg_and_cmp![std, bubblesort];
println!("{}", compare.table());
}
}