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