Skip to main content

shadow_benchmarks/
profiler.rs

1//! Simple profiler for tracking operation timings
2
3use std::collections::HashMap;
4use std::time::{Duration, Instant};
5
6/// A lightweight profiler that tracks operation timings
7#[derive(Debug, Clone)]
8pub struct Profiler {
9    name: String,
10    timings: HashMap<String, Vec<Duration>>,
11}
12
13/// Summary statistics for a profiled operation
14#[derive(Debug, Clone)]
15pub struct ProfileStats {
16    pub operation: String,
17    pub count: usize,
18    pub min: Duration,
19    pub max: Duration,
20    pub mean: Duration,
21    pub median: Duration,
22    pub p95: Duration,
23    pub p99: Duration,
24    pub total: Duration,
25}
26
27impl ProfileStats {
28    /// Operations per second
29    pub fn ops_per_sec(&self) -> f64 {
30        if self.mean.as_nanos() == 0 {
31            return 0.0;
32        }
33        1_000_000_000.0 / self.mean.as_nanos() as f64
34    }
35
36    /// Format as a human-readable report line
37    pub fn summary_line(&self) -> String {
38        format!(
39            "{:<30} {:>8} samples | mean {:>12.2?} | p95 {:>12.2?} | p99 {:>12.2?} | {:.0} ops/s",
40            self.operation,
41            self.count,
42            self.mean,
43            self.p95,
44            self.p99,
45            self.ops_per_sec()
46        )
47    }
48}
49
50impl Profiler {
51    /// Create a new profiler
52    pub fn new(name: impl Into<String>) -> Self {
53        Self {
54            name: name.into(),
55            timings: HashMap::new(),
56        }
57    }
58
59    /// Record a single timed operation
60    pub fn record(&mut self, operation: &str, duration: Duration) {
61        self.timings
62            .entry(operation.to_string())
63            .or_default()
64            .push(duration);
65    }
66
67    /// Time a closure and record the result
68    pub fn time<F, R>(&mut self, operation: &str, f: F) -> R
69    where
70        F: FnOnce() -> R,
71    {
72        let start = Instant::now();
73        let result = f();
74        self.record(operation, start.elapsed());
75        result
76    }
77
78    /// Time a closure N times and record all results
79    pub fn time_n<F>(&mut self, operation: &str, iterations: usize, mut f: F)
80    where
81        F: FnMut(),
82    {
83        for _ in 0..iterations {
84            let start = Instant::now();
85            f();
86            self.record(operation, start.elapsed());
87        }
88    }
89
90    /// Get statistics for a specific operation
91    pub fn stats(&self, operation: &str) -> Option<ProfileStats> {
92        let timings = self.timings.get(operation)?;
93        if timings.is_empty() {
94            return None;
95        }
96
97        let mut sorted: Vec<Duration> = timings.clone();
98        sorted.sort();
99
100        let count = sorted.len();
101        let total: Duration = sorted.iter().sum();
102        let mean = total / count as u32;
103
104        let percentile = |p: f64| -> Duration {
105            let idx = ((count as f64 * p) as usize).min(count - 1);
106            sorted[idx]
107        };
108
109        Some(ProfileStats {
110            operation: operation.to_string(),
111            count,
112            min: sorted[0],
113            max: sorted[count - 1],
114            mean,
115            median: percentile(0.5),
116            p95: percentile(0.95),
117            p99: percentile(0.99),
118            total,
119        })
120    }
121
122    /// Get statistics for all operations
123    pub fn all_stats(&self) -> Vec<ProfileStats> {
124        let mut stats: Vec<ProfileStats> = self
125            .timings
126            .keys()
127            .filter_map(|op| self.stats(op))
128            .collect();
129        stats.sort_by_key(|s| s.operation.clone());
130        stats
131    }
132
133    /// Print a full report
134    pub fn report(&self) -> String {
135        let mut lines = vec![format!("╔══ {} Profiler Report ══╗", self.name)];
136        for stat in self.all_stats() {
137            lines.push(stat.summary_line());
138        }
139        lines.push(format!("╚══ {} operations profiled ══╝", self.timings.len()));
140        lines.join("\n")
141    }
142
143    /// Reset all timings
144    pub fn reset(&mut self) {
145        self.timings.clear();
146    }
147
148    /// Name of this profiler
149    pub fn name(&self) -> &str {
150        &self.name
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_profiler_basic() {
160        let mut profiler = Profiler::new("test");
161        profiler.record("op1", Duration::from_millis(10));
162        profiler.record("op1", Duration::from_millis(20));
163        profiler.record("op1", Duration::from_millis(30));
164
165        let stats = profiler.stats("op1").unwrap();
166        assert_eq!(stats.count, 3);
167        assert_eq!(stats.min, Duration::from_millis(10));
168        assert_eq!(stats.max, Duration::from_millis(30));
169    }
170
171    #[test]
172    fn test_profiler_time_closure() {
173        let mut profiler = Profiler::new("test");
174        let result = profiler.time("add", || 2 + 2);
175        assert_eq!(result, 4);
176
177        let stats = profiler.stats("add").unwrap();
178        assert_eq!(stats.count, 1);
179    }
180
181    #[test]
182    fn test_profiler_time_n() {
183        let mut profiler = Profiler::new("test");
184        let mut counter = 0u64;
185        profiler.time_n("increment", 100, || {
186            counter += 1;
187        });
188        assert_eq!(counter, 100);
189
190        let stats = profiler.stats("increment").unwrap();
191        assert_eq!(stats.count, 100);
192    }
193
194    #[test]
195    fn test_profiler_report() {
196        let mut profiler = Profiler::new("demo");
197        profiler.record("fast_op", Duration::from_nanos(500));
198        profiler.record("slow_op", Duration::from_millis(5));
199
200        let report = profiler.report();
201        assert!(report.contains("demo"));
202        assert!(report.contains("fast_op"));
203        assert!(report.contains("slow_op"));
204    }
205
206    #[test]
207    fn test_profiler_reset() {
208        let mut profiler = Profiler::new("test");
209        profiler.record("op", Duration::from_millis(1));
210        assert!(profiler.stats("op").is_some());
211        profiler.reset();
212        assert!(profiler.stats("op").is_none());
213    }
214
215    #[test]
216    fn test_ops_per_sec() {
217        let stats = ProfileStats {
218            operation: "test".into(),
219            count: 100,
220            min: Duration::from_micros(100),
221            max: Duration::from_micros(200),
222            mean: Duration::from_millis(1), // 1ms mean = 1000 ops/s
223            median: Duration::from_millis(1),
224            p95: Duration::from_millis(2),
225            p99: Duration::from_millis(3),
226            total: Duration::from_millis(100),
227        };
228        let ops = stats.ops_per_sec();
229        assert!((ops - 1000.0).abs() < 1.0);
230    }
231}