use crate::error::Result;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::time::{Duration, Instant, SystemTime};
#[derive(Debug)]
pub struct AdvancedBenchmarkRunner {
pub config: BenchmarkConfig,
pub history: BenchmarkHistory,
pub analyzer: RegressionAnalyzer,
pub baselines: HashMap<String, PerformanceBaseline>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkConfig {
pub warmup_iterations: usize,
pub measurement_iterations: usize,
pub confidence_level: f64,
pub max_degradation_threshold: f64,
pub enable_outlier_detection: bool,
pub sample_size: usize,
}
impl Default for BenchmarkConfig {
fn default() -> Self {
Self {
warmup_iterations: 10,
measurement_iterations: 100,
confidence_level: 0.95,
max_degradation_threshold: 0.10, enable_outlier_detection: true,
sample_size: 50,
}
}
}
#[derive(Debug, Clone)]
pub struct BenchmarkHistory {
pub results: HashMap<String, VecDeque<BenchmarkResult>>,
pub max_history_length: usize,
}
impl BenchmarkHistory {
pub fn new(max_history_length: usize) -> Self {
Self {
results: HashMap::new(),
max_history_length,
}
}
pub fn add_result(&mut self, name: String, result: BenchmarkResult) {
let entry = self.results.entry(name).or_default();
entry.push_back(result);
while entry.len() > self.max_history_length {
entry.pop_front();
}
}
pub fn get_history(&self, name: &str) -> Option<&VecDeque<BenchmarkResult>> {
self.results.get(name)
}
pub fn get_summary(&self, name: &str) -> Option<HistoricalSummary> {
let history = self.get_history(name)?;
if history.is_empty() {
return None;
}
let durations: Vec<f64> = history
.iter()
.map(|r| r.median_duration.as_secs_f64())
.collect();
let mean = durations.iter().sum::<f64>() / durations.len() as f64;
let variance =
durations.iter().map(|d| (d - mean).powi(2)).sum::<f64>() / durations.len() as f64;
let std_dev = variance.sqrt();
let mut sorted = durations.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
Some(HistoricalSummary {
mean_duration: Duration::from_secs_f64(mean),
std_deviation: std_dev,
min_duration: Duration::from_secs_f64(sorted[0]),
max_duration: Duration::from_secs_f64(*sorted.last().expect("last should succeed")),
median_duration: Duration::from_secs_f64(sorted[sorted.len() / 2]),
sample_count: durations.len(),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkResult {
pub name: String,
pub timestamp: SystemTime,
pub durations: Vec<Duration>,
pub median_duration: Duration,
pub mean_duration: Duration,
pub std_deviation: f64,
pub throughput: f64,
pub memory_stats: Option<MemoryStats>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryStats {
pub peak_bytes: usize,
pub average_bytes: usize,
pub allocation_count: usize,
pub deallocation_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoricalSummary {
pub mean_duration: Duration,
pub std_deviation: f64,
pub min_duration: Duration,
pub max_duration: Duration,
pub median_duration: Duration,
pub sample_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceBaseline {
pub name: String,
pub baseline_duration: Duration,
pub acceptable_variance: f64,
pub established_at: SystemTime,
pub git_commit: Option<String>,
}
#[derive(Debug, Clone)]
pub struct RegressionAnalyzer {
pub config: AnalyzerConfig,
pub detected_regressions: Vec<RegressionReport>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyzerConfig {
pub min_sample_size: usize,
pub sensitivity: f64,
pub use_hypothesis_testing: bool,
}
impl Default for AnalyzerConfig {
fn default() -> Self {
Self {
min_sample_size: 10,
sensitivity: 0.05, use_hypothesis_testing: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegressionReport {
pub benchmark_name: String,
pub severity: RegressionSeverity,
pub degradation_percent: f64,
pub current_performance: Duration,
pub expected_performance: Duration,
pub confidence: f64,
pub details: String,
pub detected_at: SystemTime,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum RegressionSeverity {
Minor,
Moderate,
Major,
Critical,
}
impl AdvancedBenchmarkRunner {
pub fn new() -> Self {
Self {
config: BenchmarkConfig::default(),
history: BenchmarkHistory::new(100),
analyzer: RegressionAnalyzer {
config: AnalyzerConfig::default(),
detected_regressions: Vec::new(),
},
baselines: HashMap::new(),
}
}
pub fn with_config(config: BenchmarkConfig) -> Self {
Self {
config,
history: BenchmarkHistory::new(100),
analyzer: RegressionAnalyzer {
config: AnalyzerConfig::default(),
detected_regressions: Vec::new(),
},
baselines: HashMap::new(),
}
}
pub fn run_benchmark<F>(&mut self, name: &str, mut benchmark_fn: F) -> Result<BenchmarkResult>
where
F: FnMut(),
{
for _ in 0..self.config.warmup_iterations {
benchmark_fn();
}
let mut durations = Vec::new();
for _ in 0..self.config.measurement_iterations {
let start = Instant::now();
benchmark_fn();
durations.push(start.elapsed());
}
if self.config.enable_outlier_detection {
durations = self.remove_outliers(durations);
}
let mut sorted_durations = durations.clone();
sorted_durations.sort();
let median = sorted_durations[sorted_durations.len() / 2];
let mean = Duration::from_secs_f64(
durations.iter().map(|d| d.as_secs_f64()).sum::<f64>() / durations.len() as f64,
);
let variance = durations
.iter()
.map(|d| (d.as_secs_f64() - mean.as_secs_f64()).powi(2))
.sum::<f64>()
/ durations.len() as f64;
let std_dev = variance.sqrt();
let throughput = 1.0 / mean.as_secs_f64();
let result = BenchmarkResult {
name: name.to_string(),
timestamp: SystemTime::now(),
durations,
median_duration: median,
mean_duration: mean,
std_deviation: std_dev,
throughput,
memory_stats: None, };
self.history.add_result(name.to_string(), result.clone());
self.check_for_regression(name, &result)?;
Ok(result)
}
fn remove_outliers(&self, mut durations: Vec<Duration>) -> Vec<Duration> {
if durations.len() < 10 {
return durations; }
let values: Vec<f64> = durations.iter().map(|d| d.as_secs_f64()).collect();
let mut sorted_values = values.clone();
sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let q1_idx = sorted_values.len() / 4;
let q3_idx = (sorted_values.len() * 3) / 4;
let q1 = sorted_values[q1_idx];
let q3 = sorted_values[q3_idx];
let iqr = q3 - q1;
let lower_bound = q1 - 1.5 * iqr;
let upper_bound = q3 + 1.5 * iqr;
durations.retain(|d| {
let val = d.as_secs_f64();
val >= lower_bound && val <= upper_bound
});
durations
}
fn check_for_regression(&mut self, name: &str, current: &BenchmarkResult) -> Result<()> {
if let Some(baseline) = self.baselines.get(name) {
let degradation = (current.median_duration.as_secs_f64()
- baseline.baseline_duration.as_secs_f64())
/ baseline.baseline_duration.as_secs_f64();
if degradation > self.config.max_degradation_threshold {
let severity = match degradation {
d if d < 0.05 => RegressionSeverity::Minor,
d if d < 0.15 => RegressionSeverity::Moderate,
d if d < 0.30 => RegressionSeverity::Major,
_ => RegressionSeverity::Critical,
};
let report = RegressionReport {
benchmark_name: name.to_string(),
severity,
degradation_percent: degradation * 100.0,
current_performance: current.median_duration,
expected_performance: baseline.baseline_duration,
confidence: self.config.confidence_level,
details: format!(
"Performance degraded by {:.2}% compared to baseline",
degradation * 100.0
),
detected_at: SystemTime::now(),
};
self.analyzer.detected_regressions.push(report);
}
}
Ok(())
}
pub fn set_baseline(&mut self, name: String, duration: Duration) {
let baseline = PerformanceBaseline {
name: name.clone(),
baseline_duration: duration,
acceptable_variance: self.config.max_degradation_threshold,
established_at: SystemTime::now(),
git_commit: None,
};
self.baselines.insert(name, baseline);
}
pub fn get_regressions(&self) -> &[RegressionReport] {
&self.analyzer.detected_regressions
}
pub fn generate_report(&self) -> BenchmarkReport {
let mut benchmark_summaries = HashMap::new();
for name in self.history.results.keys() {
if let Some(summary) = self.history.get_summary(name) {
benchmark_summaries.insert(name.clone(), summary);
}
}
BenchmarkReport {
total_benchmarks: self.history.results.len(),
regressions_detected: self.analyzer.detected_regressions.len(),
benchmark_summaries,
regressions: self.analyzer.detected_regressions.clone(),
generated_at: SystemTime::now(),
}
}
}
impl Default for AdvancedBenchmarkRunner {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkReport {
pub total_benchmarks: usize,
pub regressions_detected: usize,
pub benchmark_summaries: HashMap<String, HistoricalSummary>,
pub regressions: Vec<RegressionReport>,
pub generated_at: SystemTime,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_benchmark_runner_creation() {
let runner = AdvancedBenchmarkRunner::new();
assert_eq!(runner.config.warmup_iterations, 10);
assert_eq!(runner.config.measurement_iterations, 100);
}
#[test]
fn test_custom_config() {
let config = BenchmarkConfig {
warmup_iterations: 5,
measurement_iterations: 50,
confidence_level: 0.99,
max_degradation_threshold: 0.05,
enable_outlier_detection: false,
sample_size: 30,
};
let runner = AdvancedBenchmarkRunner::with_config(config);
assert_eq!(runner.config.warmup_iterations, 5);
assert_eq!(runner.config.measurement_iterations, 50);
}
#[test]
fn test_simple_benchmark() {
let mut runner = AdvancedBenchmarkRunner::new();
let result = runner
.run_benchmark("test_benchmark", || {
let _sum: u64 = (0..1000).sum();
})
.expect("expected valid value");
assert_eq!(result.name, "test_benchmark");
assert!(result.median_duration > Duration::from_nanos(0));
assert!(result.throughput > 0.0);
}
#[test]
fn test_baseline_setting() {
let mut runner = AdvancedBenchmarkRunner::new();
runner.set_baseline("test".to_string(), Duration::from_millis(10));
assert!(runner.baselines.contains_key("test"));
assert_eq!(
runner
.baselines
.get("test")
.expect("key should exist")
.baseline_duration,
Duration::from_millis(10)
);
}
#[test]
fn test_regression_detection() {
let mut runner = AdvancedBenchmarkRunner::new();
runner.set_baseline("test".to_string(), Duration::from_micros(100));
let _result = runner
.run_benchmark("test", || {
std::thread::sleep(Duration::from_micros(200));
})
.expect("expected valid value");
let regressions = runner.get_regressions();
assert!(!regressions.is_empty());
}
#[test]
fn test_history_tracking() {
let mut runner = AdvancedBenchmarkRunner::new();
runner
.run_benchmark("test", || {
let _x = 1 + 1;
})
.expect("expected valid value");
runner
.run_benchmark("test", || {
let _x = 1 + 1;
})
.expect("expected valid value");
let history = runner
.history
.get_history("test")
.expect("get_history should succeed");
assert_eq!(history.len(), 2);
}
#[test]
fn test_historical_summary() {
let mut runner = AdvancedBenchmarkRunner::new();
for _ in 0..5 {
runner
.run_benchmark("test", || {
let mut sum = 0u64;
for i in 0..100 {
sum = std::hint::black_box(sum.wrapping_add(i));
}
std::hint::black_box(sum);
})
.expect("expected valid value");
}
let summary = runner
.history
.get_summary("test")
.expect("get_summary should succeed");
assert_eq!(summary.sample_count, 5);
assert!(summary.mean_duration > Duration::from_nanos(0));
}
#[test]
fn test_report_generation() {
let mut runner = AdvancedBenchmarkRunner::new();
runner
.run_benchmark("bench1", || {
let x = std::hint::black_box(1 + 1);
std::hint::black_box(x);
})
.expect("expected valid value");
runner
.run_benchmark("bench2", || {
let y = std::hint::black_box(2 + 2);
std::hint::black_box(y);
})
.expect("expected valid value");
let report = runner.generate_report();
assert_eq!(report.total_benchmarks, 2);
}
#[test]
fn test_outlier_removal() {
let runner = AdvancedBenchmarkRunner::new();
let durations = vec![
Duration::from_millis(10),
Duration::from_millis(11),
Duration::from_millis(10),
Duration::from_millis(100), Duration::from_millis(10),
Duration::from_millis(11),
Duration::from_millis(10),
Duration::from_millis(11),
Duration::from_millis(10),
Duration::from_millis(11),
];
let filtered = runner.remove_outliers(durations);
assert!(filtered.len() < 10); }
#[test]
fn test_regression_severity() {
use RegressionSeverity::*;
assert!(Minor < Moderate);
assert!(Moderate < Major);
assert!(Major < Critical);
}
}