use crate::error::Error;
use crate::{compress_with, decompress, decompress_into, CompressOptions};
use std::time::{Duration, Instant};
const MAX_SAMPLES: usize = 100_000;
#[derive(Debug, Clone)]
pub struct LoopTiming {
pub loops: u32,
pub wall_ms: f64,
}
#[derive(Debug, Clone)]
pub struct InProcessBench {
pub loops: u32,
pub compress_loops: u32,
pub decompress_loops: u32,
pub wall_ms: f64,
pub compress_ms: f64,
pub decompress_ms: f64,
pub compress_best_ms: f64,
pub decompress_best_ms: f64,
pub compress_p50_ms: f64,
pub decompress_p50_ms: f64,
pub compressed_bytes: usize,
pub compress_best_ticks: u64,
pub decompress_best_ticks: u64,
}
struct Samples {
best_ns: u128,
sum_ns: u128,
kept: Vec<u64>,
}
impl Samples {
fn new() -> Self {
Self {
best_ns: u128::MAX,
sum_ns: 0,
kept: Vec::new(),
}
}
fn push(&mut self, ns: u128) {
if ns < self.best_ns {
self.best_ns = ns;
}
self.sum_ns += ns;
if self.kept.len() < MAX_SAMPLES {
self.kept.push(ns.min(u128::from(u64::MAX)) as u64);
}
}
fn best_ms(&self) -> f64 {
if self.best_ns == u128::MAX {
return 0.0;
}
self.best_ns as f64 / 1_000_000.0
}
fn sum_ms(&self) -> f64 {
self.sum_ns as f64 / 1_000_000.0
}
fn p50_ms(&mut self) -> f64 {
if self.kept.is_empty() {
return 0.0;
}
self.kept.sort_unstable();
self.kept[self.kept.len() / 2] as f64 / 1_000_000.0
}
}
pub fn time_loops<E, F>(min: Duration, mut one: F) -> Result<LoopTiming, E>
where
F: FnMut() -> Result<(), E>,
{
let start = Instant::now();
one()?;
let mut loops = 1u32;
while !min.is_zero() && start.elapsed() < min {
one()?;
loops = loops.saturating_add(1);
}
Ok(LoopTiming {
loops,
wall_ms: start.elapsed().as_secs_f64() * 1000.0,
})
}
pub const MIN_LOOPS: u32 = 25;
pub const MAX_PHASE: Duration = Duration::from_secs(20);
pub fn bench_roundtrip(src: &[u8], level: i32, min: Duration) -> Result<InProcessBench, Error> {
bench_roundtrip_clocked(src, level, min, || 0)
}
fn time_phase<F>(min: Duration, mut tick: impl FnMut() -> u64, mut one: F) -> Result<Phase, Error>
where
F: FnMut() -> Result<(), Error>,
{
let mut s = Samples::new();
let mut best_ticks = u64::MAX;
let start = Instant::now();
let mut loops = 0u32;
loop {
let k = tick();
let t = Instant::now();
one()?;
let ns = t.elapsed().as_nanos();
best_ticks = best_ticks.min(tick().saturating_sub(k));
s.push(ns);
loops += 1;
let elapsed = start.elapsed();
if elapsed >= MAX_PHASE {
break;
}
if elapsed >= min && loops >= MIN_LOOPS {
break;
}
if min.is_zero() {
break;
}
}
Ok(Phase {
loops,
best_ms: s.best_ms(),
sum_ms: s.sum_ms(),
p50_ms: s.p50_ms(),
best_ticks: if best_ticks == u64::MAX {
0
} else {
best_ticks
},
})
}
struct Phase {
loops: u32,
best_ms: f64,
sum_ms: f64,
p50_ms: f64,
best_ticks: u64,
}
pub fn bench_roundtrip_clocked<F>(
src: &[u8],
level: i32,
min: Duration,
mut tick: F,
) -> Result<InProcessBench, Error>
where
F: FnMut() -> u64,
{
let opts = CompressOptions {
level,
checksum: false,
};
let zst = compress_with(src, opts)?;
let raw = decompress(&zst)?;
if raw.as_slice() != src {
return Err(Error::Corruption);
}
let compressed_bytes = zst.len();
drop(raw);
let wall = Instant::now();
let c = time_phase(min, &mut tick, || {
compress_with(src, opts)?;
Ok(())
})?;
let mut dst = Vec::with_capacity(src.len());
let d = time_phase(min, &mut tick, || {
dst.clear();
decompress_into(&mut dst, &zst)?;
Ok(())
})?;
debug_assert_eq!(dst.len(), src.len());
Ok(InProcessBench {
loops: c.loops.min(d.loops),
compress_loops: c.loops,
decompress_loops: d.loops,
wall_ms: wall.elapsed().as_secs_f64() * 1000.0,
compress_ms: c.sum_ms,
decompress_ms: d.sum_ms,
compress_best_ms: c.best_ms,
decompress_best_ms: d.best_ms,
compress_p50_ms: c.p50_ms,
decompress_p50_ms: d.p50_ms,
compressed_bytes,
compress_best_ticks: c.best_ticks,
decompress_best_ticks: d.best_ticks,
})
}
pub fn mbps(src_len: usize, loops: u32, ms: f64) -> f64 {
if ms <= 0.0 {
return 0.0;
}
(src_len as f64 * f64::from(loops)) / (ms / 1000.0) / 1_000_000.0
}
pub fn mbps_best(src_len: usize, best_ms: f64) -> f64 {
if best_ms <= 0.0 {
return 0.0;
}
(src_len as f64) / (best_ms / 1000.0) / 1_000_000.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_loop_roundtrip() {
let src = b"m7 in-process bench rusty_zstd. ".repeat(40);
let b = bench_roundtrip(&src, 1, Duration::ZERO).expect("bench");
assert_eq!(b.loops, 1);
assert!(b.compressed_bytes > 0);
assert!(b.compress_ms >= 0.0);
assert!(b.decompress_ms >= 0.0);
}
#[test]
fn best_is_never_slower_than_the_mean_loop() {
let src = b"best-of-N vs mean-of-N parity check. ".repeat(4000);
let b = bench_roundtrip(&src, 1, Duration::from_millis(120)).expect("bench");
assert!(b.loops >= 1);
let mean_c = b.compress_ms / f64::from(b.loops);
let mean_d = b.decompress_ms / f64::from(b.loops);
assert!(b.compress_best_ms <= mean_c + 1e-9, "compress best > mean");
assert!(
b.decompress_best_ms <= mean_d + 1e-9,
"decompress best > mean"
);
assert!(b.compress_p50_ms >= b.compress_best_ms - 1e-9);
}
#[test]
fn mbps_best_uses_one_loop_not_the_sum() {
assert!((mbps_best(1_000_000, 1.0) - 1000.0).abs() < 1e-6);
assert!((mbps(1_000_000, 10, 10.0) - 1000.0).abs() < 1e-6);
}
}