Skip to main content

oxistore_cache/
stats.rs

1//! Cache hit/miss statistics.
2//!
3//! [`CacheStats`] provides atomic counters for cache hits and misses.
4//! [`StatsCache`] wraps any `Cache<Vec<u8>, Vec<u8>>` and records access
5//! outcomes automatically.
6
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9
10use crate::Cache;
11
12/// Atomic hit/miss counters for a cache.
13///
14/// All counters use `Relaxed` ordering — precise ordering guarantees across
15/// threads are not required for statistics (approximate values are fine).
16#[derive(Debug, Default)]
17pub struct CacheStats {
18    hits: AtomicU64,
19    misses: AtomicU64,
20}
21
22impl CacheStats {
23    /// Create a fresh `CacheStats` with all counters at zero.
24    #[must_use]
25    pub fn new() -> Self {
26        CacheStats::default()
27    }
28
29    /// Record a single cache hit.
30    pub fn record_hit(&self) {
31        self.hits.fetch_add(1, Ordering::Relaxed);
32    }
33
34    /// Record a single cache miss.
35    pub fn record_miss(&self) {
36        self.misses.fetch_add(1, Ordering::Relaxed);
37    }
38
39    /// Return the total number of recorded hits.
40    #[must_use]
41    pub fn hits(&self) -> u64 {
42        self.hits.load(Ordering::Relaxed)
43    }
44
45    /// Return the total number of recorded misses.
46    #[must_use]
47    pub fn misses(&self) -> u64 {
48        self.misses.load(Ordering::Relaxed)
49    }
50
51    /// Return the hit rate as a fraction in `[0.0, 1.0]`.
52    ///
53    /// Returns `0.0` if no accesses have been recorded yet.
54    #[must_use]
55    pub fn hit_rate(&self) -> f64 {
56        let h = self.hits();
57        let m = self.misses();
58        let total = h + m;
59        if total == 0 {
60            0.0
61        } else {
62            h as f64 / total as f64
63        }
64    }
65
66    /// Reset all counters to zero.
67    pub fn reset(&self) {
68        self.hits.store(0, Ordering::Relaxed);
69        self.misses.store(0, Ordering::Relaxed);
70    }
71}
72
73/// A cache wrapper that records hits and misses via [`CacheStats`].
74///
75/// On each `get` call:
76/// - If the inner cache returns `Some(v)` → hit recorded.
77/// - If the inner cache returns `None`    → miss recorded.
78///
79/// # Type parameters
80///
81/// - `C`: inner `Cache<Vec<u8>, Vec<u8>>` implementation.
82pub struct StatsCache<C> {
83    inner: C,
84    stats: Arc<CacheStats>,
85}
86
87impl<C> StatsCache<C>
88where
89    C: Cache<Vec<u8>, Vec<u8>>,
90{
91    /// Wrap `inner` with a freshly created `CacheStats`.
92    pub fn new(inner: C) -> Self {
93        StatsCache {
94            inner,
95            stats: Arc::new(CacheStats::new()),
96        }
97    }
98
99    /// Wrap `inner` with a shared `CacheStats` (useful for sharing across wrappers).
100    pub fn with_stats(inner: C, stats: Arc<CacheStats>) -> Self {
101        StatsCache { inner, stats }
102    }
103
104    /// Return a reference to the underlying stats.
105    #[must_use]
106    pub fn stats(&self) -> &Arc<CacheStats> {
107        &self.stats
108    }
109}
110
111impl<C> Cache<Vec<u8>, Vec<u8>> for StatsCache<C>
112where
113    C: Cache<Vec<u8>, Vec<u8>>,
114{
115    fn get(&mut self, key: &Vec<u8>) -> Option<&Vec<u8>> {
116        let result = self.inner.get(key);
117        if result.is_some() {
118            self.stats.record_hit();
119        } else {
120            self.stats.record_miss();
121        }
122        result
123    }
124
125    fn put(&mut self, key: Vec<u8>, value: Vec<u8>) -> Option<Vec<u8>> {
126        self.inner.put(key, value)
127    }
128
129    fn put_with_ttl(
130        &mut self,
131        key: Vec<u8>,
132        value: Vec<u8>,
133        ttl: std::time::Duration,
134    ) -> Option<Vec<u8>> {
135        self.inner.put_with_ttl(key, value, ttl)
136    }
137
138    fn len(&self) -> usize {
139        self.inner.len()
140    }
141
142    fn cap(&self) -> usize {
143        self.inner.cap()
144    }
145
146    fn remove(&mut self, key: &Vec<u8>) -> Option<Vec<u8>> {
147        self.inner.remove(key)
148    }
149
150    fn clear(&mut self) {
151        self.inner.clear();
152    }
153
154    fn peek(&self, key: &Vec<u8>) -> Option<&Vec<u8>> {
155        self.inner.peek(key)
156    }
157
158    fn contains_key(&self, key: &Vec<u8>) -> bool {
159        self.inner.contains_key(key)
160    }
161
162    fn resize(&mut self, new_cap: usize) {
163        self.inner.resize(new_cap);
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::LruCache;
171
172    #[test]
173    fn stats_initial_state() {
174        let stats = CacheStats::new();
175        assert_eq!(stats.hits(), 0);
176        assert_eq!(stats.misses(), 0);
177        assert!((stats.hit_rate() - 0.0).abs() < f64::EPSILON);
178    }
179
180    #[test]
181    fn stats_record_hit_miss() {
182        let stats = CacheStats::new();
183        stats.record_hit();
184        stats.record_hit();
185        stats.record_miss();
186        assert_eq!(stats.hits(), 2);
187        assert_eq!(stats.misses(), 1);
188        let rate = stats.hit_rate();
189        assert!((rate - 2.0 / 3.0).abs() < 1e-10);
190    }
191
192    #[test]
193    fn stats_reset() {
194        let stats = CacheStats::new();
195        stats.record_hit();
196        stats.record_miss();
197        stats.reset();
198        assert_eq!(stats.hits(), 0);
199        assert_eq!(stats.misses(), 0);
200    }
201
202    #[test]
203    fn stats_cache_hit_and_miss() {
204        let inner = LruCache::<Vec<u8>, Vec<u8>>::new(4);
205        let mut cache = StatsCache::new(inner);
206
207        cache.put(b"hello".to_vec(), b"world".to_vec());
208
209        // Hit
210        let v = cache.get(&b"hello".to_vec());
211        assert_eq!(v, Some(&b"world".to_vec()));
212        assert_eq!(cache.stats().hits(), 1);
213        assert_eq!(cache.stats().misses(), 0);
214
215        // Miss
216        let v = cache.get(&b"missing".to_vec());
217        assert!(v.is_none());
218        assert_eq!(cache.stats().hits(), 1);
219        assert_eq!(cache.stats().misses(), 1);
220
221        let rate = cache.stats().hit_rate();
222        assert!((rate - 0.5).abs() < 1e-10);
223    }
224
225    #[test]
226    fn stats_cache_delegates_put_remove_clear() {
227        let inner = LruCache::<Vec<u8>, Vec<u8>>::new(4);
228        let mut cache = StatsCache::new(inner);
229
230        cache.put(b"k".to_vec(), b"v".to_vec());
231        assert_eq!(cache.len(), 1);
232
233        cache.remove(&b"k".to_vec());
234        assert_eq!(cache.len(), 0);
235
236        cache.put(b"a".to_vec(), b"1".to_vec());
237        cache.put(b"b".to_vec(), b"2".to_vec());
238        cache.clear();
239        assert!(cache.is_empty());
240    }
241}