use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use super::{BenchmarkResultSet, BenchmarkResult, Runtime, MetricType};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceHistory {
pub commit_hash: String,
pub timestamp: SystemTime,
pub results: BenchmarkResultSet,
pub git_branch: String,
pub build_info: BuildInfo,
}
impl PerformanceHistory {
pub fn new(
commit_hash: String,
results: BenchmarkResultSet,
git_branch: String,
) -> Self {
Self {
commit_hash,
timestamp: SystemTime::now(),
results,
git_branch,
build_info: BuildInfo::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BuildInfo {
pub rust_version: String,
pub v8_version: Option<String>,
pub build_time: SystemTime,
pub optimization_level: String,
}
#[derive(Debug)]
pub struct RegressionDetector {
pub history_path: PathBuf,
pub significance_threshold: f64,
pub regression_threshold: f64,
}
impl RegressionDetector {
pub fn new(history_path: PathBuf) -> Self {
Self {
history_path,
significance_threshold: 0.05,
regression_threshold: 10.0, }
}
pub async fn detect_regressions(
&self,
current_results: &BenchmarkResultSet,
baseline_results: &BenchmarkResultSet,
) -> Result<RegressionReport, super::BenchmarkError> {
let mut report = RegressionReport::new();
for current_result in ¤t_results.results {
if let Some(baseline_result) = Self::find_baseline_result(
baseline_results,
¤t_result.name,
current_result.runtime,
) {
let analysis: _ = self.analyze_regression(current_result, baseline_result);
report.add_analysis(analysis);
}
}
Ok(report)
}
fn analyze_regression(
&self,
current: &BenchmarkResult,
baseline: &BenchmarkResult,
) -> RegressionAnalysis {
let performance_change: _ = self.calculate_performance_change(current, baseline);
let is_regression: _ = performance_change < -self.regression_threshold;
let is_significant: _ = Self::is_statistically_significant(
current,
baseline,
self.significance_threshold,
);
RegressionAnalysis {
test_name: current.name.clone(),
runtime: current.runtime,
baseline_performance: baseline.average_duration(),
current_performance: current.average_duration(),
performance_change_percent: performance_change,
is_regression,
is_significant,
confidence_level: if is_significant { 0.95 } else { 0.0 },
}
}
fn calculate_performance_change(&self, current: &BenchmarkResult, baseline: &BenchmarkResult) -> f64 {
let baseline_ns: _ = baseline.average_duration().as_nanos() as f64;
let current_ns: _ = current.average_duration().as_nanos() as f64;
if baseline_ns == 0.0 {
0.0
} else {
((baseline_ns - current_ns) / baseline_ns) * 100.0
}
}
fn is_statistically_significant(
current: &BenchmarkResult,
baseline: &BenchmarkResult,
alpha: f64,
) -> bool {
current.is_statistically_significant(baseline, alpha)
}
fn find_baseline_result(
baseline_results: &BenchmarkResultSet,
test_name: &str,
runtime: Runtime,
) -> Option<&BenchmarkResult> {
baseline_results.results.iter().find(|r| {
r.name == test_name && r.runtime == runtime
})
}
pub async fn save_history(
&self,
history: &PerformanceHistory,
) -> Result<(), super::BenchmarkError> {
use super::super::utils::{create_dir_if_not_exists, write_file};
create_dir_if_not_exists(&self.history_path)?;
let filename: _ = format!(
"{}_{}.json",
history.commit_hash,
history.timestamp.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()
);
let filepath: _ = self.history_path.join(filename);
let json: _ = serde_json::to_string_pretty(history)
.map_err(super::BenchmarkError::JsonError)?;
write_file(&filepath, &json)?;
Ok(())
}
pub async fn load_history(
&self,
commit_hash: &str,
) -> Result<PerformanceHistory, super::BenchmarkError> {
use super::super::utils::read_file;
let entries: _ = tokio::fs::read_dir(&self.history_path).await?;
for entry in entries {
let entry: _ = entry?;
let filename: _ = entry.file_name().to_string_lossy().to_string();
if filename.starts_with(commit_hash) {
let filepath: _ = self.history_path.join(&filename);
let content: _ = read_file(&filepath)?;
let history: PerformanceHistory = serde_json::from_str(&content)
.map_err(super::BenchmarkError::JsonError)?;
return Ok(history);
}
}
Err(super::BenchmarkError::ConfigError(
format!("History not found for commit: {}", commit_hash))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RegressionReport {
pub analyses: Vec<RegressionAnalysis>,
pub summary: RegressionSummary,
}
impl RegressionReport {
pub fn new() -> Self {
Self::default()
}
pub fn add_analysis(&mut self, analysis: RegressionAnalysis) {
self.analyses.push(analysis.clone());
self.update_summary();
}
fn update_summary(&mut self) {
let total_tests: _ = self.analyses.len();
let regressions: _ = self.analyses.iter().filter(|a| a.is_regression).count();
let significant_regressions: _ = self.analyses
.iter()
.filter(|a| a.is_regression && a.is_significant)
.count();
self.summary = RegressionSummary {
total_tests,
regressions,
significant_regressions,
regression_rate: if total_tests > 0 {
(regressions as f64 / total_tests as f64) * 100.0
} else {
0.0
},
max_performance_decline: self.analyses
.iter()
.filter(|a| a.is_regression)
.map(|a| a.performance_change_percent.abs())
.fold(0.0, f64::max),
};
}
pub fn has_regressions(&self) -> bool {
self.analyses.iter().any(|a| a.is_regression)
}
pub fn has_significant_regressions(&self) -> bool {
self.analyses.iter().any(|a| a.is_regression && a.is_significant)
}
pub fn get_regressions(&self) -> Vec<&RegressionAnalysis> {
self.analyses.iter().filter(|a| a.is_regression).collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegressionAnalysis {
pub test_name: String,
pub runtime: Runtime,
pub baseline_performance: Duration,
pub current_performance: Duration,
pub performance_change_percent: f64,
pub is_regression: bool,
pub is_significant: bool,
pub confidence_level: f64,
}
impl RegressionAnalysis {
pub fn get_change_description(&self) -> String {
if self.performance_change_percent > 0.0 {
format!("性能提升 {:.2}%", self.performance_change_percent)
} else if self.performance_change_percent < 0.0 {
format!("性能下降 {:.2}%", self.performance_change_percent.abs())
} else {
"性能无变化".to_string()
}
}
pub fn get_severity(&self) -> RegressionSeverity {
if !self.is_regression {
return RegressionSeverity::None;
}
let decline: _ = self.performance_change_percent.abs();
if decline >= 50.0 {
RegressionSeverity::Critical
} else if decline >= 20.0 {
RegressionSeverity::High
} else if decline >= 10.0 {
RegressionSeverity::Medium
} else {
RegressionSeverity::Low
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RegressionSeverity {
None,
Low,
Medium,
High,
Critical,
}
impl std::fmt::Display for RegressionSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RegressionSeverity::None => write!(f, "None"),
RegressionSeverity::Low => write!(f, "Low"),
RegressionSeverity::Medium => write!(f, "Medium"),
RegressionSeverity::High => write!(f, "High"),
RegressionSeverity::Critical => write!(f, "Critical"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegressionSummary {
pub total_tests: usize,
pub regressions: usize,
pub significant_regressions: usize,
pub regression_rate: f64,
pub max_performance_decline: f64,
}
impl RegressionSummary {
pub fn is_pass(&self) -> bool {
self.significant_regressions == 0
}
pub fn get_status(&self) -> String {
if self.is_pass() {
"PASS".to_string()
} else {
"FAIL".to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{BTreeMap, HashMap};
use std::time::{Duration, SystemTime};
#[test]
fn test_regression_analysis() {
let current: _ = super::super::result::BenchmarkResult::new("test", Runtime::Amber);
let baseline: _ = super::super::result::BenchmarkResult::new("test", Runtime::Amber);
let detector: _ = RegressionDetector::new(PathBuf::from("/tmp/test"));
let analysis: _ = detector.analyze_regression(¤t, &baseline);
println!("Analysis: {:?}", analysis);
}
#[test]
fn test_regression_summary() {
let mut report = RegressionReport::new();
for i in 0..5 {
let mut analysis = RegressionAnalysis {
test_name: format!("test_{}", i),
runtime: Runtime::Amber,
baseline_performance: Duration::from_millis(100),
current_performance: Duration::from_millis(120),
performance_change_percent: -20.0,
is_regression: i < 3,
is_significant: i < 2,
confidence_level: 0.95,
};
report.add_analysis(analysis);
}
println!("Summary: {:?}", report.summary);
println!("Has regressions: {}", report.has_regressions());
println!("Has significant regressions: {}", report.has_significant_regressions());
}
}