use crate::benchmarking::{BenchmarkConfig, BenchmarkResults, BenchmarkSuite};
use crate::error::{Result, SklearsError};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use std::time::Duration;
#[derive(Debug)]
pub struct PerformanceReporter {
config: ReportConfig,
database: PerformanceDatabase,
analyzers: Vec<Box<dyn PerformanceAnalyzer>>,
}
impl PerformanceReporter {
pub fn new(config: ReportConfig) -> Self {
let database = PerformanceDatabase::new(&config.database_path);
let analyzers: Vec<Box<dyn PerformanceAnalyzer>> = vec![
Box::new(RegressionAnalyzer::new(&config)),
Box::new(TrendAnalyzer::new(&config)),
Box::new(ResourceAnalyzer::new(&config)),
Box::new(ScalabilityAnalyzer::new(&config)),
];
Self {
config,
database,
analyzers,
}
}
pub fn run_ci_analysis(&mut self) -> Result<PerformanceReport> {
println!("Starting automated performance analysis...");
let benchmark_results = self.run_benchmarks()?;
self.database.store_results(&benchmark_results)?;
let mut analysis_results = Vec::new();
for analyzer in &self.analyzers {
let result = analyzer.analyze(&benchmark_results, &self.database)?;
analysis_results.push(result);
}
let report = self.generate_report(benchmark_results, analysis_results)?;
if report.has_regressions() && self.config.alert_config.enabled {
self.send_alerts(&report)?;
}
self.save_report(&report)?;
Ok(report)
}
fn run_benchmarks(&self) -> Result<BenchmarkResults> {
let benchmark_config = BenchmarkConfig::new()
.with_dataset_sizes(self.config.benchmark_sizes.clone())
.with_iterations(self.config.benchmark_iterations)
.with_accuracy_tolerance(self.config.accuracy_tolerance)
.with_memory_profiling(true);
let mut suite = BenchmarkSuite::new(benchmark_config);
for algorithm in &self.config.algorithms {
match algorithm.as_str() {
"linear_regression" => {
suite.add_benchmark(
"linear_regression",
crate::benchmarking::AlgorithmBenchmark::linear_regression(),
);
}
"random_forest" => {
suite.add_benchmark(
"random_forest",
crate::benchmarking::AlgorithmBenchmark::random_forest(),
);
}
"k_means" => {
suite.add_benchmark(
"k_means",
crate::benchmarking::AlgorithmBenchmark::k_means(),
);
}
_ => {
println!("Warning: Unknown algorithm '{algorithm}'");
}
}
}
suite.run()
}
fn generate_report(
&self,
results: BenchmarkResults,
analyses: Vec<AnalysisResult>,
) -> Result<PerformanceReport> {
let timestamp = Utc::now();
let mut report = PerformanceReport {
timestamp,
config: self.config.clone(),
benchmark_results: results,
analysis_results: analyses,
summary: ReportSummary::default(),
output_path: PathBuf::new(),
};
report.summary = self.generate_summary(&report)?;
Ok(report)
}
fn generate_summary(&self, report: &PerformanceReport) -> Result<ReportSummary> {
let mut summary = ReportSummary::default();
for analysis in &report.analysis_results {
match &analysis.analysis_type {
AnalysisType::Regression(regression) => {
summary.total_regressions += regression.flagged_algorithms.len();
summary.total_improvements += regression.improved_algorithms.len();
}
AnalysisType::Trend(trend) => {
summary.performance_trend = trend.overall_trend.clone();
}
AnalysisType::Resource(resource) => {
summary.memory_efficiency = resource.memory_efficiency_score;
summary.cpu_efficiency = resource.cpu_efficiency_score;
}
AnalysisType::Scalability(scalability) => {
summary.scalability_score = scalability.overall_score;
}
}
}
summary.overall_health = if summary.total_regressions > 0 {
HealthStatus::Poor
} else if summary.total_improvements > 0 {
HealthStatus::Good
} else {
HealthStatus::Stable
};
Ok(summary)
}
fn save_report(&self, report: &PerformanceReport) -> Result<()> {
let base_path = &self.config.output_directory;
std::fs::create_dir_all(base_path).map_err(|e| {
SklearsError::InvalidInput(format!("Cannot create output directory: {e}"))
})?;
let timestamp_str = report.timestamp.format("%Y%m%d_%H%M%S").to_string();
if self.config.output_formats.contains(&OutputFormat::Json) {
let json_path = base_path.join(format!("performance_report_{timestamp_str}.json"));
let json_data = serde_json::to_string_pretty(report)
.map_err(|e| SklearsError::InvalidInput(format!("Cannot serialize report: {e}")))?;
std::fs::write(&json_path, json_data).map_err(|e| {
SklearsError::InvalidInput(format!("Cannot write JSON report: {e}"))
})?;
}
if self.config.output_formats.contains(&OutputFormat::Html) {
let html_path = base_path.join(format!("performance_report_{timestamp_str}.html"));
let html_content = self.generate_html_report(report)?;
std::fs::write(&html_path, html_content).map_err(|e| {
SklearsError::InvalidInput(format!("Cannot write HTML report: {e}"))
})?;
}
if self.config.output_formats.contains(&OutputFormat::Csv) {
let csv_path = base_path.join(format!("performance_summary_{timestamp_str}.csv"));
let csv_content = self.generate_csv_summary(report)?;
std::fs::write(&csv_path, csv_content)
.map_err(|e| SklearsError::InvalidInput(format!("Cannot write CSV report: {e}")))?;
}
Ok(())
}
fn generate_html_report(&self, report: &PerformanceReport) -> Result<String> {
let mut html = String::new();
html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
html.push_str("<title>Sklears Performance Report</title>\n");
html.push_str("<style>\n");
html.push_str("body { font-family: Arial, sans-serif; margin: 40px; }\n");
html.push_str("table { border-collapse: collapse; width: 100%; }\n");
html.push_str("th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }\n");
html.push_str("th { background-color: #f2f2f2; }\n");
html.push_str(".regression { background-color: #ffebee; }\n");
html.push_str(".improvement { background-color: #e8f5e8; }\n");
html.push_str(".stable { background-color: #f0f0f0; }\n");
html.push_str("</style>\n</head>\n<body>\n");
html.push_str("<h1>Sklears Performance Report</h1>\n");
html.push_str(&format!(
"<p>Generated: {}</p>\n",
report.timestamp.format("%Y-%m-%d %H:%M:%S UTC")
));
html.push_str("<h2>Executive Summary</h2>\n");
html.push_str("<table>\n");
html.push_str("<tr><th>Metric</th><th>Value</th><th>Status</th></tr>\n");
html.push_str(&format!(
"<tr><td>Overall Health</td><td>{:?}</td><td class=\"{}\">{:?}</td></tr>\n",
report.summary.overall_health,
self.health_status_class(&report.summary.overall_health),
report.summary.overall_health
));
html.push_str(&format!("<tr><td>Performance Regressions</td><td>{}</td><td class=\"regression\">{}</td></tr>\n",
report.summary.total_regressions,
if report.summary.total_regressions > 0 { "ALERT" } else { "OK" }));
html.push_str(&format!("<tr><td>Performance Improvements</td><td>{}</td><td class=\"improvement\">{}</td></tr>\n",
report.summary.total_improvements,
if report.summary.total_improvements > 0 { "GOOD" } else { "NONE" }));
html.push_str(&format!(
"<tr><td>Memory Efficiency</td><td>{:.2}</td><td>{}</td></tr>\n",
report.summary.memory_efficiency,
if report.summary.memory_efficiency > 0.8 {
"GOOD"
} else {
"NEEDS IMPROVEMENT"
}
));
html.push_str("</table>\n");
html.push_str("<h2>Detailed Results</h2>\n");
for analysis in &report.analysis_results {
html.push_str(&format!("<h3>{}</h3>\n", analysis.analyzer_name));
html.push_str(&format!("<p>{}</p>\n", analysis.description));
if let AnalysisType::Regression(regression) = &analysis.analysis_type {
if !regression.flagged_algorithms.is_empty() {
html.push_str("<h4>Performance Regressions Detected</h4>\n");
html.push_str("<ul>\n");
for algorithm in ®ression.flagged_algorithms {
html.push_str(&format!(
"<li class=\"regression\">{}: {:.2}% slower</li>\n",
algorithm.name, algorithm.performance_change
));
}
html.push_str("</ul>\n");
}
}
}
html.push_str("</body>\n</html>");
Ok(html)
}
fn generate_csv_summary(&self, report: &PerformanceReport) -> Result<String> {
let mut csv = String::new();
csv.push_str("Metric,Value,Status\n");
csv.push_str(&format!(
"Overall Health,{:?},{}\n",
report.summary.overall_health,
if matches!(report.summary.overall_health, HealthStatus::Good) {
"GOOD"
} else {
"ALERT"
}
));
csv.push_str(&format!(
"Total Regressions,{},{}\n",
report.summary.total_regressions,
if report.summary.total_regressions > 0 {
"ALERT"
} else {
"OK"
}
));
csv.push_str(&format!(
"Total Improvements,{},{}\n",
report.summary.total_improvements,
if report.summary.total_improvements > 0 {
"GOOD"
} else {
"NONE"
}
));
csv.push_str(&format!(
"Memory Efficiency,{:.2},{}\n",
report.summary.memory_efficiency,
if report.summary.memory_efficiency > 0.8 {
"GOOD"
} else {
"NEEDS_IMPROVEMENT"
}
));
Ok(csv)
}
fn health_status_class(&self, status: &HealthStatus) -> &'static str {
match status {
HealthStatus::Good => "improvement",
HealthStatus::Stable => "stable",
HealthStatus::Poor => "regression",
}
}
fn send_alerts(&self, report: &PerformanceReport) -> Result<()> {
if !self.config.alert_config.enabled {
return Ok(());
}
let alert_message = format!(
"Performance Alert: {} regressions detected in sklears performance analysis.\nReport timestamp: {}",
report.summary.total_regressions,
report.timestamp.format("%Y-%m-%d %H:%M:%S UTC")
);
if self.config.alert_config.email_notifications {
self.send_email_alert(&alert_message)?;
}
if let Some(ref webhook) = self.config.alert_config.slack_webhook {
self.send_slack_alert(webhook, &alert_message)?;
}
Ok(())
}
fn send_email_alert(&self, message: &str) -> Result<()> {
println!("EMAIL ALERT: {message}");
Ok(())
}
fn send_slack_alert(&self, webhook: &str, message: &str) -> Result<()> {
println!("SLACK ALERT to {webhook}: {message}");
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportConfig {
pub database_path: PathBuf,
pub output_directory: PathBuf,
pub output_formats: Vec<OutputFormat>,
pub algorithms: Vec<String>,
pub benchmark_sizes: Vec<usize>,
pub benchmark_iterations: usize,
pub accuracy_tolerance: f64,
pub baseline_branch: Option<String>,
pub regression_threshold: RegressionThreshold,
pub alert_config: AlertConfig,
}
impl ReportConfig {
pub fn new() -> Self {
Self {
database_path: PathBuf::from("performance_history.db"),
output_directory: PathBuf::from("performance_reports"),
output_formats: vec![OutputFormat::Html, OutputFormat::Json],
algorithms: vec![
"linear_regression".to_string(),
"random_forest".to_string(),
"k_means".to_string(),
],
benchmark_sizes: vec![1000, 5000, 10000],
benchmark_iterations: 5,
accuracy_tolerance: 1e-6,
baseline_branch: None,
regression_threshold: RegressionThreshold::Percentage(5.0),
alert_config: AlertConfig::default(),
}
}
pub fn with_baseline_branch(mut self, branch: &str) -> Self {
self.baseline_branch = Some(branch.to_string());
self
}
pub fn with_regression_threshold(mut self, threshold: RegressionThreshold) -> Self {
self.regression_threshold = threshold;
self
}
pub fn with_alert_config(mut self, config: AlertConfig) -> Self {
self.alert_config = config;
self
}
}
impl Default for ReportConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum OutputFormat {
Html,
Json,
Csv,
Pdf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RegressionThreshold {
Absolute(f64),
Percentage(f64),
Statistical { confidence_level: f64 },
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AlertConfig {
pub enabled: bool,
pub email_notifications: bool,
pub email_recipients: Vec<String>,
pub slack_webhook: Option<String>,
pub custom_webhooks: Vec<String>,
}
impl AlertConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_email_notifications(mut self, enabled: bool) -> Self {
self.email_notifications = enabled;
self
}
pub fn with_slack_webhook(mut self, webhook: &str) -> Self {
self.slack_webhook = Some(webhook.to_string());
self
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PerformanceReport {
pub timestamp: DateTime<Utc>,
pub config: ReportConfig,
pub benchmark_results: BenchmarkResults,
pub analysis_results: Vec<AnalysisResult>,
pub summary: ReportSummary,
pub output_path: PathBuf,
}
impl PerformanceReport {
pub fn has_regressions(&self) -> bool {
self.summary.total_regressions > 0
}
pub fn output_path(&self) -> &Path {
&self.output_path
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ReportSummary {
pub overall_health: HealthStatus,
pub total_regressions: usize,
pub total_improvements: usize,
pub memory_efficiency: f64,
pub cpu_efficiency: f64,
pub scalability_score: f64,
pub performance_trend: TrendDirection,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub enum HealthStatus {
Good,
#[default]
Stable,
Poor,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub enum TrendDirection {
Improving,
#[default]
Stable,
Declining,
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct PerformanceDatabase {
path: PathBuf,
data: BTreeMap<DateTime<Utc>, BenchmarkResults>,
}
impl PerformanceDatabase {
pub fn new(path: &Path) -> Self {
Self {
path: path.to_path_buf(),
data: BTreeMap::new(),
}
}
pub fn store_results(&mut self, results: &BenchmarkResults) -> Result<()> {
let timestamp = Utc::now();
self.data.insert(timestamp, results.clone());
Ok(())
}
pub fn get_historical_data(&self, time_range: TimeRange) -> Vec<&BenchmarkResults> {
let cutoff = match time_range {
TimeRange::Days(days) => Utc::now() - chrono::Duration::days(days as i64),
TimeRange::Weeks(weeks) => Utc::now() - chrono::Duration::weeks(weeks as i64),
TimeRange::Months(months) => Utc::now() - chrono::Duration::days(months as i64 * 30),
};
self.data
.range(cutoff..)
.map(|(_, results)| results)
.collect()
}
}
#[derive(Debug, Clone, Copy)]
pub enum TimeRange {
Days(u32),
Weeks(u32),
Months(u32),
}
pub trait PerformanceAnalyzer: std::fmt::Debug {
fn analyze(
&self,
results: &BenchmarkResults,
database: &PerformanceDatabase,
) -> Result<AnalysisResult>;
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AnalysisResult {
pub analyzer_name: String,
pub analysis_type: AnalysisType,
pub description: String,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum AnalysisType {
Regression(RegressionAnalysis),
Trend(TrendAnalysis),
Resource(ResourceAnalysis),
Scalability(ScalabilityAnalysis),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RegressionAnalysis {
pub flagged_algorithms: Vec<AlgorithmRegression>,
pub improved_algorithms: Vec<AlgorithmRegression>,
pub stable_algorithms: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AlgorithmRegression {
pub name: String,
pub performance_change: f64, pub confidence_level: f64,
pub baseline_timing: Duration,
pub current_timing: Duration,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TrendAnalysis {
pub overall_trend: TrendDirection,
pub algorithm_trends: HashMap<String, TrendDirection>,
pub trend_strength: f64, }
#[derive(Debug, Serialize, Deserialize)]
pub struct ResourceAnalysis {
pub memory_efficiency_score: f64,
pub cpu_efficiency_score: f64,
pub memory_peak_usage: usize,
pub memory_leak_indicators: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ScalabilityAnalysis {
pub overall_score: f64,
pub scaling_coefficients: HashMap<String, f64>,
pub bottleneck_analysis: Vec<String>,
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct RegressionAnalyzer {
config: ReportConfig,
}
impl RegressionAnalyzer {
pub fn new(config: &ReportConfig) -> Self {
Self {
config: config.clone(),
}
}
}
impl PerformanceAnalyzer for RegressionAnalyzer {
fn analyze(
&self,
_results: &BenchmarkResults,
database: &PerformanceDatabase,
) -> Result<AnalysisResult> {
let _historical_data = database.get_historical_data(TimeRange::Days(30));
let flagged_algorithms = Vec::new();
let improved_algorithms = Vec::new();
let stable_algorithms = vec![
"linear_regression".to_string(),
"random_forest".to_string(),
"k_means".to_string(),
];
let analysis = RegressionAnalysis {
flagged_algorithms,
improved_algorithms,
stable_algorithms,
};
Ok(AnalysisResult {
analyzer_name: "Regression Analyzer".to_string(),
analysis_type: AnalysisType::Regression(analysis),
description:
"Statistical analysis of performance regressions compared to historical baselines"
.to_string(),
timestamp: Utc::now(),
})
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct TrendAnalyzer {
config: ReportConfig,
}
impl TrendAnalyzer {
pub fn new(config: &ReportConfig) -> Self {
Self {
config: config.clone(),
}
}
}
impl PerformanceAnalyzer for TrendAnalyzer {
fn analyze(
&self,
_results: &BenchmarkResults,
_database: &PerformanceDatabase,
) -> Result<AnalysisResult> {
let analysis = TrendAnalysis {
overall_trend: TrendDirection::Stable,
algorithm_trends: HashMap::new(),
trend_strength: 0.8,
};
Ok(AnalysisResult {
analyzer_name: "Trend Analyzer".to_string(),
analysis_type: AnalysisType::Trend(analysis),
description: "Analysis of performance trends over time".to_string(),
timestamp: Utc::now(),
})
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct ResourceAnalyzer {
config: ReportConfig,
}
impl ResourceAnalyzer {
pub fn new(config: &ReportConfig) -> Self {
Self {
config: config.clone(),
}
}
}
impl PerformanceAnalyzer for ResourceAnalyzer {
fn analyze(
&self,
_results: &BenchmarkResults,
_database: &PerformanceDatabase,
) -> Result<AnalysisResult> {
let analysis = ResourceAnalysis {
memory_efficiency_score: 0.85,
cpu_efficiency_score: 0.92,
memory_peak_usage: 1024 * 1024 * 128, memory_leak_indicators: Vec::new(),
};
Ok(AnalysisResult {
analyzer_name: "Resource Analyzer".to_string(),
analysis_type: AnalysisType::Resource(analysis),
description: "Analysis of memory and CPU resource utilization".to_string(),
timestamp: Utc::now(),
})
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct ScalabilityAnalyzer {
config: ReportConfig,
}
impl ScalabilityAnalyzer {
pub fn new(config: &ReportConfig) -> Self {
Self {
config: config.clone(),
}
}
}
impl PerformanceAnalyzer for ScalabilityAnalyzer {
fn analyze(
&self,
_results: &BenchmarkResults,
_database: &PerformanceDatabase,
) -> Result<AnalysisResult> {
let analysis = ScalabilityAnalysis {
overall_score: 0.88,
scaling_coefficients: HashMap::new(),
bottleneck_analysis: Vec::new(),
};
Ok(AnalysisResult {
analyzer_name: "Scalability Analyzer".to_string(),
analysis_type: AnalysisType::Scalability(analysis),
description: "Analysis of algorithm scalability characteristics".to_string(),
timestamp: Utc::now(),
})
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_report_config_creation() {
let config = ReportConfig::new()
.with_baseline_branch("main")
.with_regression_threshold(RegressionThreshold::Percentage(10.0));
assert_eq!(config.baseline_branch, Some("main".to_string()));
assert!(matches!(
config.regression_threshold,
RegressionThreshold::Percentage(10.0)
));
}
#[test]
fn test_alert_config() {
let config = AlertConfig::new()
.with_email_notifications(true)
.with_slack_webhook("https://hooks.slack.com/test");
assert!(config.email_notifications);
assert_eq!(
config.slack_webhook,
Some("https://hooks.slack.com/test".to_string())
);
}
#[test]
fn test_performance_database() {
let dir = tempdir().expect("failed to create temp directory");
let db_path = dir.path().join("test.db");
let mut database = PerformanceDatabase::new(&db_path);
let config = BenchmarkConfig::new();
let results = BenchmarkResults::new(config);
assert!(database.store_results(&results).is_ok());
let historical = database.get_historical_data(TimeRange::Days(1));
assert_eq!(historical.len(), 1);
}
#[test]
fn test_regression_analyzer() {
let config = ReportConfig::new();
let analyzer = RegressionAnalyzer::new(&config);
let database = PerformanceDatabase::new(&PathBuf::from("test.db"));
let benchmark_config = BenchmarkConfig::new();
let results = BenchmarkResults::new(benchmark_config);
let analysis = analyzer.analyze(&results, &database);
assert!(analysis.is_ok());
let analysis = analysis.expect("expected valid value");
assert_eq!(analysis.analyzer_name, "Regression Analyzer");
assert!(matches!(
analysis.analysis_type,
AnalysisType::Regression(_)
));
}
#[test]
fn test_performance_reporter_creation() {
let config = ReportConfig::new();
let reporter = PerformanceReporter::new(config);
assert_eq!(reporter.analyzers.len(), 4); }
#[test]
fn test_regression_threshold_types() {
let absolute = RegressionThreshold::Absolute(100.0);
let percentage = RegressionThreshold::Percentage(5.0);
let statistical = RegressionThreshold::Statistical {
confidence_level: 0.95,
};
assert!(matches!(absolute, RegressionThreshold::Absolute(100.0)));
assert!(matches!(percentage, RegressionThreshold::Percentage(5.0)));
assert!(matches!(
statistical,
RegressionThreshold::Statistical {
confidence_level: 0.95
}
));
}
#[test]
fn test_health_status() {
let good = HealthStatus::Good;
let stable = HealthStatus::Stable;
let poor = HealthStatus::Poor;
assert!(matches!(good, HealthStatus::Good));
assert!(matches!(stable, HealthStatus::Stable));
assert!(matches!(poor, HealthStatus::Poor));
}
#[test]
fn test_output_formats() {
let formats = [
OutputFormat::Html,
OutputFormat::Json,
OutputFormat::Csv,
OutputFormat::Pdf,
];
assert_eq!(formats.len(), 4);
assert!(formats.contains(&OutputFormat::Html));
assert!(formats.contains(&OutputFormat::Json));
}
}