shadow-benchmarks 0.1.0

Performance benchmarks for Shadow Network
Documentation
//! Simple profiler for tracking operation timings

use std::collections::HashMap;
use std::time::{Duration, Instant};

/// A lightweight profiler that tracks operation timings
#[derive(Debug, Clone)]
pub struct Profiler {
    name: String,
    timings: HashMap<String, Vec<Duration>>,
}

/// Summary statistics for a profiled operation
#[derive(Debug, Clone)]
pub struct ProfileStats {
    pub operation: String,
    pub count: usize,
    pub min: Duration,
    pub max: Duration,
    pub mean: Duration,
    pub median: Duration,
    pub p95: Duration,
    pub p99: Duration,
    pub total: Duration,
}

impl ProfileStats {
    /// Operations per second
    pub fn ops_per_sec(&self) -> f64 {
        if self.mean.as_nanos() == 0 {
            return 0.0;
        }
        1_000_000_000.0 / self.mean.as_nanos() as f64
    }

    /// Format as a human-readable report line
    pub fn summary_line(&self) -> String {
        format!(
            "{:<30} {:>8} samples | mean {:>12.2?} | p95 {:>12.2?} | p99 {:>12.2?} | {:.0} ops/s",
            self.operation,
            self.count,
            self.mean,
            self.p95,
            self.p99,
            self.ops_per_sec()
        )
    }
}

impl Profiler {
    /// Create a new profiler
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            timings: HashMap::new(),
        }
    }

    /// Record a single timed operation
    pub fn record(&mut self, operation: &str, duration: Duration) {
        self.timings
            .entry(operation.to_string())
            .or_default()
            .push(duration);
    }

    /// Time a closure and record the result
    pub fn time<F, R>(&mut self, operation: &str, f: F) -> R
    where
        F: FnOnce() -> R,
    {
        let start = Instant::now();
        let result = f();
        self.record(operation, start.elapsed());
        result
    }

    /// Time a closure N times and record all results
    pub fn time_n<F>(&mut self, operation: &str, iterations: usize, mut f: F)
    where
        F: FnMut(),
    {
        for _ in 0..iterations {
            let start = Instant::now();
            f();
            self.record(operation, start.elapsed());
        }
    }

    /// Get statistics for a specific operation
    pub fn stats(&self, operation: &str) -> Option<ProfileStats> {
        let timings = self.timings.get(operation)?;
        if timings.is_empty() {
            return None;
        }

        let mut sorted: Vec<Duration> = timings.clone();
        sorted.sort();

        let count = sorted.len();
        let total: Duration = sorted.iter().sum();
        let mean = total / count as u32;

        let percentile = |p: f64| -> Duration {
            let idx = ((count as f64 * p) as usize).min(count - 1);
            sorted[idx]
        };

        Some(ProfileStats {
            operation: operation.to_string(),
            count,
            min: sorted[0],
            max: sorted[count - 1],
            mean,
            median: percentile(0.5),
            p95: percentile(0.95),
            p99: percentile(0.99),
            total,
        })
    }

    /// Get statistics for all operations
    pub fn all_stats(&self) -> Vec<ProfileStats> {
        let mut stats: Vec<ProfileStats> = self
            .timings
            .keys()
            .filter_map(|op| self.stats(op))
            .collect();
        stats.sort_by_key(|s| s.operation.clone());
        stats
    }

    /// Print a full report
    pub fn report(&self) -> String {
        let mut lines = vec![format!("╔══ {} Profiler Report ══╗", self.name)];
        for stat in self.all_stats() {
            lines.push(stat.summary_line());
        }
        lines.push(format!("╚══ {} operations profiled ══╝", self.timings.len()));
        lines.join("\n")
    }

    /// Reset all timings
    pub fn reset(&mut self) {
        self.timings.clear();
    }

    /// Name of this profiler
    pub fn name(&self) -> &str {
        &self.name
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_profiler_basic() {
        let mut profiler = Profiler::new("test");
        profiler.record("op1", Duration::from_millis(10));
        profiler.record("op1", Duration::from_millis(20));
        profiler.record("op1", Duration::from_millis(30));

        let stats = profiler.stats("op1").unwrap();
        assert_eq!(stats.count, 3);
        assert_eq!(stats.min, Duration::from_millis(10));
        assert_eq!(stats.max, Duration::from_millis(30));
    }

    #[test]
    fn test_profiler_time_closure() {
        let mut profiler = Profiler::new("test");
        let result = profiler.time("add", || 2 + 2);
        assert_eq!(result, 4);

        let stats = profiler.stats("add").unwrap();
        assert_eq!(stats.count, 1);
    }

    #[test]
    fn test_profiler_time_n() {
        let mut profiler = Profiler::new("test");
        let mut counter = 0u64;
        profiler.time_n("increment", 100, || {
            counter += 1;
        });
        assert_eq!(counter, 100);

        let stats = profiler.stats("increment").unwrap();
        assert_eq!(stats.count, 100);
    }

    #[test]
    fn test_profiler_report() {
        let mut profiler = Profiler::new("demo");
        profiler.record("fast_op", Duration::from_nanos(500));
        profiler.record("slow_op", Duration::from_millis(5));

        let report = profiler.report();
        assert!(report.contains("demo"));
        assert!(report.contains("fast_op"));
        assert!(report.contains("slow_op"));
    }

    #[test]
    fn test_profiler_reset() {
        let mut profiler = Profiler::new("test");
        profiler.record("op", Duration::from_millis(1));
        assert!(profiler.stats("op").is_some());
        profiler.reset();
        assert!(profiler.stats("op").is_none());
    }

    #[test]
    fn test_ops_per_sec() {
        let stats = ProfileStats {
            operation: "test".into(),
            count: 100,
            min: Duration::from_micros(100),
            max: Duration::from_micros(200),
            mean: Duration::from_millis(1), // 1ms mean = 1000 ops/s
            median: Duration::from_millis(1),
            p95: Duration::from_millis(2),
            p99: Duration::from_millis(3),
            total: Duration::from_millis(100),
        };
        let ops = stats.ops_per_sec();
        assert!((ops - 1000.0).abs() < 1.0);
    }
}