1use std::collections::HashMap;
30use std::time::{Duration, Instant};
31
32#[derive(Clone, Debug)]
34pub struct TestMetrics {
35 pub name: String,
37 pub timings_ns: Vec<u64>,
39 start: Option<Instant>,
41 pub op_counts: HashMap<String, u64>,
43 pub custom_metrics: HashMap<String, f64>,
45 pub memory_samples: Vec<usize>,
47 pub error_count: u64,
49 pub warning_count: u64,
50}
51
52impl TestMetrics {
53 pub fn new(name: &str) -> Self {
55 Self {
56 name: name.to_string(),
57 timings_ns: Vec::new(),
58 start: None,
59 op_counts: HashMap::new(),
60 custom_metrics: HashMap::new(),
61 memory_samples: Vec::new(),
62 error_count: 0,
63 warning_count: 0,
64 }
65 }
66
67 #[inline]
69 pub fn start_timing(&mut self) {
70 self.start = Some(Instant::now());
71 }
72
73 #[inline]
75 pub fn stop_timing(&mut self) {
76 if let Some(start) = self.start.take() {
77 self.timings_ns.push(start.elapsed().as_nanos() as u64);
78 }
79 }
80
81 #[inline]
83 pub fn time_operation<F, R>(&mut self, f: F) -> R
84 where
85 F: FnOnce() -> R,
86 {
87 self.start_timing();
88 let result = f();
89 self.stop_timing();
90 result
91 }
92
93 #[inline]
95 pub fn inc_op(&mut self, category: &str) {
96 *self.op_counts.entry(category.to_string()).or_insert(0) += 1;
97 }
98
99 #[inline]
101 pub fn record_operation(&mut self, count: u64) {
102 *self.op_counts.entry("operations".to_string()).or_insert(0) += count;
103 }
104
105 #[inline]
107 pub fn record_metric(&mut self, name: &str, value: f64) {
108 self.custom_metrics.insert(name.to_string(), value);
109 }
110
111 #[inline]
113 pub fn record_memory(&mut self, bytes: usize) {
114 self.memory_samples.push(bytes);
115 }
116
117 #[inline]
119 pub fn record_error(&mut self) {
120 self.error_count += 1;
121 }
122
123 #[inline]
125 pub fn record_warning(&mut self) {
126 self.warning_count += 1;
127 }
128
129 pub fn timing_stats(&self) -> TimingStats {
131 if self.timings_ns.is_empty() {
132 return TimingStats::default();
133 }
134
135 let mut sorted = self.timings_ns.clone();
136 sorted.sort_unstable();
137
138 let sum: u64 = sorted.iter().sum();
139 let count = sorted.len() as f64;
140 let mean = sum as f64 / count;
141
142 let variance = sorted
143 .iter()
144 .map(|&t| {
145 let diff = t as f64 - mean;
146 diff * diff
147 })
148 .sum::<f64>()
149 / count;
150
151 TimingStats {
152 count: sorted.len(),
153 min_ns: sorted[0],
154 max_ns: sorted[sorted.len() - 1],
155 mean_ns: mean,
156 std_dev_ns: variance.sqrt(),
157 p50_ns: sorted[sorted.len() / 2],
158 p95_ns: sorted[(sorted.len() as f64 * 0.95) as usize],
159 p99_ns: sorted[(sorted.len() as f64 * 0.99).min(sorted.len() as f64 - 1.0) as usize],
160 total_ns: sum,
161 }
162 }
163
164 pub fn summary(&self) -> String {
166 let stats = self.timing_stats();
167 let mut report = format!("=== {} Metrics ===\n", self.name);
168
169 if stats.count > 0 {
170 report.push_str(&format!(
171 "Timing: {} ops, mean={:.2}µs, p50={:.2}µs, p95={:.2}µs, p99={:.2}µs\n",
172 stats.count,
173 stats.mean_ns / 1000.0,
174 stats.p50_ns as f64 / 1000.0,
175 stats.p95_ns as f64 / 1000.0,
176 stats.p99_ns as f64 / 1000.0,
177 ));
178 report.push_str(&format!(
179 " min={:.2}µs, max={:.2}µs, stddev={:.2}µs\n",
180 stats.min_ns as f64 / 1000.0,
181 stats.max_ns as f64 / 1000.0,
182 stats.std_dev_ns / 1000.0,
183 ));
184 }
185
186 if !self.op_counts.is_empty() {
187 report.push_str("Operations: ");
188 let ops: Vec<_> = self
189 .op_counts
190 .iter()
191 .map(|(k, v)| format!("{}={}", k, v))
192 .collect();
193 report.push_str(&ops.join(", "));
194 report.push('\n');
195 }
196
197 if !self.custom_metrics.is_empty() {
198 report.push_str("Metrics: ");
199 let metrics: Vec<_> = self
200 .custom_metrics
201 .iter()
202 .map(|(k, v)| format!("{}={:.4}", k, v))
203 .collect();
204 report.push_str(&metrics.join(", "));
205 report.push('\n');
206 }
207
208 if !self.memory_samples.is_empty() {
209 let max_mem = self.memory_samples.iter().max().unwrap_or(&0);
210 let avg_mem = self.memory_samples.iter().sum::<usize>() / self.memory_samples.len();
211 report.push_str(&format!(
212 "Memory: peak={}KB, avg={}KB\n",
213 max_mem / 1024,
214 avg_mem / 1024,
215 ));
216 }
217
218 if self.error_count > 0 || self.warning_count > 0 {
219 report.push_str(&format!(
220 "Issues: errors={}, warnings={}\n",
221 self.error_count, self.warning_count
222 ));
223 }
224
225 report
226 }
227
228 pub fn reset(&mut self) {
230 self.timings_ns.clear();
231 self.start = None;
232 self.op_counts.clear();
233 self.custom_metrics.clear();
234 self.memory_samples.clear();
235 self.error_count = 0;
236 self.warning_count = 0;
237 }
238}
239
240#[derive(Clone, Debug, Default)]
242pub struct TimingStats {
243 pub count: usize,
244 pub min_ns: u64,
245 pub max_ns: u64,
246 pub mean_ns: f64,
247 pub std_dev_ns: f64,
248 pub p50_ns: u64,
249 pub p95_ns: u64,
250 pub p99_ns: u64,
251 pub total_ns: u64,
252}
253
254impl TimingStats {
255 pub fn total_duration(&self) -> Duration {
257 Duration::from_nanos(self.total_ns)
258 }
259
260 pub fn ops_per_sec(&self) -> f64 {
262 if self.total_ns == 0 {
263 0.0
264 } else {
265 (self.count as f64) / (self.total_ns as f64 / 1_000_000_000.0)
266 }
267 }
268
269 pub fn avg_latency_us(&self) -> f64 {
271 self.mean_ns / 1000.0
272 }
273
274 pub fn p50_latency_us(&self) -> f64 {
276 self.p50_ns as f64 / 1000.0
277 }
278
279 pub fn p95_latency_us(&self) -> f64 {
281 self.p95_ns as f64 / 1000.0
282 }
283
284 pub fn p99_latency_us(&self) -> f64 {
286 self.p99_ns as f64 / 1000.0
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn test_basic_timing() {
296 let mut metrics = TestMetrics::new("test_op");
297
298 metrics.start_timing();
299 std::thread::sleep(Duration::from_micros(100));
300 metrics.stop_timing();
301
302 assert_eq!(metrics.timings_ns.len(), 1);
303 assert!(metrics.timings_ns[0] >= 100_000); }
305
306 #[test]
307 fn test_time_operation() {
308 let mut metrics = TestMetrics::new("closure_test");
309
310 let result = metrics.time_operation(|| {
311 std::thread::sleep(Duration::from_micros(50));
312 42
313 });
314
315 assert_eq!(result, 42);
316 assert_eq!(metrics.timings_ns.len(), 1);
317 }
318
319 #[test]
320 fn test_operation_counting() {
321 let mut metrics = TestMetrics::new("ops");
322
323 metrics.inc_op("reads");
324 metrics.inc_op("reads");
325 metrics.inc_op("writes");
326
327 assert_eq!(metrics.op_counts.get("reads"), Some(&2));
328 assert_eq!(metrics.op_counts.get("writes"), Some(&1));
329 }
330
331 #[test]
332 fn test_custom_metrics() {
333 let mut metrics = TestMetrics::new("custom");
334
335 metrics.record_metric("accuracy", 0.95);
336 metrics.record_metric("loss", 0.12);
337
338 assert_eq!(metrics.custom_metrics.get("accuracy"), Some(&0.95));
339 assert_eq!(metrics.custom_metrics.get("loss"), Some(&0.12));
340 }
341
342 #[test]
343 fn test_memory_tracking() {
344 let mut metrics = TestMetrics::new("memory");
345
346 metrics.record_memory(1024);
347 metrics.record_memory(2048);
348 metrics.record_memory(1536);
349
350 assert_eq!(metrics.memory_samples.len(), 3);
351 }
352
353 #[test]
354 fn test_error_warning_counts() {
355 let mut metrics = TestMetrics::new("issues");
356
357 metrics.record_error();
358 metrics.record_error();
359 metrics.record_warning();
360
361 assert_eq!(metrics.error_count, 2);
362 assert_eq!(metrics.warning_count, 1);
363 }
364
365 #[test]
366 fn test_timing_stats() {
367 let mut metrics = TestMetrics::new("stats");
368
369 metrics.timings_ns = vec![100, 200, 150, 300, 250];
371
372 let stats = metrics.timing_stats();
373 assert_eq!(stats.count, 5);
374 assert_eq!(stats.min_ns, 100);
375 assert_eq!(stats.max_ns, 300);
376 assert_eq!(stats.mean_ns, 200.0);
377 }
378
379 #[test]
380 fn test_summary_generation() {
381 let mut metrics = TestMetrics::new("summary_test");
382
383 metrics.timings_ns = vec![1000, 2000, 1500];
384 metrics.inc_op("test");
385 metrics.record_metric("score", 0.85);
386
387 let summary = metrics.summary();
388
389 assert!(summary.contains("summary_test"));
390 assert!(summary.contains("Timing"));
391 assert!(summary.contains("Operations"));
392 assert!(summary.contains("Metrics"));
393 }
394
395 #[test]
396 fn test_reset() {
397 let mut metrics = TestMetrics::new("reset_test");
398
399 metrics.start_timing();
400 std::thread::sleep(Duration::from_micros(10));
401 metrics.stop_timing();
402 metrics.inc_op("test");
403 metrics.record_error();
404
405 assert_eq!(metrics.timings_ns.len(), 1);
406 assert_eq!(metrics.error_count, 1);
407
408 metrics.reset();
409
410 assert_eq!(metrics.timings_ns.len(), 0);
411 assert_eq!(metrics.error_count, 0);
412 assert_eq!(metrics.op_counts.len(), 0);
413 }
414
415 #[test]
416 fn test_empty_stats() {
417 let metrics = TestMetrics::new("empty");
418 let stats = metrics.timing_stats();
419
420 assert_eq!(stats.count, 0);
421 assert_eq!(stats.total_ns, 0);
422 }
423
424 #[test]
425 fn test_throughput_calculation() {
426 let mut stats = TimingStats::default();
427 stats.count = 1000;
428 stats.total_ns = 1_000_000_000; assert_eq!(stats.ops_per_sec(), 1000.0);
431 }
432}