1use std::collections::HashMap;
10use std::time::{Duration, Instant};
11
12#[derive(Clone, Debug)]
14pub struct TestMetrics {
15 pub name: String,
17 pub timings_ns: Vec<u64>,
19 start: Option<Instant>,
21 pub op_counts: HashMap<String, u64>,
23 pub custom_metrics: HashMap<String, f64>,
25 pub memory_samples: Vec<usize>,
27 pub error_count: u64,
29 pub warning_count: u64,
30}
31
32impl TestMetrics {
33 pub fn new(name: &str) -> Self {
35 Self {
36 name: name.to_string(),
37 timings_ns: Vec::new(),
38 start: None,
39 op_counts: HashMap::new(),
40 custom_metrics: HashMap::new(),
41 memory_samples: Vec::new(),
42 error_count: 0,
43 warning_count: 0,
44 }
45 }
46
47 #[inline]
49 pub fn start_timing(&mut self) {
50 self.start = Some(Instant::now());
51 }
52
53 #[inline]
55 pub fn stop_timing(&mut self) {
56 if let Some(start) = self.start.take() {
57 self.timings_ns.push(start.elapsed().as_nanos() as u64);
58 }
59 }
60
61 #[inline]
63 pub fn time_operation<F, R>(&mut self, f: F) -> R
64 where
65 F: FnOnce() -> R,
66 {
67 self.start_timing();
68 let result = f();
69 self.stop_timing();
70 result
71 }
72
73 #[inline]
75 pub fn inc_op(&mut self, category: &str) {
76 *self.op_counts.entry(category.to_string()).or_insert(0) += 1;
77 }
78
79 #[inline]
81 pub fn record_metric(&mut self, name: &str, value: f64) {
82 self.custom_metrics.insert(name.to_string(), value);
83 }
84
85 #[inline]
87 pub fn record_memory(&mut self, bytes: usize) {
88 self.memory_samples.push(bytes);
89 }
90
91 #[inline]
93 pub fn record_operation(&mut self, count: usize) {
94 self.inc_op("operations");
95 self.record_metric("last_count", count as f64);
96 }
97
98 #[inline]
100 pub fn record_error(&mut self) {
101 self.error_count += 1;
102 }
103
104 #[inline]
106 pub fn record_warning(&mut self) {
107 self.warning_count += 1;
108 }
109
110 pub fn timing_stats(&self) -> TimingStats {
112 if self.timings_ns.is_empty() {
113 return TimingStats::default();
114 }
115
116 let mut sorted = self.timings_ns.clone();
117 sorted.sort_unstable();
118
119 let sum: u64 = sorted.iter().sum();
120 let count = sorted.len() as f64;
121 let mean = sum as f64 / count;
122
123 let variance = sorted
124 .iter()
125 .map(|&t| {
126 let diff = t as f64 - mean;
127 diff * diff
128 })
129 .sum::<f64>()
130 / count;
131
132 TimingStats {
133 count: sorted.len(),
134 min_ns: sorted[0],
135 max_ns: sorted[sorted.len() - 1],
136 mean_ns: mean,
137 std_dev_ns: variance.sqrt(),
138 p50_ns: sorted[sorted.len() / 2],
139 p95_ns: sorted[(sorted.len() as f64 * 0.95) as usize],
140 p99_ns: sorted[(sorted.len() as f64 * 0.99).min(sorted.len() as f64 - 1.0) as usize],
141 total_ns: sum,
142 }
143 }
144
145 pub fn summary(&self) -> String {
147 let stats = self.timing_stats();
148 let mut report = format!("=== {} Metrics ===\n", self.name);
149
150 if stats.count > 0 {
151 report.push_str(&format!(
152 "Timing: {} ops, mean={:.2}µs, p50={:.2}µs, p95={:.2}µs, p99={:.2}µs\n",
153 stats.count,
154 stats.mean_ns / 1000.0,
155 stats.p50_ns as f64 / 1000.0,
156 stats.p95_ns as f64 / 1000.0,
157 stats.p99_ns as f64 / 1000.0,
158 ));
159 report.push_str(&format!(
160 " min={:.2}µs, max={:.2}µs, stddev={:.2}µs\n",
161 stats.min_ns as f64 / 1000.0,
162 stats.max_ns as f64 / 1000.0,
163 stats.std_dev_ns / 1000.0,
164 ));
165 }
166
167 if !self.op_counts.is_empty() {
168 report.push_str("Operations: ");
169 let ops: Vec<_> = self
170 .op_counts
171 .iter()
172 .map(|(k, v)| format!("{}={}", k, v))
173 .collect();
174 report.push_str(&ops.join(", "));
175 report.push('\n');
176 }
177
178 if !self.custom_metrics.is_empty() {
179 report.push_str("Metrics: ");
180 let metrics: Vec<_> = self
181 .custom_metrics
182 .iter()
183 .map(|(k, v)| format!("{}={:.4}", k, v))
184 .collect();
185 report.push_str(&metrics.join(", "));
186 report.push('\n');
187 }
188
189 if !self.memory_samples.is_empty() {
190 let max_mem = self.memory_samples.iter().max().unwrap_or(&0);
191 let avg_mem = self.memory_samples.iter().sum::<usize>() / self.memory_samples.len();
192 report.push_str(&format!(
193 "Memory: peak={}KB, avg={}KB\n",
194 max_mem / 1024,
195 avg_mem / 1024,
196 ));
197 }
198
199 if self.error_count > 0 || self.warning_count > 0 {
200 report.push_str(&format!(
201 "Issues: errors={}, warnings={}\n",
202 self.error_count, self.warning_count
203 ));
204 }
205
206 report
207 }
208}
209
210#[derive(Clone, Debug, Default)]
212pub struct TimingStats {
213 pub count: usize,
214 pub min_ns: u64,
215 pub max_ns: u64,
216 pub mean_ns: f64,
217 pub std_dev_ns: f64,
218 pub p50_ns: u64,
219 pub p95_ns: u64,
220 pub p99_ns: u64,
221 pub total_ns: u64,
222}
223
224impl TimingStats {
225 pub fn total_duration(&self) -> Duration {
227 Duration::from_nanos(self.total_ns)
228 }
229
230 pub fn ops_per_sec(&self) -> f64 {
232 if self.total_ns == 0 {
233 0.0
234 } else {
235 (self.count as f64) / (self.total_ns as f64 / 1_000_000_000.0)
236 }
237 }
238
239 pub fn mean_duration(&self) -> Duration {
241 Duration::from_nanos(self.mean_ns as u64)
242 }
243
244 pub fn median_duration(&self) -> Duration {
246 Duration::from_nanos(self.p50_ns)
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use std::thread;
254
255 #[test]
256 fn test_metrics_timing() {
257 let mut metrics = TestMetrics::new("test_operation");
258
259 metrics.start_timing();
260 thread::sleep(Duration::from_millis(10));
261 metrics.stop_timing();
262
263 let stats = metrics.timing_stats();
264 assert_eq!(stats.count, 1);
265 assert!(stats.mean_ns > 10_000_000.0); }
267
268 #[test]
269 fn test_time_operation() {
270 let mut metrics = TestMetrics::new("test");
271
272 let result = metrics.time_operation(|| {
273 thread::sleep(Duration::from_millis(5));
274 42
275 });
276
277 assert_eq!(result, 42);
278 assert_eq!(metrics.timings_ns.len(), 1);
279 }
280
281 #[test]
282 fn test_custom_metrics() {
283 let mut metrics = TestMetrics::new("test");
284 metrics.record_metric("accuracy", 0.95);
285 metrics.record_metric("loss", 0.05);
286
287 assert_eq!(metrics.custom_metrics.get("accuracy"), Some(&0.95));
288 assert_eq!(metrics.custom_metrics.get("loss"), Some(&0.05));
289 }
290
291 #[test]
292 fn test_summary() {
293 let mut metrics = TestMetrics::new("test_op");
294 metrics.start_timing();
295 thread::sleep(Duration::from_millis(1));
296 metrics.stop_timing();
297
298 let summary = metrics.summary();
299 assert!(summary.contains("test_op"));
300 assert!(summary.contains("Timing:"));
301 }
302}