benchkit 0.19.0

Lightweight benchmarking toolkit focused on practical performance analysis and report generation. Non-restrictive alternative to criterion, designed for easy integration and markdown report generation.
Documentation
//! Analysis tools for benchmark results
//!
//! This module provides tools for analyzing benchmark results, including
//! comparative analysis, regression detection, and statistical analysis.

use crate::measurement::{ BenchmarkResult, Comparison };
use std::collections::HashMap;

/// Comparative analysis for multiple algorithm variants
pub struct ComparativeAnalysis {
  name: String,
  variants: HashMap<String, Box<dyn FnMut() + Send>>,
}

impl std::fmt::Debug for ComparativeAnalysis {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    f.debug_struct("ComparativeAnalysis")
      .field("name", &self.name)
      .field("variants", &format!("{} variants", self.variants.len()))
      .finish()
  }
}

impl ComparativeAnalysis {
  /// Create a new comparative analysis
  pub fn new(name: impl Into<String>) -> Self {
    Self {
      name: name.into(),
      variants: HashMap::new(), 
    }
  }

  /// Add an algorithm variant to compare
  #[must_use]
  pub fn add_variant<F>(mut self, name: impl Into<String>, f: F) -> Self 
  where
    F: FnMut() + Send + 'static,
  {
    self.variants.insert(name.into(), Box::new(f));
    self
  }

  /// Add an algorithm variant to compare (builder pattern alias)
  #[must_use]
  pub fn algorithm<F>(self, name: impl Into<String>, f: F) -> Self
  where  
    F: FnMut() + Send + 'static,
  {
    self.add_variant(name, f)
  }

  /// Run the comparative analysis
  #[must_use]
  pub fn run(self) -> ComparisonAnalysisReport {
    let mut results = HashMap::new();
    
    for (name, variant) in self.variants {
      let result = crate::measurement::bench_function(&name, variant);
      results.insert(name.clone(), result);
    }
    
    ComparisonAnalysisReport {
      name: self.name,
      results,
    }
  }
}

/// Report containing results of comparative analysis
#[derive(Debug)]
pub struct ComparisonAnalysisReport {
  /// Name of the comparison analysis
  pub name: String,
  /// Results of each algorithm variant tested
  pub results: HashMap<String, BenchmarkResult>,
}

impl ComparisonAnalysisReport {
  /// Get the fastest result
  #[must_use]
  pub fn fastest(&self) -> Option<(&String, &BenchmarkResult)> {
    self.results
      .iter()
      .min_by(|a, b| a.1.mean_time().cmp(&b.1.mean_time()))
  }

  /// Get the slowest result
  #[must_use]
  pub fn slowest(&self) -> Option<(&String, &BenchmarkResult)> {
    self.results
      .iter()
      .max_by(|a, b| a.1.mean_time().cmp(&b.1.mean_time()))
  }

  /// Get all results sorted by performance (fastest first)
  #[must_use]
  pub fn sorted_by_performance(&self) -> Vec<(&String, &BenchmarkResult)> {
    let mut results: Vec<_> = self.results.iter().collect();
    results.sort_by(|a, b| a.1.mean_time().cmp(&b.1.mean_time()));
    results
  }

  /// Print a summary of the comparison
  pub fn print_summary(&self) {
    println!("=== {} Comparison ===", self.name);
    
    if let Some((fastest_name, fastest_result)) = self.fastest() {
      println!("🏆 Fastest: {} ({:.2?})", fastest_name, fastest_result.mean_time());
      
      // Show relative performance of all variants
      println!("\nRelative Performance:");
      for (name, result) in self.sorted_by_performance() {
        let _comparison = result.compare(fastest_result);
        let relative_speed = if name == fastest_name {
          "baseline".to_string()
        } else {
          format!("{:.1}x slower", 
                  result.mean_time().as_secs_f64() / fastest_result.mean_time().as_secs_f64())
        };
        
        println!("  {} - {:.2?} ({})", name, result.mean_time(), relative_speed);
      }
    }
    
    println!(); // Empty line for readability
  }

  /// Generate markdown summary
  ///
  /// # Panics
  ///
  /// Panics if `fastest()` returns Some but `unwrap()` fails on the same call.
  #[must_use]
  pub fn to_markdown(&self) -> String {
    let mut output = String::new();
    output.push_str(&format!("## {} Comparison\n\n", self.name));
    
    if self.results.is_empty() {
      output.push_str("No results available.\n");
      return output;
    }
    
    // Results table
    output.push_str("| Algorithm | Mean Time | Operations/sec | Relative Performance |\n");
    output.push_str("|-----------|-----------|----------------|----------------------|\n");
    
    let fastest = self.fastest().map(|(_, result)| result);
    
    for (name, result) in self.sorted_by_performance() {
      let relative = if let Some(fastest_result) = fastest {
        if result.mean_time() == fastest_result.mean_time() {
          "**Fastest**".to_string()
        } else {
          format!("{:.1}x slower", 
                  result.mean_time().as_secs_f64() / fastest_result.mean_time().as_secs_f64())
        }
      } else {
        "N/A".to_string()
      };
      
      output.push_str(&format!("| {} | {:.2?} | {:.0} | {} |\n",
                               name,
                               result.mean_time(),
                               result.operations_per_second(),
                               relative));
    }
    
    output.push('\n');
    
    // Key insights
    if let (Some((fastest_name, _)), Some((slowest_name, slowest_result))) = 
      (self.fastest(), self.slowest()) {
      output.push_str("### Key Insights\n\n");
      output.push_str(&format!("- **Best performing**: {fastest_name} algorithm\n"));
      if fastest_name != slowest_name {
        if let Some((_, fastest)) = self.fastest() {
          let speedup = slowest_result.mean_time().as_secs_f64() / fastest.mean_time().as_secs_f64();
          output.push_str(&format!("- **Performance range**: {speedup:.1}x difference between fastest and slowest\n"));
        }
      }
    }
    
    output
  }
}

/// Performance regression analysis
#[derive(Debug, Clone)]
pub struct RegressionAnalysis {
  /// Baseline benchmark results to compare against
  pub baseline_results: HashMap<String, BenchmarkResult>,
  /// Current benchmark results being analyzed
  pub current_results: HashMap<String, BenchmarkResult>,
}

impl RegressionAnalysis {
  /// Create new regression analysis from baseline and current results
  #[must_use]
  pub fn new(
    baseline: HashMap<String, BenchmarkResult>,
    current: HashMap<String, BenchmarkResult>
  ) -> Self {
    Self {
      baseline_results: baseline,
      current_results: current,
    }
  }

  /// Detect regressions (performance degradations > threshold)
  #[must_use]
  pub fn detect_regressions(&self, threshold_percent: f64) -> Vec<Comparison> {
    let mut regressions = Vec::new();
    
    for (name, current) in &self.current_results {
      if let Some(baseline) = self.baseline_results.get(name) {
        let comparison = current.compare(baseline);
        if comparison.improvement_percentage < -threshold_percent {
          regressions.push(comparison);
        }
      }
    }
    
    regressions
  }

  /// Detect improvements (performance gains > threshold)
  #[must_use]
  pub fn detect_improvements(&self, threshold_percent: f64) -> Vec<Comparison> {
    let mut improvements = Vec::new();
    
    for (name, current) in &self.current_results {
      if let Some(baseline) = self.baseline_results.get(name) {
        let comparison = current.compare(baseline);
        if comparison.improvement_percentage > threshold_percent {
          improvements.push(comparison);
        }
      }
    }
    
    improvements
  }

  /// Get overall regression percentage (worst case)
  #[must_use]
  pub fn worst_regression_percentage(&self) -> f64 {
    self.detect_regressions(0.0)
      .iter()
      .map(|c| c.improvement_percentage.abs())
      .fold(0.0, f64::max)
  }

  /// Generate regression report
  #[must_use]
  pub fn generate_report(&self) -> String {
    let mut report = String::new();
    report.push_str("# Performance Regression Analysis\n\n");
    
    let regressions = self.detect_regressions(5.0);
    let improvements = self.detect_improvements(5.0);
    
    if !regressions.is_empty() {
      report.push_str("## 🚨 Performance Regressions\n\n");
      for regression in &regressions {
        report.push_str(&format!("- **{}**: {:.1}% slower ({:.2?} -> {:.2?})\n",
                                 regression.current.name,
                                 regression.improvement_percentage.abs(),
                                 regression.baseline.mean_time(),
                                 regression.current.mean_time()));
      }
      report.push('\n');
    }
    
    if !improvements.is_empty() {
      report.push_str("## 🎉 Performance Improvements\n\n");
      for improvement in &improvements {
        report.push_str(&format!("- **{}**: {:.1}% faster ({:.2?} -> {:.2?})\n",
                                 improvement.current.name,
                                 improvement.improvement_percentage,
                                 improvement.baseline.mean_time(),
                                 improvement.current.mean_time()));
      }
      report.push('\n');
    }
    
    if regressions.is_empty() && improvements.is_empty() {
      report.push_str("## ✅ No Significant Changes\n\n");
      report.push_str("Performance appears stable compared to baseline.\n\n");
    }
    
    report
  }
}