Skip to main content

subms_block_cache/features/
metrics.rs

1//! Per-instance counters: hits, misses, evictions, admissions, and
2//! shard-contention events.
3//!
4//! Wraps the base `BlockCache` and adds atomic counters around `get` /
5//! `put`. Use this when you want a single-instance summary; for system-
6//! wide histograms reach for `subms-hdr-histogram`.
7//!
8//! All counters are `AtomicU64` so the cache is `Send + Sync` and can
9//! be wrapped in `Arc` for multi-threaded summary reads even when the
10//! cache itself is single-writer.
11
12use std::hash::Hash;
13use std::sync::atomic::{AtomicU64, Ordering};
14
15use crate::BlockCache;
16
17#[derive(Debug, Default)]
18pub struct CacheMetrics {
19    pub hits: AtomicU64,
20    pub misses: AtomicU64,
21    pub evictions: AtomicU64,
22    pub admissions: AtomicU64,
23    pub contention_events: AtomicU64,
24}
25
26impl CacheMetrics {
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    pub fn hits(&self) -> u64 {
32        self.hits.load(Ordering::Relaxed)
33    }
34    pub fn misses(&self) -> u64 {
35        self.misses.load(Ordering::Relaxed)
36    }
37    pub fn evictions(&self) -> u64 {
38        self.evictions.load(Ordering::Relaxed)
39    }
40    pub fn admissions(&self) -> u64 {
41        self.admissions.load(Ordering::Relaxed)
42    }
43    pub fn contention_events(&self) -> u64 {
44        self.contention_events.load(Ordering::Relaxed)
45    }
46
47    pub fn hit_ratio(&self) -> f64 {
48        let h = self.hits() as f64;
49        let m = self.misses() as f64;
50        let total = h + m;
51        if total == 0.0 { 0.0 } else { h / total }
52    }
53
54    pub fn record_hit(&self) {
55        self.hits.fetch_add(1, Ordering::Relaxed);
56    }
57    pub fn record_miss(&self) {
58        self.misses.fetch_add(1, Ordering::Relaxed);
59    }
60    pub fn record_eviction(&self) {
61        self.evictions.fetch_add(1, Ordering::Relaxed);
62    }
63    pub fn record_admission(&self) {
64        self.admissions.fetch_add(1, Ordering::Relaxed);
65    }
66    pub fn record_contention(&self) {
67        self.contention_events.fetch_add(1, Ordering::Relaxed);
68    }
69}
70
71/// Counter-wrapped block cache.
72pub struct MetricsCache<K, V> {
73    inner: BlockCache<K, V>,
74    metrics: CacheMetrics,
75}
76
77impl<K: Hash + Eq + Clone, V> MetricsCache<K, V> {
78    pub fn with_capacity(capacity: usize) -> Self {
79        Self {
80            inner: BlockCache::with_capacity(capacity),
81            metrics: CacheMetrics::new(),
82        }
83    }
84
85    pub fn capacity(&self) -> usize {
86        self.inner.capacity()
87    }
88    pub fn len(&self) -> usize {
89        self.inner.len()
90    }
91    pub fn is_empty(&self) -> bool {
92        self.inner.is_empty()
93    }
94    pub fn metrics(&self) -> &CacheMetrics {
95        &self.metrics
96    }
97
98    pub fn get(&mut self, key: &K) -> Option<&V> {
99        let r = self.inner.get(key);
100        if r.is_some() {
101            self.metrics.record_hit();
102        } else {
103            self.metrics.record_miss();
104        }
105        r
106    }
107
108    pub fn put(&mut self, key: K, value: V) -> Option<(K, V)> {
109        let r = self.inner.put(key, value);
110        self.metrics.record_admission();
111        if r.is_some() {
112            self.metrics.record_eviction();
113        }
114        r
115    }
116
117    /// Invalidation is not eviction, so it moves no counter.
118    pub fn remove(&mut self, key: &K) -> Option<V> {
119        self.inner.remove(key)
120    }
121
122    pub fn clear(&mut self) {
123        self.inner.clear();
124    }
125}
126
127#[cfg(test)]
128#[path = "metrics_tests.rs"]
129mod tests;