use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BenchmarkResult {
pub name: String,
pub category: String,
pub input_size: usize,
pub iterations: usize,
pub mean_ns: u64,
pub stddev_ns: u64,
pub min_ns: u64,
pub max_ns: u64,
pub throughput_mbps: Option<f64>,
}
impl BenchmarkResult {
pub fn mean_duration(&self) -> Duration {
Duration::from_nanos(self.mean_ns)
}
pub fn ops_per_sec(&self) -> f64 {
if self.mean_ns == 0 {
return 0.0;
}
1_000_000_000.0 / self.mean_ns as f64
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BenchmarkReport {
pub title: String,
pub timestamp: String,
pub results: Vec<BenchmarkResult>,
pub system_info: SystemInfo,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SystemInfo {
pub os: String,
pub arch: String,
pub num_cpus: usize,
}
impl Default for SystemInfo {
fn default() -> Self {
Self {
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
num_cpus: std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1),
}
}
}
#[derive(Debug, Clone)]
pub struct BenchmarkComparison {
pub name: String,
pub baseline_ns: u64,
pub current_ns: u64,
pub change_pct: f64,
pub regression: bool,
}
impl BenchmarkReport {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
timestamp: chrono::Utc::now().to_rfc3339(),
results: Vec::new(),
system_info: SystemInfo::default(),
}
}
pub fn add(&mut self, result: BenchmarkResult) {
self.results.push(result);
}
pub fn by_category(&self) -> HashMap<&str, Vec<&BenchmarkResult>> {
let mut map: HashMap<&str, Vec<&BenchmarkResult>> = HashMap::new();
for result in &self.results {
map.entry(&result.category).or_default().push(result);
}
map
}
pub fn compare(&self, baseline: &BenchmarkReport) -> Vec<BenchmarkComparison> {
let baseline_map: HashMap<&str, &BenchmarkResult> = baseline
.results
.iter()
.map(|r| (r.name.as_str(), r))
.collect();
self.results
.iter()
.filter_map(|current| {
let base = baseline_map.get(current.name.as_str())?;
let change_pct = if base.mean_ns == 0 {
0.0
} else {
((current.mean_ns as f64 - base.mean_ns as f64) / base.mean_ns as f64) * 100.0
};
Some(BenchmarkComparison {
name: current.name.clone(),
baseline_ns: base.mean_ns,
current_ns: current.mean_ns,
change_pct,
regression: change_pct > 10.0, })
})
.collect()
}
pub fn summary(&self) -> String {
let mut lines = vec![
format!("╔══ {} ══╗", self.title),
format!(
" System: {} {} ({} CPUs)",
self.system_info.os, self.system_info.arch, self.system_info.num_cpus
),
format!(" Time: {}", self.timestamp),
format!(" Benchmarks: {}", self.results.len()),
String::new(),
];
for (category, results) in self.by_category() {
lines.push(format!(" ── {} ──", category));
for r in results {
let tp = r
.throughput_mbps
.map(|t| format!(" | {:.1} MB/s", t))
.unwrap_or_default();
lines.push(format!(
" {:<35} {:>10.2?} mean | {:.0} ops/s{}",
r.name,
r.mean_duration(),
r.ops_per_sec(),
tp
));
}
lines.push(String::new());
}
lines.push("╚══════════════════════════════════╝".to_string());
lines.join("\n")
}
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_report_creation() {
let mut report = BenchmarkReport::new("Test Suite");
report.add(BenchmarkResult {
name: "encrypt_256b".into(),
category: "crypto".into(),
input_size: 256,
iterations: 1000,
mean_ns: 5000,
stddev_ns: 200,
min_ns: 4500,
max_ns: 6000,
throughput_mbps: Some(48.8),
});
assert_eq!(report.results.len(), 1);
assert!(report.summary().contains("encrypt_256b"));
}
#[test]
fn test_comparison() {
let mut baseline = BenchmarkReport::new("baseline");
baseline.add(BenchmarkResult {
name: "op1".into(),
category: "test".into(),
input_size: 0,
iterations: 100,
mean_ns: 1000,
stddev_ns: 50,
min_ns: 900,
max_ns: 1100,
throughput_mbps: None,
});
let mut current = BenchmarkReport::new("current");
current.add(BenchmarkResult {
name: "op1".into(),
category: "test".into(),
input_size: 0,
iterations: 100,
mean_ns: 1200, stddev_ns: 60,
min_ns: 1000,
max_ns: 1400,
throughput_mbps: None,
});
let comparisons = current.compare(&baseline);
assert_eq!(comparisons.len(), 1);
assert!(comparisons[0].regression);
assert!((comparisons[0].change_pct - 20.0).abs() < 0.1);
}
#[test]
fn test_by_category() {
let mut report = BenchmarkReport::new("test");
report.add(BenchmarkResult {
name: "a".into(),
category: "crypto".into(),
input_size: 0,
iterations: 1,
mean_ns: 100,
stddev_ns: 0,
min_ns: 100,
max_ns: 100,
throughput_mbps: None,
});
report.add(BenchmarkResult {
name: "b".into(),
category: "dht".into(),
input_size: 0,
iterations: 1,
mean_ns: 200,
stddev_ns: 0,
min_ns: 200,
max_ns: 200,
throughput_mbps: None,
});
let cats = report.by_category();
assert_eq!(cats.len(), 2);
assert!(cats.contains_key("crypto"));
assert!(cats.contains_key("dht"));
}
#[test]
fn test_json_roundtrip() {
let mut report = BenchmarkReport::new("json_test");
report.add(BenchmarkResult {
name: "op".into(),
category: "test".into(),
input_size: 64,
iterations: 50,
mean_ns: 5000,
stddev_ns: 100,
min_ns: 4800,
max_ns: 5500,
throughput_mbps: Some(12.2),
});
let json = report.to_json();
let parsed: BenchmarkReport = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.results.len(), 1);
assert_eq!(parsed.results[0].name, "op");
}
}