use scirs2_core::ndarray::Array2;
use sklears_core::error::SklearsError;
use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub type PerformanceResult<T> = Result<T, SklearsError>;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MemoryStats {
pub peak_usage_bytes: u64,
pub current_usage_bytes: u64,
pub allocated_bytes: u64,
pub deallocated_bytes: u64,
pub allocation_count: u64,
pub deallocation_count: u64,
}
impl MemoryStats {
pub fn new() -> Self {
Self {
peak_usage_bytes: 0,
current_usage_bytes: 0,
allocated_bytes: 0,
deallocated_bytes: 0,
allocation_count: 0,
deallocation_count: 0,
}
}
pub fn efficiency_ratio(&self) -> f64 {
if self.allocated_bytes > 0 {
self.deallocated_bytes as f64 / self.allocated_bytes as f64
} else {
1.0
}
}
pub fn avg_allocation_size(&self) -> f64 {
if self.allocation_count > 0 {
self.allocated_bytes as f64 / self.allocation_count as f64
} else {
0.0
}
}
}
impl Default for MemoryStats {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PerformanceMetrics {
pub execution_time: Duration,
pub memory_stats: MemoryStats,
pub ops_per_second: f64,
pub samples_per_second: f64,
pub cpu_usage_percent: f64,
pub gpu_usage_percent: Option<f64>,
pub custom_metrics: HashMap<String, f64>,
}
impl PerformanceMetrics {
pub fn new(execution_time: Duration, memory_stats: MemoryStats) -> Self {
Self {
execution_time,
memory_stats,
ops_per_second: 0.0,
samples_per_second: 0.0,
cpu_usage_percent: 0.0,
gpu_usage_percent: None,
custom_metrics: HashMap::new(),
}
}
pub fn with_ops_per_second(mut self, ops_per_second: f64) -> Self {
self.ops_per_second = ops_per_second;
self
}
pub fn with_samples_per_second(mut self, samples_per_second: f64) -> Self {
self.samples_per_second = samples_per_second;
self
}
pub fn with_cpu_usage(mut self, cpu_usage_percent: f64) -> Self {
self.cpu_usage_percent = cpu_usage_percent;
self
}
pub fn with_gpu_usage(mut self, gpu_usage_percent: f64) -> Self {
self.gpu_usage_percent = Some(gpu_usage_percent);
self
}
pub fn add_custom_metric(&mut self, name: String, value: f64) {
self.custom_metrics.insert(name, value);
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BenchmarkComparison {
pub benchmark_name: String,
pub baseline: PerformanceMetrics,
pub current: PerformanceMetrics,
pub performance_ratio: f64,
pub memory_ratio: f64,
pub regression_detected: bool,
pub improvement_percent: f64,
}
impl BenchmarkComparison {
pub fn new(
benchmark_name: String,
baseline: PerformanceMetrics,
current: PerformanceMetrics,
) -> Self {
let baseline_time = baseline.execution_time.as_secs_f64();
let current_time = current.execution_time.as_secs_f64();
let performance_ratio = if baseline_time > 0.0 {
current_time / baseline_time
} else {
1.0
};
let memory_ratio = if baseline.memory_stats.peak_usage_bytes > 0 {
current.memory_stats.peak_usage_bytes as f64
/ baseline.memory_stats.peak_usage_bytes as f64
} else {
1.0
};
let improvement_percent = (1.0 - performance_ratio) * 100.0;
let regression_detected = performance_ratio > 1.1;
Self {
benchmark_name,
baseline,
current,
performance_ratio,
memory_ratio,
regression_detected,
improvement_percent,
}
}
pub fn has_improvement(&self) -> bool {
self.improvement_percent > 5.0
}
pub fn has_memory_regression(&self) -> bool {
self.memory_ratio > 1.2
}
}
pub struct PerformanceProfiler {
start_time: Option<Instant>,
memory_tracker: Arc<Mutex<MemoryStats>>,
operation_count: u64,
sample_count: u64,
}
impl PerformanceProfiler {
pub fn new() -> Self {
Self {
start_time: None,
memory_tracker: Arc::new(Mutex::new(MemoryStats::new())),
operation_count: 0,
sample_count: 0,
}
}
pub fn start(&mut self) {
self.start_time = Some(Instant::now());
}
pub fn stop(&self) -> PerformanceResult<PerformanceMetrics> {
let start_time = self
.start_time
.ok_or_else(|| SklearsError::InvalidParameter {
name: "profiler_state".to_string(),
reason: "Profiler was not started".to_string(),
})?;
let execution_time = start_time.elapsed();
let memory_stats = self
.memory_tracker
.lock()
.map_err(|_| SklearsError::InvalidParameter {
name: "memory_tracker".to_string(),
reason: "Failed to acquire memory tracker lock".to_string(),
})?
.clone();
let ops_per_second = if execution_time.as_secs_f64() > 0.0 {
self.operation_count as f64 / execution_time.as_secs_f64()
} else {
0.0
};
let samples_per_second = if execution_time.as_secs_f64() > 0.0 {
self.sample_count as f64 / execution_time.as_secs_f64()
} else {
0.0
};
Ok(PerformanceMetrics::new(execution_time, memory_stats)
.with_ops_per_second(ops_per_second)
.with_samples_per_second(samples_per_second))
}
pub fn record_operation(&mut self) {
self.operation_count += 1;
}
pub fn record_operations(&mut self, count: u64) {
self.operation_count += count;
}
pub fn record_samples(&mut self, count: u64) {
self.sample_count += count;
}
pub fn record_allocation(&self, bytes: u64) {
if let Ok(mut stats) = self.memory_tracker.lock() {
stats.allocated_bytes += bytes;
stats.allocation_count += 1;
stats.current_usage_bytes += bytes;
if stats.current_usage_bytes > stats.peak_usage_bytes {
stats.peak_usage_bytes = stats.current_usage_bytes;
}
}
}
pub fn record_deallocation(&self, bytes: u64) {
if let Ok(mut stats) = self.memory_tracker.lock() {
stats.deallocated_bytes += bytes;
stats.deallocation_count += 1;
stats.current_usage_bytes = stats.current_usage_bytes.saturating_sub(bytes);
}
}
}
impl Default for PerformanceProfiler {
fn default() -> Self {
Self::new()
}
}
pub struct BenchmarkSuite {
benchmarks: HashMap<String, Box<dyn Fn() -> PerformanceResult<PerformanceMetrics>>>,
baselines: HashMap<String, PerformanceMetrics>,
results: HashMap<String, BenchmarkComparison>,
}
impl BenchmarkSuite {
pub fn new() -> Self {
Self {
benchmarks: HashMap::new(),
baselines: HashMap::new(),
results: HashMap::new(),
}
}
pub fn add_benchmark<F>(&mut self, name: String, benchmark_fn: F)
where
F: Fn() -> PerformanceResult<PerformanceMetrics> + 'static,
{
self.benchmarks.insert(name, Box::new(benchmark_fn));
}
pub fn set_baseline(&mut self, name: String, baseline: PerformanceMetrics) {
self.baselines.insert(name, baseline);
}
pub fn run_all(&mut self) -> PerformanceResult<()> {
for (name, benchmark_fn) in &self.benchmarks {
let current_metrics = benchmark_fn()?;
if let Some(baseline) = self.baselines.get(name) {
let comparison =
BenchmarkComparison::new(name.clone(), baseline.clone(), current_metrics);
self.results.insert(name.clone(), comparison);
}
}
Ok(())
}
pub fn run_benchmark(&mut self, name: &str) -> PerformanceResult<()> {
if let Some(benchmark_fn) = self.benchmarks.get(name) {
let current_metrics = benchmark_fn()?;
if let Some(baseline) = self.baselines.get(name) {
let comparison =
BenchmarkComparison::new(name.to_string(), baseline.clone(), current_metrics);
self.results.insert(name.to_string(), comparison);
}
} else {
return Err(SklearsError::InvalidParameter {
name: "benchmark_name".to_string(),
reason: format!("Benchmark '{}' not found", name),
});
}
Ok(())
}
pub fn get_results(&self) -> &HashMap<String, BenchmarkComparison> {
&self.results
}
pub fn has_regressions(&self) -> bool {
self.results.values().any(|r| r.regression_detected)
}
pub fn get_regressions(&self) -> Vec<&BenchmarkComparison> {
self.results
.values()
.filter(|r| r.regression_detected)
.collect()
}
pub fn generate_report(&self) -> PerformanceReport {
let total_benchmarks = self.results.len();
let regressions = self.get_regressions();
let regression_count = regressions.len();
let improvements: Vec<&BenchmarkComparison> = self
.results
.values()
.filter(|r| r.has_improvement())
.collect();
let improvement_count = improvements.len();
let memory_regressions: Vec<&BenchmarkComparison> = self
.results
.values()
.filter(|r| r.has_memory_regression())
.collect();
let memory_regression_count = memory_regressions.len();
PerformanceReport {
total_benchmarks,
regression_count,
improvement_count,
memory_regression_count,
overall_status: if regression_count == 0 {
PerformanceStatus::Pass
} else {
PerformanceStatus::Fail
},
regressions: regressions.into_iter().cloned().collect(),
improvements: improvements.into_iter().cloned().collect(),
memory_regressions: memory_regressions.into_iter().cloned().collect(),
}
}
}
impl Default for BenchmarkSuite {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum PerformanceStatus {
Pass,
Fail,
Warning,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PerformanceReport {
pub total_benchmarks: usize,
pub regression_count: usize,
pub improvement_count: usize,
pub memory_regression_count: usize,
pub overall_status: PerformanceStatus,
pub regressions: Vec<BenchmarkComparison>,
pub improvements: Vec<BenchmarkComparison>,
pub memory_regressions: Vec<BenchmarkComparison>,
}
impl fmt::Display for PerformanceReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "=== Performance Test Report ===")?;
writeln!(f, "Total Benchmarks: {}", self.total_benchmarks)?;
writeln!(f, "Overall Status: {:?}", self.overall_status)?;
writeln!(f)?;
writeln!(f, "Improvements: {} benchmarks", self.improvement_count)?;
for improvement in &self.improvements {
writeln!(
f,
" {}: {:.1}% faster",
improvement.benchmark_name, improvement.improvement_percent
)?;
}
writeln!(f)?;
writeln!(
f,
"Performance Regressions: {} benchmarks",
self.regression_count
)?;
for regression in &self.regressions {
writeln!(
f,
" {} {:.1}% slower (ratio: {:.2})",
regression.benchmark_name,
-regression.improvement_percent,
regression.performance_ratio
)?;
}
writeln!(f)?;
writeln!(
f,
"Memory Regressions: {} benchmarks",
self.memory_regression_count
)?;
for mem_regression in &self.memory_regressions {
writeln!(
f,
" {}: {:.1}% more memory (ratio: {:.2})",
mem_regression.benchmark_name,
(mem_regression.memory_ratio - 1.0) * 100.0,
mem_regression.memory_ratio
)?;
}
Ok(())
}
}
#[allow(dead_code)] pub struct MemoryLeakDetector {
initial_memory: u64,
allocations: HashMap<usize, u64>,
allocation_tracker: Arc<Mutex<u64>>,
}
impl MemoryLeakDetector {
pub fn new() -> Self {
Self {
initial_memory: Self::get_current_memory_usage(),
allocations: HashMap::new(),
allocation_tracker: Arc::new(Mutex::new(0)),
}
}
pub fn start_monitoring(&mut self) {
self.initial_memory = Self::get_current_memory_usage();
self.allocations.clear();
}
pub fn check_for_leaks(&self) -> MemoryLeakReport {
let current_memory = Self::get_current_memory_usage();
let memory_increase = current_memory.saturating_sub(self.initial_memory);
let leak_threshold = 1024 * 1024; let has_leak = memory_increase > leak_threshold;
let active_allocations = self.allocations.len();
let total_allocated = self.allocations.values().sum::<u64>();
MemoryLeakReport {
initial_memory: self.initial_memory,
current_memory,
memory_increase,
has_leak,
leak_threshold,
active_allocations,
total_allocated,
}
}
pub fn track_allocation(&mut self, ptr: usize, size: u64) {
self.allocations.insert(ptr, size);
}
pub fn track_deallocation(&mut self, ptr: usize) {
self.allocations.remove(&ptr);
}
fn get_current_memory_usage() -> u64 {
std::process::id() as u64 * 1024 }
}
impl Default for MemoryLeakDetector {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MemoryLeakReport {
pub initial_memory: u64,
pub current_memory: u64,
pub memory_increase: u64,
pub has_leak: bool,
pub leak_threshold: u64,
pub active_allocations: usize,
pub total_allocated: u64,
}
impl fmt::Display for MemoryLeakReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "=== Memory Leak Detection Report ===")?;
writeln!(f, "Initial Memory: {} bytes", self.initial_memory)?;
writeln!(f, "Current Memory: {} bytes", self.current_memory)?;
writeln!(f, "Memory Increase: {} bytes", self.memory_increase)?;
writeln!(f, "Leak Detected: {}", self.has_leak)?;
writeln!(f, "Active Allocations: {}", self.active_allocations)?;
writeln!(f, "Total Allocated: {} bytes", self.total_allocated)?;
if self.has_leak {
writeln!(
f,
"⚠️ Memory leak detected! Increase exceeds threshold of {} bytes",
self.leak_threshold
)?;
} else {
writeln!(f, "✅ No memory leaks detected")?;
}
Ok(())
}
}
pub mod utils {
use super::*;
pub fn benchmark_function<F, T>(f: F, iterations: u32) -> PerformanceResult<PerformanceMetrics>
where
F: Fn() -> T,
{
let mut profiler = PerformanceProfiler::new();
profiler.start();
for _ in 0..iterations {
let _ = f();
profiler.record_operation();
}
profiler.stop()
}
pub fn benchmark_matrix_multiply(
a: &Array2<f64>,
b: &Array2<f64>,
iterations: u32,
) -> PerformanceResult<PerformanceMetrics> {
benchmark_function(|| a.dot(b), iterations)
}
pub fn benchmark_forward_pass<F>(
forward_fn: F,
input: &Array2<f64>,
iterations: u32,
) -> PerformanceResult<PerformanceMetrics>
where
F: Fn(&Array2<f64>) -> Array2<f64>,
{
let mut profiler = PerformanceProfiler::new();
profiler.start();
for _ in 0..iterations {
let _ = forward_fn(input);
profiler.record_operation();
profiler.record_samples(input.nrows() as u64);
}
profiler.stop()
}
pub fn create_standard_benchmark_suite() -> BenchmarkSuite {
let mut suite = BenchmarkSuite::new();
suite.add_benchmark("matrix_multiply_100x100".to_string(), || {
let a = Array2::ones((100, 100));
let b = Array2::ones((100, 100));
benchmark_matrix_multiply(&a, &b, 1000)
});
suite.add_benchmark("matrix_multiply_1000x1000".to_string(), || {
let a = Array2::ones((1000, 1000));
let b = Array2::ones((1000, 1000));
benchmark_matrix_multiply(&a, &b, 10)
});
suite
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use approx;
#[test]
fn test_memory_stats() {
let mut stats = MemoryStats::new();
stats.allocated_bytes = 1000;
stats.deallocated_bytes = 800;
stats.allocation_count = 10;
assert_eq!(stats.efficiency_ratio(), 0.8);
assert_eq!(stats.avg_allocation_size(), 100.0);
}
#[test]
fn test_performance_profiler() {
let mut profiler = PerformanceProfiler::new();
profiler.start();
std::thread::sleep(Duration::from_millis(10));
profiler.record_operation();
profiler.record_samples(100);
let metrics = profiler.stop().expect("operation should succeed");
assert!(metrics.execution_time >= Duration::from_millis(10));
assert!(metrics.ops_per_second > 0.0);
assert!(metrics.samples_per_second > 0.0);
}
#[test]
fn test_benchmark_comparison() {
let baseline = PerformanceMetrics::new(Duration::from_millis(100), MemoryStats::new());
let improved = PerformanceMetrics::new(Duration::from_millis(80), MemoryStats::new());
let comparison = BenchmarkComparison::new("test_benchmark".to_string(), baseline, improved);
assert!(comparison.has_improvement());
assert!(!comparison.regression_detected);
approx::assert_abs_diff_eq!(comparison.improvement_percent, 20.0, epsilon = 1e-10);
}
#[test]
fn test_memory_leak_detector() {
let mut detector = MemoryLeakDetector::new();
detector.start_monitoring();
detector.track_allocation(0x1000, 1024);
detector.track_allocation(0x2000, 2048);
let report = detector.check_for_leaks();
assert_eq!(report.active_allocations, 2);
assert_eq!(report.total_allocated, 3072);
detector.track_deallocation(0x1000);
let report = detector.check_for_leaks();
assert_eq!(report.active_allocations, 1);
assert_eq!(report.total_allocated, 2048);
}
#[test]
fn test_benchmark_suite() {
let mut suite = BenchmarkSuite::new();
suite.add_benchmark("simple_add".to_string(), || {
let mut profiler = PerformanceProfiler::new();
profiler.start();
let _ = 1 + 1;
profiler.record_operation();
profiler.stop()
});
let baseline = PerformanceMetrics::new(Duration::from_millis(1), MemoryStats::new());
suite.set_baseline("simple_add".to_string(), baseline);
suite
.run_benchmark("simple_add")
.expect("operation should succeed");
let results = suite.get_results();
assert!(results.contains_key("simple_add"));
}
#[test]
fn test_benchmark_utilities() {
let result = utils::benchmark_function(
|| {
let a = Array2::<f64>::ones((10, 10));
let b = Array2::<f64>::ones((10, 10));
a.dot(&b)
},
100,
);
assert!(result.is_ok());
let metrics = result.expect("operation should succeed");
assert!(metrics.ops_per_second > 0.0);
}
}