cachelito_core/
stats.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2
3/// Cache statistics for monitoring hit/miss rates and performance.
4///
5/// This structure tracks cache access patterns using atomic operations for
6/// thread-safe statistics collection with minimal overhead.
7///
8/// # Thread Safety
9///
10/// All operations are thread-safe using atomic operations with `Relaxed` ordering,
11/// which provides the best performance while still maintaining consistency.
12///
13/// # Examples
14///
15/// ```
16/// use cachelito_core::CacheStats;
17///
18/// let stats = CacheStats::new();
19///
20/// // Simulate cache operations
21/// stats.record_hit();
22/// stats.record_hit();
23/// stats.record_miss();
24///
25/// assert_eq!(stats.hits(), 2);
26/// assert_eq!(stats.misses(), 1);
27/// assert_eq!(stats.total_accesses(), 3);
28/// assert!((stats.hit_rate() - 0.6666).abs() < 0.001);
29/// ```
30#[derive(Debug)]
31pub struct CacheStats {
32    hits: AtomicU64,
33    misses: AtomicU64,
34}
35
36impl CacheStats {
37    /// Creates a new `CacheStats` instance with zero counters.
38    ///
39    /// # Examples
40    ///
41    /// ```
42    /// use cachelito_core::CacheStats;
43    ///
44    /// let stats = CacheStats::new();
45    /// assert_eq!(stats.hits(), 0);
46    /// assert_eq!(stats.misses(), 0);
47    /// ```
48    pub fn new() -> Self {
49        Self {
50            hits: AtomicU64::new(0),
51            misses: AtomicU64::new(0),
52        }
53    }
54
55    /// Records a cache hit (successful lookup).
56    ///
57    /// This method is called internally when a cache lookup finds a valid entry.
58    /// Uses atomic operations for thread-safety with minimal overhead.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use cachelito_core::CacheStats;
64    ///
65    /// let stats = CacheStats::new();
66    /// stats.record_hit();
67    /// assert_eq!(stats.hits(), 1);
68    /// ```
69    #[inline]
70    pub fn record_hit(&self) {
71        self.hits.fetch_add(1, Ordering::Relaxed);
72    }
73
74    /// Records a cache miss (failed lookup).
75    ///
76    /// This method is called internally when a cache lookup doesn't find an entry
77    /// or finds an expired entry.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use cachelito_core::CacheStats;
83    ///
84    /// let stats = CacheStats::new();
85    /// stats.record_miss();
86    /// assert_eq!(stats.misses(), 1);
87    /// ```
88    #[inline]
89    pub fn record_miss(&self) {
90        self.misses.fetch_add(1, Ordering::Relaxed);
91    }
92
93    /// Returns the total number of cache hits.
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use cachelito_core::CacheStats;
99    ///
100    /// let stats = CacheStats::new();
101    /// stats.record_hit();
102    /// stats.record_hit();
103    /// assert_eq!(stats.hits(), 2);
104    /// ```
105    #[inline]
106    pub fn hits(&self) -> u64 {
107        self.hits.load(Ordering::Relaxed)
108    }
109
110    /// Returns the total number of cache misses.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use cachelito_core::CacheStats;
116    ///
117    /// let stats = CacheStats::new();
118    /// stats.record_miss();
119    /// stats.record_miss();
120    /// stats.record_miss();
121    /// assert_eq!(stats.misses(), 3);
122    /// ```
123    #[inline]
124    pub fn misses(&self) -> u64 {
125        self.misses.load(Ordering::Relaxed)
126    }
127
128    /// Returns the total number of cache accesses (hits + misses).
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use cachelito_core::CacheStats;
134    ///
135    /// let stats = CacheStats::new();
136    /// stats.record_hit();
137    /// stats.record_miss();
138    /// stats.record_hit();
139    /// assert_eq!(stats.total_accesses(), 3);
140    /// ```
141    #[inline]
142    pub fn total_accesses(&self) -> u64 {
143        self.hits() + self.misses()
144    }
145
146    /// Calculates and returns the cache hit rate as a fraction (0.0 to 1.0).
147    ///
148    /// The hit rate is the ratio of successful lookups to total lookups.
149    /// Returns 0.0 if there have been no accesses.
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use cachelito_core::CacheStats;
155    ///
156    /// let stats = CacheStats::new();
157    /// stats.record_hit();
158    /// stats.record_hit();
159    /// stats.record_miss();
160    ///
161    /// // 2 hits out of 3 total = 0.6666...
162    /// assert!((stats.hit_rate() - 0.6666).abs() < 0.001);
163    /// ```
164    #[inline]
165    pub fn hit_rate(&self) -> f64 {
166        let total = self.total_accesses();
167        if total == 0 {
168            0.0
169        } else {
170            self.hits() as f64 / total as f64
171        }
172    }
173
174    /// Calculates and returns the cache miss rate as a fraction (0.0 to 1.0).
175    ///
176    /// The miss rate is the ratio of failed lookups to total lookups.
177    /// Returns 0.0 if there have been no accesses.
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use cachelito_core::CacheStats;
183    ///
184    /// let stats = CacheStats::new();
185    /// stats.record_hit();
186    /// stats.record_miss();
187    /// stats.record_miss();
188    ///
189    /// // 2 misses out of 3 total = 0.6666...
190    /// assert!((stats.miss_rate() - 0.6666).abs() < 0.001);
191    /// ```
192    #[inline]
193    pub fn miss_rate(&self) -> f64 {
194        let total = self.total_accesses();
195        if total == 0 {
196            0.0
197        } else {
198            self.misses() as f64 / total as f64
199        }
200    }
201
202    /// Resets all statistics counters to zero.
203    ///
204    /// This can be useful for measuring statistics over specific time periods
205    /// or after configuration changes.
206    ///
207    /// # Examples
208    ///
209    /// ```
210    /// use cachelito_core::CacheStats;
211    ///
212    /// let stats = CacheStats::new();
213    /// stats.record_hit();
214    /// stats.record_miss();
215    /// assert_eq!(stats.total_accesses(), 2);
216    ///
217    /// stats.reset();
218    /// assert_eq!(stats.total_accesses(), 0);
219    /// assert_eq!(stats.hits(), 0);
220    /// assert_eq!(stats.misses(), 0);
221    /// ```
222    pub fn reset(&self) {
223        self.hits.store(0, Ordering::Relaxed);
224        self.misses.store(0, Ordering::Relaxed);
225    }
226}
227
228impl Default for CacheStats {
229    fn default() -> Self {
230        Self::new()
231    }
232}
233
234impl Clone for CacheStats {
235    fn clone(&self) -> Self {
236        Self {
237            hits: AtomicU64::new(self.hits()),
238            misses: AtomicU64::new(self.misses()),
239        }
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_new_stats() {
249        let stats = CacheStats::new();
250        assert_eq!(stats.hits(), 0);
251        assert_eq!(stats.misses(), 0);
252        assert_eq!(stats.total_accesses(), 0);
253    }
254
255    #[test]
256    fn test_record_hit() {
257        let stats = CacheStats::new();
258        stats.record_hit();
259        stats.record_hit();
260        assert_eq!(stats.hits(), 2);
261        assert_eq!(stats.misses(), 0);
262    }
263
264    #[test]
265    fn test_record_miss() {
266        let stats = CacheStats::new();
267        stats.record_miss();
268        stats.record_miss();
269        stats.record_miss();
270        assert_eq!(stats.hits(), 0);
271        assert_eq!(stats.misses(), 3);
272    }
273
274    #[test]
275    fn test_total_accesses() {
276        let stats = CacheStats::new();
277        stats.record_hit();
278        stats.record_hit();
279        stats.record_miss();
280        assert_eq!(stats.total_accesses(), 3);
281    }
282
283    #[test]
284    fn test_hit_rate() {
285        let stats = CacheStats::new();
286        stats.record_hit();
287        stats.record_hit();
288        stats.record_miss();
289        assert!((stats.hit_rate() - 0.6666).abs() < 0.001);
290    }
291
292    #[test]
293    fn test_miss_rate() {
294        let stats = CacheStats::new();
295        stats.record_hit();
296        stats.record_miss();
297        stats.record_miss();
298        assert!((stats.miss_rate() - 0.6666).abs() < 0.001);
299    }
300
301    #[test]
302    fn test_hit_rate_no_accesses() {
303        let stats = CacheStats::new();
304        assert_eq!(stats.hit_rate(), 0.0);
305        assert_eq!(stats.miss_rate(), 0.0);
306    }
307
308    #[test]
309    fn test_reset() {
310        let stats = CacheStats::new();
311        stats.record_hit();
312        stats.record_hit();
313        stats.record_miss();
314        assert_eq!(stats.total_accesses(), 3);
315
316        stats.reset();
317        assert_eq!(stats.hits(), 0);
318        assert_eq!(stats.misses(), 0);
319        assert_eq!(stats.total_accesses(), 0);
320    }
321
322    #[test]
323    fn test_default() {
324        let stats = CacheStats::default();
325        assert_eq!(stats.hits(), 0);
326        assert_eq!(stats.misses(), 0);
327    }
328
329    #[test]
330    fn test_clone() {
331        let stats = CacheStats::new();
332        stats.record_hit();
333        stats.record_miss();
334
335        let cloned = stats.clone();
336        assert_eq!(cloned.hits(), stats.hits());
337        assert_eq!(cloned.misses(), stats.misses());
338
339        // Ensure they're independent
340        stats.record_hit();
341        assert_eq!(stats.hits(), 2);
342        assert_eq!(cloned.hits(), 1);
343    }
344
345    #[test]
346    fn test_concurrent_access() {
347        use std::sync::Arc;
348        use std::thread;
349
350        let stats = Arc::new(CacheStats::new());
351        let mut handles = vec![];
352
353        // Spawn 10 threads that each record 100 hits and 50 misses
354        for _ in 0..10 {
355            let stats_clone = Arc::clone(&stats);
356            let handle = thread::spawn(move || {
357                for _ in 0..100 {
358                    stats_clone.record_hit();
359                }
360                for _ in 0..50 {
361                    stats_clone.record_miss();
362                }
363            });
364            handles.push(handle);
365        }
366
367        // Wait for all threads to finish
368        for handle in handles {
369            handle.join().unwrap();
370        }
371
372        // Verify totals: 10 threads * 100 hits = 1000, 10 threads * 50 misses = 500
373        assert_eq!(stats.hits(), 1000);
374        assert_eq!(stats.misses(), 500);
375        assert_eq!(stats.total_accesses(), 1500);
376        assert!((stats.hit_rate() - 0.6666).abs() < 0.001);
377    }
378}