Skip to main content

heliosdb_proxy/cache/
metrics.rs

1//! Cache Metrics
2//!
3//! Tracks cache performance statistics including hit rates,
4//! latencies, memory usage, and invalidation counts.
5
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::{Duration, Instant};
8
9use super::CacheLevel;
10
11/// Cache metrics collector
12#[derive(Debug)]
13pub struct CacheMetrics {
14    /// L1 cache statistics
15    l1: CacheStats,
16
17    /// L2 cache statistics
18    l2: CacheStats,
19
20    /// L3 cache statistics
21    l3: CacheStats,
22
23    /// Total misses
24    misses: AtomicU64,
25
26    /// Total skips (due to hints)
27    skips: AtomicU64,
28
29    /// Total puts
30    puts: AtomicU64,
31
32    /// Total invalidations
33    invalidations: AtomicU64,
34
35    /// Tables invalidated
36    tables_invalidated: AtomicU64,
37
38    /// Cache clears
39    clears: AtomicU64,
40
41    /// Size exceeded rejections
42    size_exceeded: AtomicU64,
43
44    /// Creation time
45    created_at: Instant,
46}
47
48/// Statistics for a single cache level
49#[derive(Debug, Default)]
50pub struct CacheStats {
51    /// Cache hits
52    hits: AtomicU64,
53
54    /// Total latency in microseconds (for average calculation)
55    total_latency_us: AtomicU64,
56
57    /// Minimum latency in microseconds
58    min_latency_us: AtomicU64,
59
60    /// Maximum latency in microseconds
61    max_latency_us: AtomicU64,
62
63    /// Current entry count
64    entry_count: AtomicU64,
65
66    /// Current memory usage in bytes
67    memory_bytes: AtomicU64,
68
69    /// Evictions
70    evictions: AtomicU64,
71}
72
73impl CacheStats {
74    fn new() -> Self {
75        Self {
76            hits: AtomicU64::new(0),
77            total_latency_us: AtomicU64::new(0),
78            min_latency_us: AtomicU64::new(u64::MAX),
79            max_latency_us: AtomicU64::new(0),
80            entry_count: AtomicU64::new(0),
81            memory_bytes: AtomicU64::new(0),
82            evictions: AtomicU64::new(0),
83        }
84    }
85
86    fn record_hit(&self, latency: Duration) {
87        self.hits.fetch_add(1, Ordering::Relaxed);
88
89        let latency_us = latency.as_micros() as u64;
90        self.total_latency_us
91            .fetch_add(latency_us, Ordering::Relaxed);
92
93        // Update min
94        let mut current = self.min_latency_us.load(Ordering::Relaxed);
95        while latency_us < current {
96            match self.min_latency_us.compare_exchange_weak(
97                current,
98                latency_us,
99                Ordering::Relaxed,
100                Ordering::Relaxed,
101            ) {
102                Ok(_) => break,
103                Err(c) => current = c,
104            }
105        }
106
107        // Update max
108        let mut current = self.max_latency_us.load(Ordering::Relaxed);
109        while latency_us > current {
110            match self.max_latency_us.compare_exchange_weak(
111                current,
112                latency_us,
113                Ordering::Relaxed,
114                Ordering::Relaxed,
115            ) {
116                Ok(_) => break,
117                Err(c) => current = c,
118            }
119        }
120    }
121
122    fn snapshot(&self) -> CacheStatsLevelSnapshot {
123        let hits = self.hits.load(Ordering::Relaxed);
124        let total_latency = self.total_latency_us.load(Ordering::Relaxed);
125        let min_latency = self.min_latency_us.load(Ordering::Relaxed);
126        let max_latency = self.max_latency_us.load(Ordering::Relaxed);
127
128        CacheStatsLevelSnapshot {
129            hits,
130            avg_latency_us: total_latency.checked_div(hits).unwrap_or(0),
131            min_latency_us: if min_latency == u64::MAX {
132                0
133            } else {
134                min_latency
135            },
136            max_latency_us: max_latency,
137            entry_count: self.entry_count.load(Ordering::Relaxed),
138            memory_bytes: self.memory_bytes.load(Ordering::Relaxed),
139            evictions: self.evictions.load(Ordering::Relaxed),
140        }
141    }
142}
143
144impl CacheMetrics {
145    /// Create a new metrics collector
146    pub fn new() -> Self {
147        Self {
148            l1: CacheStats::new(),
149            l2: CacheStats::new(),
150            l3: CacheStats::new(),
151            misses: AtomicU64::new(0),
152            skips: AtomicU64::new(0),
153            puts: AtomicU64::new(0),
154            invalidations: AtomicU64::new(0),
155            tables_invalidated: AtomicU64::new(0),
156            clears: AtomicU64::new(0),
157            size_exceeded: AtomicU64::new(0),
158            created_at: Instant::now(),
159        }
160    }
161
162    /// Record a cache hit
163    pub fn record_hit(&self, level: CacheLevel, latency: Duration) {
164        match level {
165            CacheLevel::L1Hot => self.l1.record_hit(latency),
166            CacheLevel::L2Warm => self.l2.record_hit(latency),
167            CacheLevel::L3Semantic => self.l3.record_hit(latency),
168        }
169    }
170
171    /// Record a cache miss
172    pub fn record_miss(&self, _latency: Duration) {
173        self.misses.fetch_add(1, Ordering::Relaxed);
174    }
175
176    /// Record a cache skip (due to hint)
177    pub fn record_skip(&self) {
178        self.skips.fetch_add(1, Ordering::Relaxed);
179    }
180
181    /// Record a cache put
182    pub fn record_put(&self) {
183        self.puts.fetch_add(1, Ordering::Relaxed);
184    }
185
186    /// Record cache invalidation
187    pub fn record_invalidation(&self, table_count: usize) {
188        self.invalidations.fetch_add(1, Ordering::Relaxed);
189        self.tables_invalidated
190            .fetch_add(table_count as u64, Ordering::Relaxed);
191    }
192
193    /// Record cache clear
194    pub fn record_clear(&self) {
195        self.clears.fetch_add(1, Ordering::Relaxed);
196    }
197
198    /// Record size exceeded rejection
199    pub fn record_size_exceeded(&self) {
200        self.size_exceeded.fetch_add(1, Ordering::Relaxed);
201    }
202
203    /// Record eviction for a cache level
204    pub fn record_eviction(&self, level: CacheLevel) {
205        match level {
206            CacheLevel::L1Hot => self.l1.evictions.fetch_add(1, Ordering::Relaxed),
207            CacheLevel::L2Warm => self.l2.evictions.fetch_add(1, Ordering::Relaxed),
208            CacheLevel::L3Semantic => self.l3.evictions.fetch_add(1, Ordering::Relaxed),
209        };
210    }
211
212    /// Update entry count for a cache level
213    pub fn set_entry_count(&self, level: CacheLevel, count: u64) {
214        match level {
215            CacheLevel::L1Hot => self.l1.entry_count.store(count, Ordering::Relaxed),
216            CacheLevel::L2Warm => self.l2.entry_count.store(count, Ordering::Relaxed),
217            CacheLevel::L3Semantic => self.l3.entry_count.store(count, Ordering::Relaxed),
218        }
219    }
220
221    /// Update memory usage for a cache level
222    pub fn set_memory_bytes(&self, level: CacheLevel, bytes: u64) {
223        match level {
224            CacheLevel::L1Hot => self.l1.memory_bytes.store(bytes, Ordering::Relaxed),
225            CacheLevel::L2Warm => self.l2.memory_bytes.store(bytes, Ordering::Relaxed),
226            CacheLevel::L3Semantic => self.l3.memory_bytes.store(bytes, Ordering::Relaxed),
227        }
228    }
229
230    /// Get a snapshot of current metrics
231    pub fn snapshot(&self) -> CacheStatsSnapshot {
232        let l1 = self.l1.snapshot();
233        let l2 = self.l2.snapshot();
234        let l3 = self.l3.snapshot();
235        let misses = self.misses.load(Ordering::Relaxed);
236        let skips = self.skips.load(Ordering::Relaxed);
237
238        let total_hits = l1.hits + l2.hits + l3.hits;
239        let total_requests = total_hits + misses;
240
241        CacheStatsSnapshot {
242            l1,
243            l2,
244            l3,
245            total_hits,
246            total_misses: misses,
247            total_skips: skips,
248            hit_rate: if total_requests > 0 {
249                (total_hits as f64 / total_requests as f64) * 100.0
250            } else {
251                0.0
252            },
253            puts: self.puts.load(Ordering::Relaxed),
254            invalidations: self.invalidations.load(Ordering::Relaxed),
255            tables_invalidated: self.tables_invalidated.load(Ordering::Relaxed),
256            clears: self.clears.load(Ordering::Relaxed),
257            size_exceeded: self.size_exceeded.load(Ordering::Relaxed),
258            uptime_secs: self.created_at.elapsed().as_secs(),
259        }
260    }
261
262    /// Get total hit count
263    pub fn total_hits(&self) -> u64 {
264        self.l1.hits.load(Ordering::Relaxed)
265            + self.l2.hits.load(Ordering::Relaxed)
266            + self.l3.hits.load(Ordering::Relaxed)
267    }
268
269    /// Get total miss count
270    pub fn total_misses(&self) -> u64 {
271        self.misses.load(Ordering::Relaxed)
272    }
273
274    /// Calculate hit rate percentage
275    pub fn hit_rate(&self) -> f64 {
276        let hits = self.total_hits();
277        let misses = self.total_misses();
278        let total = hits + misses;
279
280        if total > 0 {
281            (hits as f64 / total as f64) * 100.0
282        } else {
283            0.0
284        }
285    }
286}
287
288impl Default for CacheMetrics {
289    fn default() -> Self {
290        Self::new()
291    }
292}
293
294/// Snapshot of cache statistics for a single level
295#[derive(Debug, Clone)]
296pub struct CacheStatsLevelSnapshot {
297    /// Number of cache hits
298    pub hits: u64,
299
300    /// Average latency in microseconds
301    pub avg_latency_us: u64,
302
303    /// Minimum latency in microseconds
304    pub min_latency_us: u64,
305
306    /// Maximum latency in microseconds
307    pub max_latency_us: u64,
308
309    /// Current entry count
310    pub entry_count: u64,
311
312    /// Current memory usage in bytes
313    pub memory_bytes: u64,
314
315    /// Number of evictions
316    pub evictions: u64,
317}
318
319/// Snapshot of all cache statistics
320#[derive(Debug, Clone)]
321pub struct CacheStatsSnapshot {
322    /// L1 cache statistics
323    pub l1: CacheStatsLevelSnapshot,
324
325    /// L2 cache statistics
326    pub l2: CacheStatsLevelSnapshot,
327
328    /// L3 cache statistics
329    pub l3: CacheStatsLevelSnapshot,
330
331    /// Total hits across all levels
332    pub total_hits: u64,
333
334    /// Total misses
335    pub total_misses: u64,
336
337    /// Total skips (due to hints)
338    pub total_skips: u64,
339
340    /// Overall hit rate percentage
341    pub hit_rate: f64,
342
343    /// Total cache puts
344    pub puts: u64,
345
346    /// Total invalidation operations
347    pub invalidations: u64,
348
349    /// Total tables invalidated
350    pub tables_invalidated: u64,
351
352    /// Total cache clears
353    pub clears: u64,
354
355    /// Requests rejected due to size limits
356    pub size_exceeded: u64,
357
358    /// Uptime in seconds
359    pub uptime_secs: u64,
360}
361
362impl CacheStatsSnapshot {
363    /// Calculate total memory usage across all levels
364    pub fn total_memory_bytes(&self) -> u64 {
365        self.l1.memory_bytes + self.l2.memory_bytes + self.l3.memory_bytes
366    }
367
368    /// Calculate total entry count across all levels
369    pub fn total_entries(&self) -> u64 {
370        self.l1.entry_count + self.l2.entry_count + self.l3.entry_count
371    }
372
373    /// Format as human-readable string
374    pub fn format(&self) -> String {
375        format!(
376            "Cache Stats:\n\
377             ├─ Hit Rate: {:.2}%\n\
378             ├─ Total Hits: {} (L1: {}, L2: {}, L3: {})\n\
379             ├─ Total Misses: {}\n\
380             ├─ Total Entries: {} ({} bytes)\n\
381             ├─ L1 Avg Latency: {}μs\n\
382             ├─ L2 Avg Latency: {}μs\n\
383             ├─ L3 Avg Latency: {}μs\n\
384             ├─ Invalidations: {} ({} tables)\n\
385             └─ Uptime: {}s",
386            self.hit_rate,
387            self.total_hits,
388            self.l1.hits,
389            self.l2.hits,
390            self.l3.hits,
391            self.total_misses,
392            self.total_entries(),
393            self.total_memory_bytes(),
394            self.l1.avg_latency_us,
395            self.l2.avg_latency_us,
396            self.l3.avg_latency_us,
397            self.invalidations,
398            self.tables_invalidated,
399            self.uptime_secs
400        )
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn test_metrics_creation() {
410        let metrics = CacheMetrics::new();
411        assert_eq!(metrics.total_hits(), 0);
412        assert_eq!(metrics.total_misses(), 0);
413    }
414
415    #[test]
416    fn test_record_hit() {
417        let metrics = CacheMetrics::new();
418
419        metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(100));
420        metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(200));
421        metrics.record_hit(CacheLevel::L2Warm, Duration::from_micros(500));
422
423        let snapshot = metrics.snapshot();
424        assert_eq!(snapshot.l1.hits, 2);
425        assert_eq!(snapshot.l2.hits, 1);
426        assert_eq!(snapshot.total_hits, 3);
427    }
428
429    #[test]
430    fn test_record_miss() {
431        let metrics = CacheMetrics::new();
432
433        metrics.record_miss(Duration::from_micros(100));
434        metrics.record_miss(Duration::from_micros(100));
435
436        assert_eq!(metrics.total_misses(), 2);
437    }
438
439    #[test]
440    fn test_hit_rate() {
441        let metrics = CacheMetrics::new();
442
443        // 3 hits, 1 miss = 75% hit rate
444        metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(100));
445        metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(100));
446        metrics.record_hit(CacheLevel::L2Warm, Duration::from_micros(100));
447        metrics.record_miss(Duration::from_micros(100));
448
449        let rate = metrics.hit_rate();
450        assert!((rate - 75.0).abs() < 0.01);
451    }
452
453    #[test]
454    fn test_latency_tracking() {
455        let metrics = CacheMetrics::new();
456
457        metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(100));
458        metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(300));
459        metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(200));
460
461        let snapshot = metrics.snapshot();
462        assert_eq!(snapshot.l1.min_latency_us, 100);
463        assert_eq!(snapshot.l1.max_latency_us, 300);
464        assert_eq!(snapshot.l1.avg_latency_us, 200); // (100+300+200)/3 = 200
465    }
466
467    #[test]
468    fn test_invalidation_tracking() {
469        let metrics = CacheMetrics::new();
470
471        metrics.record_invalidation(3);
472        metrics.record_invalidation(2);
473
474        let snapshot = metrics.snapshot();
475        assert_eq!(snapshot.invalidations, 2);
476        assert_eq!(snapshot.tables_invalidated, 5);
477    }
478
479    #[test]
480    fn test_entry_count_tracking() {
481        let metrics = CacheMetrics::new();
482
483        metrics.set_entry_count(CacheLevel::L1Hot, 100);
484        metrics.set_entry_count(CacheLevel::L2Warm, 500);
485        metrics.set_entry_count(CacheLevel::L3Semantic, 50);
486
487        let snapshot = metrics.snapshot();
488        assert_eq!(snapshot.l1.entry_count, 100);
489        assert_eq!(snapshot.l2.entry_count, 500);
490        assert_eq!(snapshot.l3.entry_count, 50);
491        assert_eq!(snapshot.total_entries(), 650);
492    }
493
494    #[test]
495    fn test_memory_tracking() {
496        let metrics = CacheMetrics::new();
497
498        metrics.set_memory_bytes(CacheLevel::L1Hot, 1024);
499        metrics.set_memory_bytes(CacheLevel::L2Warm, 1024 * 1024);
500
501        let snapshot = metrics.snapshot();
502        assert_eq!(snapshot.l1.memory_bytes, 1024);
503        assert_eq!(snapshot.l2.memory_bytes, 1024 * 1024);
504    }
505
506    #[test]
507    fn test_snapshot_format() {
508        let metrics = CacheMetrics::new();
509        metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(100));
510        metrics.record_miss(Duration::from_micros(100));
511
512        let snapshot = metrics.snapshot();
513        let formatted = snapshot.format();
514
515        assert!(formatted.contains("Hit Rate:"));
516        assert!(formatted.contains("Total Hits:"));
517    }
518
519    #[test]
520    fn test_eviction_tracking() {
521        let metrics = CacheMetrics::new();
522
523        metrics.record_eviction(CacheLevel::L1Hot);
524        metrics.record_eviction(CacheLevel::L1Hot);
525        metrics.record_eviction(CacheLevel::L2Warm);
526
527        let snapshot = metrics.snapshot();
528        assert_eq!(snapshot.l1.evictions, 2);
529        assert_eq!(snapshot.l2.evictions, 1);
530    }
531}