shadow-benchmarks 0.1.0

Performance benchmarks for Shadow Network
Documentation
//! Benchmark report generation and comparison

use std::collections::HashMap;
use std::time::Duration;

/// A benchmark result entry
#[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 {
    /// Mean duration
    pub fn mean_duration(&self) -> Duration {
        Duration::from_nanos(self.mean_ns)
    }

    /// Operations per second
    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
    }
}

/// Collection of benchmark results with comparison capabilities
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BenchmarkReport {
    pub title: String,
    pub timestamp: String,
    pub results: Vec<BenchmarkResult>,
    pub system_info: SystemInfo,
}

/// Basic system info captured with benchmarks
#[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),
        }
    }
}

/// Comparison between two benchmark runs
#[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 {
    /// Create a new report
    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(),
        }
    }

    /// Add a benchmark result
    pub fn add(&mut self, result: BenchmarkResult) {
        self.results.push(result);
    }

    /// Get results by category
    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
    }

    /// Compare against a baseline report, returns regressions
    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, // >10% slower = regression
                })
            })
            .collect()
    }

    /// Generate text summary
    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")
    }

    /// Serialize to JSON
    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, // 20% regression
            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");
    }
}