use std::collections::HashMap;
use std::fmt;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct BenchmarkMetrics {
pub name: String,
pub duration: Duration,
pub samples_per_second: f64,
pub memory_usage_mb: f64,
pub n_samples: usize,
pub n_features: usize,
pub parallel_workers: Option<usize>,
pub chunk_size: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct BenchmarkReport {
pub total_benchmarks: usize,
pub metrics: Vec<BenchmarkMetrics>,
pub fastest_generator: Option<String>,
pub slowest_generator: Option<String>,
pub avg_samples_per_second: f64,
pub total_duration: Duration,
}
impl BenchmarkReport {
pub fn new() -> Self {
Self {
total_benchmarks: 0,
metrics: Vec::new(),
fastest_generator: None,
slowest_generator: None,
avg_samples_per_second: 0.0,
total_duration: Duration::new(0, 0),
}
}
pub fn add_metrics(&mut self, metrics: BenchmarkMetrics) {
self.total_benchmarks += 1;
self.total_duration += metrics.duration;
if self.fastest_generator.is_none() || metrics.samples_per_second > self.get_fastest_speed()
{
self.fastest_generator = Some(metrics.name.clone());
}
if self.slowest_generator.is_none() || metrics.samples_per_second < self.get_slowest_speed()
{
self.slowest_generator = Some(metrics.name.clone());
}
self.metrics.push(metrics);
self.update_averages();
}
fn get_fastest_speed(&self) -> f64 {
self.metrics
.iter()
.filter(|m| Some(&m.name) == self.fastest_generator.as_ref())
.map(|m| m.samples_per_second)
.next()
.unwrap_or(0.0)
}
fn get_slowest_speed(&self) -> f64 {
self.metrics
.iter()
.filter(|m| Some(&m.name) == self.slowest_generator.as_ref())
.map(|m| m.samples_per_second)
.next()
.unwrap_or(f64::INFINITY)
}
fn update_averages(&mut self) {
if self.total_benchmarks > 0 {
self.avg_samples_per_second = self
.metrics
.iter()
.map(|m| m.samples_per_second)
.sum::<f64>()
/ self.total_benchmarks as f64;
}
}
pub fn get_performance_ranking(&self) -> Vec<(&str, f64)> {
let mut ranking: Vec<(&str, f64)> = self
.metrics
.iter()
.map(|m| (m.name.as_str(), m.samples_per_second))
.collect();
ranking.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
ranking
}
pub fn get_scalability_analysis(&self) -> HashMap<String, Vec<(usize, f64)>> {
let mut scalability = HashMap::new();
for metric in &self.metrics {
let entry = scalability
.entry(metric.name.clone())
.or_insert_with(Vec::new);
entry.push((metric.n_samples, metric.samples_per_second));
}
for (_, samples) in scalability.iter_mut() {
samples.sort_by(|a, b| a.0.cmp(&b.0));
}
scalability
}
}
impl fmt::Display for BenchmarkReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "=== Performance Benchmark Report ===")?;
writeln!(f, "Total benchmarks: {}", self.total_benchmarks)?;
writeln!(f, "Total duration: {:.2?}", self.total_duration)?;
writeln!(f, "Average samples/sec: {:.2}", self.avg_samples_per_second)?;
writeln!(f, "")?;
if let Some(fastest) = &self.fastest_generator {
writeln!(f, "Fastest generator: {}", fastest)?;
}
if let Some(slowest) = &self.slowest_generator {
writeln!(f, "Slowest generator: {}", slowest)?;
}
writeln!(f, "")?;
writeln!(f, "Performance Rankings:")?;
for (i, (name, speed)) in self.get_performance_ranking().iter().enumerate() {
writeln!(f, "{}. {} - {:.2} samples/sec", i + 1, name, speed)?;
}
writeln!(f, "")?;
writeln!(f, "Detailed Metrics:")?;
for metric in &self.metrics {
writeln!(
f,
"{}: {:.2} samples/sec, {:.2?}, {:.2} MB",
metric.name, metric.samples_per_second, metric.duration, metric.memory_usage_mb
)?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct BenchmarkConfig {
pub sample_sizes: Vec<usize>,
pub feature_sizes: Vec<usize>,
pub num_runs: usize,
pub warmup_runs: usize,
pub measure_memory: bool,
pub parallel_workers: Vec<Option<usize>>,
pub chunk_sizes: Vec<Option<usize>>,
}
impl Default for BenchmarkConfig {
fn default() -> Self {
Self {
sample_sizes: vec![1000, 5000, 10000],
feature_sizes: vec![10, 50, 100],
num_runs: 3,
warmup_runs: 1,
measure_memory: true,
parallel_workers: vec![None, Some(2), Some(4)],
chunk_sizes: vec![None, Some(1000), Some(5000)],
}
}
}
fn estimate_memory_usage(n_samples: usize, n_features: usize) -> f64 {
(n_samples * n_features * 8) as f64 / 1024.0 / 1024.0
}
pub fn benchmark_generator<F>(
name: &str,
generator_fn: F,
n_samples: usize,
n_features: usize,
config: &BenchmarkConfig,
) -> BenchmarkMetrics
where
F: Fn(usize, usize) -> Result<Vec<Vec<f64>>, Box<dyn std::error::Error>>,
{
let mut durations = Vec::new();
for _ in 0..config.warmup_runs {
let _ = generator_fn(n_samples, n_features);
}
for _ in 0..config.num_runs {
let start = Instant::now();
let result = generator_fn(n_samples, n_features);
let duration = start.elapsed();
if result.is_ok() {
durations.push(duration);
}
}
let avg_duration = if durations.is_empty() {
Duration::new(0, 0)
} else {
durations.iter().sum::<Duration>() / durations.len() as u32
};
let samples_per_second = if avg_duration.as_secs_f64() > 0.0 {
n_samples as f64 / avg_duration.as_secs_f64()
} else {
0.0
};
let memory_usage = if config.measure_memory {
estimate_memory_usage(n_samples, n_features)
} else {
0.0
};
BenchmarkMetrics {
name: name.to_string(),
duration: avg_duration,
samples_per_second,
memory_usage_mb: memory_usage,
n_samples,
n_features,
parallel_workers: None,
chunk_size: None,
}
}
pub fn benchmark_parallel_generator<F>(
name: &str,
generator_fn: F,
n_samples: usize,
n_features: usize,
workers: usize,
config: &BenchmarkConfig,
) -> BenchmarkMetrics
where
F: Fn(usize, usize, usize) -> Result<Vec<Vec<f64>>, Box<dyn std::error::Error>>,
{
let mut durations = Vec::new();
for _ in 0..config.warmup_runs {
let _ = generator_fn(n_samples, n_features, workers);
}
for _ in 0..config.num_runs {
let start = Instant::now();
let result = generator_fn(n_samples, n_features, workers);
let duration = start.elapsed();
if result.is_ok() {
durations.push(duration);
}
}
let avg_duration = if durations.is_empty() {
Duration::new(0, 0)
} else {
durations.iter().sum::<Duration>() / durations.len() as u32
};
let samples_per_second = if avg_duration.as_secs_f64() > 0.0 {
n_samples as f64 / avg_duration.as_secs_f64()
} else {
0.0
};
let memory_usage = if config.measure_memory {
estimate_memory_usage(n_samples, n_features)
} else {
0.0
};
BenchmarkMetrics {
name: format!("{}_parallel_{}", name, workers),
duration: avg_duration,
samples_per_second,
memory_usage_mb: memory_usage,
n_samples,
n_features,
parallel_workers: Some(workers),
chunk_size: None,
}
}
pub fn run_comprehensive_benchmarks() -> BenchmarkReport {
let config = BenchmarkConfig::default();
let mut report = BenchmarkReport::new();
use crate::generators_legacy::{make_blobs, make_classification, make_regression};
for &n_samples in &config.sample_sizes {
for &n_features in &config.feature_sizes {
let classification_metrics = benchmark_generator(
"make_classification",
|n_samples, n_features| {
make_classification(n_samples, n_features, n_features / 2, 0, 2, None)
.map(|(x, _y)| {
(0..x.nrows())
.map(|i| (0..x.ncols()).map(|j| x[[i, j]]).collect())
.collect()
})
.map_err(|e| e.into())
},
n_samples,
n_features,
&config,
);
report.add_metrics(classification_metrics);
let regression_metrics = benchmark_generator(
"make_regression",
|n_samples, n_features| {
make_regression(n_samples, n_features, n_features / 2, 0.1, None)
.map(|(x, _y)| {
(0..x.nrows())
.map(|i| (0..x.ncols()).map(|j| x[[i, j]]).collect())
.collect()
})
.map_err(|e| e.into())
},
n_samples,
n_features,
&config,
);
report.add_metrics(regression_metrics);
let blobs_metrics = benchmark_generator(
"make_blobs",
|n_samples, n_features| {
make_blobs(n_samples, n_features, 3, 1.0, None)
.map(|(x, _y)| {
(0..x.nrows())
.map(|i| (0..x.ncols()).map(|j| x[[i, j]]).collect())
.collect()
})
.map_err(|e| e.into())
},
n_samples,
n_features,
&config,
);
report.add_metrics(blobs_metrics);
}
}
report
}
pub fn benchmark_memory_scalability() -> HashMap<String, Vec<(usize, f64)>> {
let mut scalability = HashMap::new();
let sample_sizes = vec![1000, 5000, 10000, 50000, 100000];
let n_features = 10;
for &n_samples in &sample_sizes {
let estimated_memory = estimate_memory_usage(n_samples, n_features);
scalability
.entry("memory_usage".to_string())
.or_insert_with(Vec::new)
.push((n_samples, estimated_memory));
}
scalability
}
pub fn benchmark_streaming_performance() -> BenchmarkReport {
let mut report = BenchmarkReport::new();
let chunk_sizes = vec![1000, 5000, 10000];
let total_samples = 50000;
let n_features = 10;
for &chunk_size in &chunk_sizes {
let start = Instant::now();
let mut total_generated = 0;
while total_generated < total_samples {
let current_chunk = std::cmp::min(chunk_size, total_samples - total_generated);
let _chunk: Vec<Vec<f64>> = (0..current_chunk)
.map(|_| (0..n_features).map(|_| scirs2_core::random::thread_rng().random_range(0.0..1.0)).collect())
.collect();
total_generated += current_chunk;
}
let duration = start.elapsed();
let samples_per_second = total_samples as f64 / duration.as_secs_f64();
let metrics = BenchmarkMetrics {
name: format!("streaming_chunk_{}", chunk_size),
duration,
samples_per_second,
memory_usage_mb: estimate_memory_usage(chunk_size, n_features),
n_samples: total_samples,
n_features,
parallel_workers: None,
chunk_size: Some(chunk_size),
};
report.add_metrics(metrics);
}
report
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_benchmark_metrics() {
let metrics = BenchmarkMetrics {
name: "test_generator".to_string(),
duration: Duration::from_millis(100),
samples_per_second: 10000.0,
memory_usage_mb: 1.5,
n_samples: 1000,
n_features: 10,
parallel_workers: None,
chunk_size: None,
};
assert_eq!(metrics.name, "test_generator");
assert_eq!(metrics.samples_per_second, 10000.0);
}
#[test]
fn test_benchmark_report() {
let mut report = BenchmarkReport::new();
let metrics1 = BenchmarkMetrics {
name: "fast_generator".to_string(),
duration: Duration::from_millis(50),
samples_per_second: 20000.0,
memory_usage_mb: 1.0,
n_samples: 1000,
n_features: 10,
parallel_workers: None,
chunk_size: None,
};
let metrics2 = BenchmarkMetrics {
name: "slow_generator".to_string(),
duration: Duration::from_millis(200),
samples_per_second: 5000.0,
memory_usage_mb: 2.0,
n_samples: 1000,
n_features: 10,
parallel_workers: None,
chunk_size: None,
};
report.add_metrics(metrics1);
report.add_metrics(metrics2);
assert_eq!(report.total_benchmarks, 2);
assert_eq!(report.fastest_generator, Some("fast_generator".to_string()));
assert_eq!(report.slowest_generator, Some("slow_generator".to_string()));
}
#[test]
fn test_memory_estimation() {
let memory_mb = estimate_memory_usage(1000, 10);
assert!(memory_mb > 0.0);
let memory_mb_2x = estimate_memory_usage(2000, 10);
assert!(memory_mb_2x > memory_mb);
}
#[test]
fn test_benchmark_simple_generator() {
let config = BenchmarkConfig {
num_runs: 1,
warmup_runs: 0,
..BenchmarkConfig::default()
};
let metrics = benchmark_generator(
"simple_generator",
|n_samples, n_features| {
Ok((0..n_samples)
.map(|_| (0..n_features).map(|_| 1.0).collect())
.collect())
},
100,
5,
&config,
);
assert_eq!(metrics.name, "simple_generator");
assert_eq!(metrics.n_samples, 100);
assert_eq!(metrics.n_features, 5);
assert!(metrics.samples_per_second > 0.0);
}
#[test]
fn test_performance_ranking() {
let mut report = BenchmarkReport::new();
let metrics1 = BenchmarkMetrics {
name: "fast".to_string(),
duration: Duration::from_millis(10),
samples_per_second: 100000.0,
memory_usage_mb: 1.0,
n_samples: 1000,
n_features: 10,
parallel_workers: None,
chunk_size: None,
};
let metrics2 = BenchmarkMetrics {
name: "slow".to_string(),
duration: Duration::from_millis(100),
samples_per_second: 10000.0,
memory_usage_mb: 1.0,
n_samples: 1000,
n_features: 10,
parallel_workers: None,
chunk_size: None,
};
report.add_metrics(metrics1);
report.add_metrics(metrics2);
let ranking = report.get_performance_ranking();
assert_eq!(ranking[0].0, "fast");
assert_eq!(ranking[1].0, "slow");
}
}