shadow_benchmarks/
profiler.rs1use std::collections::HashMap;
4use std::time::{Duration, Instant};
5
6#[derive(Debug, Clone)]
8pub struct Profiler {
9 name: String,
10 timings: HashMap<String, Vec<Duration>>,
11}
12
13#[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 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 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 pub fn new(name: impl Into<String>) -> Self {
53 Self {
54 name: name.into(),
55 timings: HashMap::new(),
56 }
57 }
58
59 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 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 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 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 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 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 pub fn reset(&mut self) {
145 self.timings.clear();
146 }
147
148 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), 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}