Skip to main content

embeddenator_obs/obs/
test_metrics.rs

1//! Test Metrics and Performance Tracking
2//!
3//! Comprehensive performance metrics for testing and benchmarking.
4//! Extracted from embeddenator core testing utilities.
5//!
6//! # Features
7//!
8//! - Multi-sample timing with percentile statistics
9//! - Operation counting by category
10//! - Custom metric recording
11//! - Memory usage tracking
12//! - Error/warning counting
13//! - Automatic statistical analysis
14//!
15//! # Usage
16//!
17//! ```rust
18//! use embeddenator_obs::test_metrics::TestMetrics;
19//!
20//! let mut metrics = TestMetrics::new("bind_operation");
21//! metrics.start_timing();
22//! // ... perform operation ...
23//! metrics.stop_timing();
24//! metrics.record_operation(1000); // e.g., items processed
25//!
26//! println!("{}", metrics.summary());
27//! ```
28
29use std::collections::HashMap;
30use std::time::{Duration, Instant};
31
32/// Granular performance metrics for test operations.
33#[derive(Clone, Debug)]
34pub struct TestMetrics {
35    /// Operation name for reporting
36    pub name: String,
37    /// Individual timing samples (nanoseconds)
38    pub timings_ns: Vec<u64>,
39    /// Start time for current measurement
40    start: Option<Instant>,
41    /// Operation counts by category
42    pub op_counts: HashMap<String, u64>,
43    /// Custom numeric metrics
44    pub custom_metrics: HashMap<String, f64>,
45    /// Memory snapshots (bytes)
46    pub memory_samples: Vec<usize>,
47    /// Error/warning counts
48    pub error_count: u64,
49    pub warning_count: u64,
50}
51
52impl TestMetrics {
53    /// Create new metrics collector for named operation.
54    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    /// Start timing measurement.
68    #[inline]
69    pub fn start_timing(&mut self) {
70        self.start = Some(Instant::now());
71    }
72
73    /// Stop timing and record sample.
74    #[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    /// Record a timed operation with closure.
82    #[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    /// Increment operation counter.
94    #[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    /// Record operation count.
100    #[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    /// Record custom metric.
106    #[inline]
107    pub fn record_metric(&mut self, name: &str, value: f64) {
108        self.custom_metrics.insert(name.to_string(), value);
109    }
110
111    /// Record memory usage.
112    #[inline]
113    pub fn record_memory(&mut self, bytes: usize) {
114        self.memory_samples.push(bytes);
115    }
116
117    /// Record an error.
118    #[inline]
119    pub fn record_error(&mut self) {
120        self.error_count += 1;
121    }
122
123    /// Record a warning.
124    #[inline]
125    pub fn record_warning(&mut self) {
126        self.warning_count += 1;
127    }
128
129    /// Get timing statistics.
130    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    /// Generate summary report.
165    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    /// Reset all metrics (useful for reusing the same collector).
229    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/// Timing statistics.
241#[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    /// Total time as Duration.
256    pub fn total_duration(&self) -> Duration {
257        Duration::from_nanos(self.total_ns)
258    }
259
260    /// Throughput in operations per second.
261    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    /// Average latency in microseconds.
270    pub fn avg_latency_us(&self) -> f64 {
271        self.mean_ns / 1000.0
272    }
273
274    /// P50 latency in microseconds.
275    pub fn p50_latency_us(&self) -> f64 {
276        self.p50_ns as f64 / 1000.0
277    }
278
279    /// P95 latency in microseconds.
280    pub fn p95_latency_us(&self) -> f64 {
281        self.p95_ns as f64 / 1000.0
282    }
283
284    /// P99 latency in microseconds.
285    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); // at least 100µs in ns
304    }
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        // Add known values for easy verification
370        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; // 1 second
429
430        assert_eq!(stats.ops_per_sec(), 1000.0);
431    }
432}