use std ::time :: { Duration, Instant };
use std ::fmt;
#[ derive(Debug, Clone) ]
pub struct BenchmarkResult
{
pub times: Vec< Duration >,
pub metrics: std ::collections ::HashMap< String, f64 >,
pub name: String,
}
impl BenchmarkResult
{
pub fn new( name: impl Into< String >, times: Vec< Duration > ) -> Self
{
Self
{
name: name.into(),
times,
metrics: std ::collections ::HashMap ::new(),
}
}
#[ must_use ]
pub fn with_metric( mut self, name: impl Into< String >, value: f64 ) -> Self
{
self.metrics.insert( name.into(), value );
self
}
#[ must_use ]
pub fn mean_time( &self ) -> Duration
{
if self.times.is_empty()
{
return Duration ::ZERO;
}
let total: Duration = self.times.iter().sum();
total / u32 ::try_from( self.times.len() ).unwrap_or( 1 )
}
#[ must_use ]
pub fn median_time( &self ) -> Duration
{
if self.times.is_empty()
{
return Duration ::ZERO;
}
let mut sorted = self.times.clone();
sorted.sort();
sorted[sorted.len() / 2]
}
#[ must_use ]
pub fn min_time( &self ) -> Duration
{
self.times.iter().min().copied().unwrap_or(Duration ::ZERO)
}
#[ must_use ]
pub fn max_time( &self ) -> Duration
{
self.times.iter().max().copied().unwrap_or(Duration ::ZERO)
}
#[ must_use ]
pub fn operations_per_second( &self ) -> f64
{
let mean_secs = self.mean_time().as_secs_f64();
if mean_secs > 0.0
{
1.0 / mean_secs
} else {
0.0
}
}
#[ must_use ]
pub fn std_deviation( &self ) -> Duration
{
if self.times.len() < 2
{
return Duration ::ZERO;
}
let mean = self.mean_time().as_secs_f64();
let variance: f64 = self.times
.iter()
.map(|&time| {
let diff = time.as_secs_f64() - mean;
diff * diff
})
.sum :: < f64 >() / (self.times.len() - 1) as f64;
Duration ::from_secs_f64(variance.sqrt())
}
#[ must_use ]
pub fn coefficient_of_variation( &self ) -> f64
{
let mean_val = self.mean_time().as_secs_f64();
if mean_val > 0.0
{
self.std_deviation().as_secs_f64() / mean_val
} else {
0.0
}
}
#[ must_use ]
pub fn standard_error( &self ) -> Duration
{
if self.times.is_empty()
{
return Duration ::ZERO;
}
let std_dev = self.std_deviation();
Duration ::from_secs_f64(std_dev.as_secs_f64() / (self.times.len() as f64).sqrt())
}
#[ must_use ]
pub fn confidence_interval_95( &self ) -> (Duration, Duration)
{
let mean = self.mean_time();
let margin = Duration ::from_secs_f64(1.96 * self.standard_error().as_secs_f64());
(mean.saturating_sub(margin), mean + margin)
}
#[ must_use ]
pub fn percentile(&self, p: f64) -> Duration
{
if self.times.is_empty()
{
return Duration ::ZERO;
}
let mut sorted = self.times.clone();
sorted.sort();
#[ allow( clippy ::cast_possible_truncation, clippy ::cast_sign_loss ) ]
let index = (p * (sorted.len() - 1) as f64).round() as usize;
sorted[index.min(sorted.len() - 1)]
}
#[ must_use ]
pub fn is_reliable( &self ) -> bool
{
let sufficient_samples = self.times.len() >= 10;
let low_variation = self.coefficient_of_variation() <= 0.1;
let reasonable_spread = if self.min_time().as_secs_f64() > 0.0
{
self.max_time().as_secs_f64() / self.min_time().as_secs_f64() < 3.0
} else {
false
};
sufficient_samples && low_variation && reasonable_spread
}
#[ must_use ]
pub fn compare(&self, other: &BenchmarkResult) -> Comparison
{
let my_time = self.mean_time().as_secs_f64();
let other_time = other.mean_time().as_secs_f64();
let improvement = if other_time > 0.0
{
((other_time - my_time) / other_time) * 100.0
} else {
0.0
};
Comparison {
baseline: other.clone(),
current: self.clone(),
improvement_percentage: improvement,
}
}
}
impl fmt ::Display for BenchmarkResult
{
fn fmt(&self, f: &mut fmt ::Formatter< '_ >) -> fmt ::Result
{
write!(f, "{} : {:.2?} (±{:.2?})",
self.name,
self.mean_time(),
self.std_deviation())
}
}
#[ derive(Debug, Clone) ]
pub struct Comparison
{
pub baseline: BenchmarkResult,
pub current: BenchmarkResult,
pub improvement_percentage: f64,
}
impl Comparison
{
#[ must_use ]
pub fn improvement( &self ) -> f64
{
self.improvement_percentage
}
#[ must_use ]
pub fn is_improvement( &self ) -> bool
{
self.improvement_percentage > 5.0
}
#[ must_use ]
pub fn is_regression( &self ) -> bool
{
self.improvement_percentage < -5.0
}
}
impl fmt ::Display for Comparison
{
fn fmt(&self, f: &mut fmt ::Formatter< '_ >) -> fmt ::Result
{
let status = if self.is_improvement()
{
"IMPROVEMENT"
} else if self.is_regression()
{
"REGRESSION"
} else {
"STABLE"
};
write!(f, "{} : {:.1}% {} ({:.2?} -> {:.2?})",
status,
self.improvement_percentage.abs(),
if self.improvement_percentage >= 0.0
{ "faster" } else { "slower" },
self.baseline.mean_time(),
self.current.mean_time())
}
}
#[ derive(Debug, Clone) ]
pub struct MeasurementConfig
{
pub iterations: usize,
pub warmup_iterations: usize,
pub max_time: Duration,
}
impl Default for MeasurementConfig
{
fn default() -> Self
{
Self {
iterations: 10,
warmup_iterations: 3,
max_time: Duration ::from_secs(10),
}
}
}
pub fn bench_function< F, R >(name: impl Into< String >, f: F) -> BenchmarkResult
where
F: FnMut() -> R,
{
bench_function_with_config(name, &MeasurementConfig ::default(), f)
}
pub fn bench_once< F, R >(mut f: F) -> BenchmarkResult
where
F: FnMut() -> R,
{
let start = Instant ::now();
let _ = f();
let elapsed = start.elapsed();
BenchmarkResult ::new("single_measurement", vec![elapsed])
}
pub fn bench_function_with_config< F, R >(
name: impl Into< String >,
config: &MeasurementConfig,
mut f: F
) -> BenchmarkResult
where
F: FnMut() -> R,
{
let name = name.into();
for _ in 0..config.warmup_iterations
{
let _ = f();
}
let mut times = Vec ::with_capacity(config.iterations);
let measurement_start = Instant ::now();
for _ in 0..config.iterations
{
if measurement_start.elapsed() > config.max_time
{
break;
}
let start = Instant ::now();
let _ = f();
times.push(start.elapsed());
}
BenchmarkResult ::new(name, times)
}
#[ macro_export ]
macro_rules! bench_block {
($block: expr) =>
{
bench_once(|| $block)
};
($name: expr, $block: expr) =>
{
bench_function($name, || $block)
};
}
pub fn time_block< F, R >(f: F) -> (R, Duration)
where
F: FnOnce() -> R,
{
let start = Instant ::now();
let result = f();
let elapsed = start.elapsed();
(result, elapsed)
}