Skip to main content

embeddenator_testkit/
metrics.rs

1//! Performance metrics and timing utilities for testing
2//!
3//! Provides granular performance measurement tools including:
4//! - Operation timing with statistics (mean, median, percentiles)
5//! - Memory usage tracking
6//! - Throughput calculations
7//! - Custom metric recording
8
9use std::collections::HashMap;
10use std::time::{Duration, Instant};
11
12/// Granular performance metrics for test operations
13#[derive(Clone, Debug)]
14pub struct TestMetrics {
15    /// Operation name for reporting
16    pub name: String,
17    /// Individual timing samples (nanoseconds)
18    pub timings_ns: Vec<u64>,
19    /// Start time for current measurement
20    start: Option<Instant>,
21    /// Operation counts by category
22    pub op_counts: HashMap<String, u64>,
23    /// Custom numeric metrics
24    pub custom_metrics: HashMap<String, f64>,
25    /// Memory snapshots (bytes)
26    pub memory_samples: Vec<usize>,
27    /// Error/warning counts
28    pub error_count: u64,
29    pub warning_count: u64,
30}
31
32impl TestMetrics {
33    /// Create new metrics collector for named operation
34    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    /// Start timing measurement
48    #[inline]
49    pub fn start_timing(&mut self) {
50        self.start = Some(Instant::now());
51    }
52
53    /// Stop timing and record sample
54    #[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    /// Record a timed operation with closure
62    #[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    /// Increment operation counter
74    #[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    /// Record custom metric
80    #[inline]
81    pub fn record_metric(&mut self, name: &str, value: f64) {
82        self.custom_metrics.insert(name.to_string(), value);
83    }
84
85    /// Record memory usage
86    #[inline]
87    pub fn record_memory(&mut self, bytes: usize) {
88        self.memory_samples.push(bytes);
89    }
90
91    /// Record operation count
92    #[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    /// Record an error
99    #[inline]
100    pub fn record_error(&mut self) {
101        self.error_count += 1;
102    }
103
104    /// Record a warning
105    #[inline]
106    pub fn record_warning(&mut self) {
107        self.warning_count += 1;
108    }
109
110    /// Get timing statistics
111    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    /// Generate summary report
146    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/// Timing statistics
211#[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    /// Total time as Duration
226    pub fn total_duration(&self) -> Duration {
227        Duration::from_nanos(self.total_ns)
228    }
229
230    /// Throughput in operations per second
231    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    /// Mean time as Duration
240    pub fn mean_duration(&self) -> Duration {
241        Duration::from_nanos(self.mean_ns as u64)
242    }
243
244    /// Median time as Duration
245    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); // At least 10ms
266    }
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}