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/// Accuracy metrics for VSA encoding/decoding fidelity
251#[derive(Clone, Debug, Default)]
252pub struct AccuracyMetrics {
253    /// Total bytes processed
254    pub total_bytes: usize,
255    /// Bytes requiring correction
256    pub correction_bytes: usize,
257    /// Number of correction entries
258    pub correction_count: usize,
259    /// Raw reconstruction accuracy (before corrections)
260    pub raw_accuracy: f64,
261    /// Final accuracy (after corrections)
262    pub final_accuracy: f64,
263    /// Signal-to-noise ratio in dB
264    pub snr_db: f64,
265    /// Compression ratio (original / encoded size)
266    pub compression_ratio: f64,
267}
268
269impl AccuracyMetrics {
270    /// Create new accuracy metrics
271    pub fn new() -> Self {
272        Self::default()
273    }
274
275    /// Record encoding fidelity from original and reconstructed data
276    pub fn record_fidelity(&mut self, original: &[u8], reconstructed: &[u8]) {
277        self.total_bytes += original.len();
278
279        let matching = original
280            .iter()
281            .zip(reconstructed.iter())
282            .filter(|(a, b)| a == b)
283            .count();
284
285        let errors = original.len().saturating_sub(matching);
286        self.correction_bytes += errors;
287
288        self.raw_accuracy = if self.total_bytes > 0 {
289            1.0 - (self.correction_bytes as f64 / self.total_bytes as f64)
290        } else {
291            1.0
292        };
293
294        // Calculate SNR: 10 * log10(signal_power / noise_power)
295        // For byte data, signal_power ≈ variance of original, noise_power ≈ MSE
296        if !original.is_empty() && !reconstructed.is_empty() {
297            let signal_power: f64 =
298                original.iter().map(|&b| (b as f64).powi(2)).sum::<f64>() / original.len() as f64;
299            let noise_power: f64 = original
300                .iter()
301                .zip(reconstructed.iter())
302                .map(|(&a, &b)| ((a as f64) - (b as f64)).powi(2))
303                .sum::<f64>()
304                / original.len() as f64;
305
306            self.snr_db = if noise_power > 0.0 {
307                10.0 * (signal_power / noise_power).log10()
308            } else {
309                f64::INFINITY // Perfect reconstruction
310            };
311        }
312    }
313
314    /// Record correction store statistics
315    pub fn record_corrections(&mut self, correction_count: usize, _correction_size_bytes: usize) {
316        self.correction_count += correction_count;
317        // Final accuracy assumes corrections fix all remaining errors
318        self.final_accuracy = 1.0;
319    }
320
321    /// Record compression statistics
322    pub fn record_compression(&mut self, original_size: usize, encoded_size: usize) {
323        if encoded_size > 0 {
324            self.compression_ratio = original_size as f64 / encoded_size as f64;
325        }
326    }
327
328    /// Get correction ratio (corrections / total bytes)
329    pub fn correction_ratio(&self) -> f64 {
330        if self.total_bytes > 0 {
331            self.correction_bytes as f64 / self.total_bytes as f64
332        } else {
333            0.0
334        }
335    }
336
337    /// Generate human-readable summary
338    pub fn summary(&self) -> String {
339        format!(
340            "Accuracy: raw={:.2}%, final={:.2}%, SNR={:.1}dB, corrections={} ({:.2}%)",
341            self.raw_accuracy * 100.0,
342            self.final_accuracy * 100.0,
343            self.snr_db,
344            self.correction_count,
345            self.correction_ratio() * 100.0
346        )
347    }
348}
349
350/// Combined performance and accuracy metrics for VSA operations
351#[derive(Clone, Debug)]
352pub struct VsaEvaluationMetrics {
353    /// Performance timing metrics
354    pub performance: TestMetrics,
355    /// Accuracy and fidelity metrics
356    pub accuracy: AccuracyMetrics,
357    /// Configuration used for evaluation
358    pub config_dimension: usize,
359    pub config_density: f64,
360    pub config_scaling: String,
361}
362
363impl VsaEvaluationMetrics {
364    /// Create new evaluation metrics
365    pub fn new(name: &str, dimension: usize, density: f64, scaling: &str) -> Self {
366        Self {
367            performance: TestMetrics::new(name),
368            accuracy: AccuracyMetrics::new(),
369            config_dimension: dimension,
370            config_density: density,
371            config_scaling: scaling.to_string(),
372        }
373    }
374
375    /// Generate comprehensive summary report
376    pub fn full_summary(&self) -> String {
377        let mut report = format!("=== VSA Evaluation: {} ===\n", self.performance.name);
378        report.push_str(&format!(
379            "Config: dim={}, density={:.3}, scaling={}\n",
380            self.config_dimension, self.config_density, self.config_scaling
381        ));
382        report.push_str(&self.performance.summary());
383        report.push_str(&self.accuracy.summary());
384        report.push('\n');
385        report
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392    use std::thread;
393
394    #[test]
395    fn test_metrics_timing() {
396        let mut metrics = TestMetrics::new("test_operation");
397
398        metrics.start_timing();
399        thread::sleep(Duration::from_millis(10));
400        metrics.stop_timing();
401
402        let stats = metrics.timing_stats();
403        assert_eq!(stats.count, 1);
404        assert!(stats.mean_ns > 10_000_000.0); // At least 10ms
405    }
406
407    #[test]
408    fn test_time_operation() {
409        let mut metrics = TestMetrics::new("test");
410
411        let result = metrics.time_operation(|| {
412            thread::sleep(Duration::from_millis(5));
413            42
414        });
415
416        assert_eq!(result, 42);
417        assert_eq!(metrics.timings_ns.len(), 1);
418    }
419
420    #[test]
421    fn test_custom_metrics() {
422        let mut metrics = TestMetrics::new("test");
423        metrics.record_metric("accuracy", 0.95);
424        metrics.record_metric("loss", 0.05);
425
426        assert_eq!(metrics.custom_metrics.get("accuracy"), Some(&0.95));
427        assert_eq!(metrics.custom_metrics.get("loss"), Some(&0.05));
428    }
429
430    #[test]
431    fn test_summary() {
432        let mut metrics = TestMetrics::new("test_op");
433        metrics.start_timing();
434        thread::sleep(Duration::from_millis(1));
435        metrics.stop_timing();
436
437        let summary = metrics.summary();
438        assert!(summary.contains("test_op"));
439        assert!(summary.contains("Timing:"));
440    }
441}