use std::{
fmt::Display,
iter::Sum,
ops::{Add, AddAssign},
time::Duration,
};
use tdigest::TDigest;
#[derive(Debug, Clone)]
pub struct BenchmarkResult {
pub(crate) success: usize,
pub(crate) http_error: usize,
pub(crate) tcp_error: usize,
pub(crate) elapsed: Duration,
pub(crate) min_time: Duration,
pub(crate) max_time: Duration,
pub(crate) timings: Vec<Duration>,
}
impl BenchmarkResult {
pub fn total_request_count(&self) -> usize {
self.success + self.http_error
}
pub fn requests_per_second(&self) -> f64 {
(self.total_request_count() as f64 / self.elapsed.as_millis() as f64) * 1000.0
}
pub fn success_count(&self) -> usize {
self.success
}
pub fn http_error_count(&self) -> usize {
self.http_error
}
pub fn tcp_error_count(&self) -> usize {
self.tcp_error
}
pub fn total_time(&self) -> Duration {
self.elapsed
}
pub fn average_time(&self) -> Option<Duration> {
match self.total_request_count() {
0 => None,
n => Some(self.timings().sum::<Duration>() / n as u32),
}
}
pub fn standard_deviation(&self) -> Option<Duration> {
let avg = self.average_time()?.as_secs_f64();
let var = self
.timings()
.map(|time| time.as_secs_f64())
.map(|time| avg - time)
.map(|val| val * val)
.sum::<f64>()
/ self.total_request_count() as f64;
Some(Duration::from_secs_f64(var.sqrt()))
}
pub fn timings(&self) -> impl Iterator<Item = Duration> + '_ {
self.timings.iter().copied()
}
pub fn percentiles(&self) -> Percentiles {
let tdigest = TDigest::new_with_size(100);
Percentiles(
tdigest.merge_unsorted(self.timings.iter().map(|dur| dur.as_secs_f64()).collect()),
)
}
}
pub struct Percentiles(TDigest);
impl Percentiles {
pub fn percentile(&self, q: f64) -> Duration {
Duration::from_secs_f64(self.0.estimate_quantile(q))
}
}
impl Default for BenchmarkResult {
fn default() -> Self {
Self {
success: Default::default(),
http_error: Default::default(),
tcp_error: Default::default(),
elapsed: Duration::ZERO,
min_time: Duration::MAX,
max_time: Duration::ZERO,
timings: Vec::with_capacity(100000),
}
}
}
impl Display for BenchmarkResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let seconds = self.elapsed.as_millis() as f64 / 1000.0;
let total_requests = self.total_request_count();
writeln!(f, "Requests: {} in {:.2}s", total_requests, seconds)?;
writeln!(f, "Reqs/sec: {:.2}", self.requests_per_second())?;
if self.success > 0 && self.http_error > 0 {
writeln!(f, "Success: {}", self.success)?;
}
if self.http_error > 0 {
writeln!(f, "Errors: {}", self.http_error)?;
}
if self.tcp_error > 0 {
writeln!(f, "TCP error: {}", self.tcp_error)?;
}
let percentiles = self.percentiles();
let p99 = percentiles.percentile(0.99).as_millis() as f64;
let p90 = percentiles.percentile(0.90).as_millis() as f64;
let p75 = percentiles.percentile(0.75).as_millis() as f64;
let p50 = percentiles.percentile(0.50).as_millis() as f64;
writeln!(f, "P99: {p99:.2}ms")?;
writeln!(f, "P90: {p90:.2}ms")?;
writeln!(f, "P75: {p75:.2}ms")?;
writeln!(f, "P50: {p50:.2}ms")?;
writeln!(f, "Min: {:.2}ms", self.min_time.as_millis() as f64)?;
writeln!(f, "Max: {:.2}ms", self.max_time.as_millis() as f64)?;
Ok(())
}
}
impl Sum<BenchmarkResult> for BenchmarkResult {
fn sum<I: Iterator<Item = BenchmarkResult>>(iter: I) -> Self {
iter.fold(BenchmarkResult::default(), |total, result| {
BenchmarkResult {
success: total.success + result.success,
http_error: total.http_error + result.http_error,
tcp_error: total.tcp_error + result.tcp_error,
elapsed: total.elapsed + result.elapsed,
min_time: total.min_time.min(result.min_time),
max_time: total.max_time.max(result.max_time),
timings: [total.timings, result.timings].concat(),
}
})
}
}
impl AddAssign<BenchmarkResult> for BenchmarkResult {
fn add_assign(&mut self, mut rhs: BenchmarkResult) {
self.success += rhs.success;
self.http_error += rhs.http_error;
self.tcp_error += rhs.tcp_error;
self.elapsed += rhs.elapsed;
if self.min_time > rhs.min_time {
self.min_time = rhs.min_time;
}
if self.max_time < rhs.max_time {
self.max_time = rhs.max_time;
}
self.timings.append(&mut rhs.timings);
}
}
impl Add<BenchmarkResult> for BenchmarkResult {
type Output = BenchmarkResult;
fn add(mut self, rhs: BenchmarkResult) -> Self::Output {
self += rhs;
self
}
}