use std::collections::HashMap;
pub type CompareResult<T> = Result<T, CompareError>;
#[derive(Debug, Clone, PartialEq)]
pub enum CompareError {
InsufficientSamples { got: usize, need: usize },
MetricNotFound { name: String },
ZeroVariance { metric: String },
InvalidConfidence { value: f64 },
NoCommonMetrics,
}
impl std::fmt::Display for CompareError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InsufficientSamples { got, need } => {
write!(f, "Insufficient samples: got {}, need {}", got, need)
}
Self::MetricNotFound { name } => {
write!(f, "Metric not found: {}", name)
}
Self::ZeroVariance { metric } => {
write!(f, "Zero variance in metric: {}", metric)
}
Self::InvalidConfidence { value } => {
write!(f, "Invalid confidence level: {}", value)
}
Self::NoCommonMetrics => {
write!(f, "Profiles have no common metrics")
}
}
}
}
impl std::error::Error for CompareError {}
#[derive(Debug, Clone)]
pub struct BenchmarkProfile {
pub name: String,
pub description: Option<String>,
pub metrics: HashMap<String, MetricSamples>,
pub metadata: HashMap<String, String>,
pub timestamp_ns: u64,
}
impl BenchmarkProfile {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
metrics: HashMap::new(),
metadata: HashMap::new(),
timestamp_ns: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0),
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn add_metric(&mut self, name: impl Into<String>, samples: Vec<f64>) {
self.metrics
.insert(name.into(), MetricSamples::new(samples));
}
pub fn get_metric(&self, name: &str) -> Option<&MetricSamples> {
self.metrics.get(name)
}
pub fn metric_names(&self) -> impl Iterator<Item = &String> {
self.metrics.keys()
}
pub fn metric_count(&self) -> usize {
self.metrics.len()
}
}
#[derive(Debug, Clone)]
pub struct MetricSamples {
pub values: Vec<f64>,
mean: f64,
variance: f64,
std_dev: f64,
}
impl MetricSamples {
pub fn new(values: Vec<f64>) -> Self {
let (mean, variance, std_dev) = if values.is_empty() {
(0.0, 0.0, 0.0)
} else {
let mean = values.iter().sum::<f64>() / values.len() as f64;
let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
/ (values.len() - 1).max(1) as f64;
let std_dev = variance.sqrt();
(mean, variance, std_dev)
};
Self {
values,
mean,
variance,
std_dev,
}
}
pub fn count(&self) -> usize {
self.values.len()
}
pub fn mean(&self) -> f64 {
self.mean
}
pub fn variance(&self) -> f64 {
self.variance
}
pub fn std_dev(&self) -> f64 {
self.std_dev
}
pub fn min(&self) -> f64 {
self.values.iter().cloned().fold(f64::INFINITY, f64::min)
}
pub fn max(&self) -> f64 {
self.values
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max)
}
}
#[derive(Debug, Clone)]
pub struct WelchTestResult {
pub t_statistic: f64,
pub degrees_of_freedom: f64,
pub p_value: f64,
pub significant: bool,
pub confidence_level: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EffectMagnitude {
Negligible,
Small,
Medium,
Large,
}
impl EffectMagnitude {
pub fn from_cohens_d(d: f64) -> Self {
let abs_d = d.abs();
if abs_d < 0.2 {
Self::Negligible
} else if abs_d < 0.5 {
Self::Small
} else if abs_d < 0.8 {
Self::Medium
} else {
Self::Large
}
}
}
#[derive(Debug, Clone)]
pub struct EffectSizeResult {
pub cohens_d: f64,
pub magnitude: EffectMagnitude,
pub percent_change: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeDirection {
Improved,
Regressed,
NoChange,
}
#[derive(Debug, Clone)]
pub struct MetricComparison {
pub name: String,
pub baseline_mean: f64,
pub baseline_std: f64,
pub comparison_mean: f64,
pub comparison_std: f64,
pub t_test: WelchTestResult,
pub effect_size: EffectSizeResult,
pub direction: ChangeDirection,
pub is_regression: bool,
pub ci_lower: f64,
pub ci_upper: f64,
}
#[derive(Debug, Clone)]
pub struct ProfileComparison {
pub baseline_name: String,
pub comparison_name: String,
pub metrics: Vec<MetricComparison>,
pub regressions: Vec<String>,
pub improvements: Vec<String>,
pub verdict: ComparisonVerdict,
pub corrected_alpha: f64,
}
impl ProfileComparison {
pub fn regression_count(&self) -> usize {
self.regressions.len()
}
pub fn improvement_count(&self) -> usize {
self.improvements.len()
}
pub fn unchanged_count(&self) -> usize {
self.metrics.len() - self.regression_count() - self.improvement_count()
}
pub fn has_regressions(&self) -> bool {
!self.regressions.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComparisonVerdict {
Pass,
Warning,
Fail,
}