Skip to main content

rust_serv/memory_cache/
stats.rs

1//! Cache statistics for monitoring
2
3use std::sync::atomic::{AtomicU64, Ordering};
4
5/// Cache statistics
6#[derive(Debug, Default)]
7pub struct CacheStats {
8    /// Number of cache hits
9    hits: AtomicU64,
10    /// Number of cache misses
11    misses: AtomicU64,
12    /// Number of cache evictions
13    evictions: AtomicU64,
14    /// Number of cache entries
15    entries: AtomicU64,
16    /// Total size of cached data in bytes
17    total_size: AtomicU64,
18    /// Number of expired entries removed
19    expired: AtomicU64,
20}
21
22impl CacheStats {
23    /// Create new cache statistics
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    /// Record a cache hit
29    pub fn record_hit(&self) {
30        self.hits.fetch_add(1, Ordering::Relaxed);
31    }
32
33    /// Record a cache miss
34    pub fn record_miss(&self) {
35        self.misses.fetch_add(1, Ordering::Relaxed);
36    }
37
38    /// Record a cache eviction
39    pub fn record_eviction(&self) {
40        self.evictions.fetch_add(1, Ordering::Relaxed);
41    }
42
43    /// Record an expired entry removal
44    pub fn record_expired(&self) {
45        self.expired.fetch_add(1, Ordering::Relaxed);
46    }
47
48    /// Add to entry count
49    pub fn add_entry(&self, size: usize) {
50        self.entries.fetch_add(1, Ordering::Relaxed);
51        self.total_size.fetch_add(size as u64, Ordering::Relaxed);
52    }
53
54    /// Remove from entry count
55    pub fn remove_entry(&self, size: usize) {
56        self.entries.fetch_sub(1, Ordering::Relaxed);
57        self.total_size.fetch_sub(size as u64, Ordering::Relaxed);
58    }
59
60    /// Update size (delta can be positive or negative)
61    pub fn update_size(&self, delta: i64) {
62        if delta >= 0 {
63            self.total_size.fetch_add(delta as u64, Ordering::Relaxed);
64        } else {
65            self.total_size.fetch_sub((-delta) as u64, Ordering::Relaxed);
66        }
67    }
68
69    /// Get cache hits
70    pub fn hits(&self) -> u64 {
71        self.hits.load(Ordering::Relaxed)
72    }
73
74    /// Get cache misses
75    pub fn misses(&self) -> u64 {
76        self.misses.load(Ordering::Relaxed)
77    }
78
79    /// Get cache evictions
80    pub fn evictions(&self) -> u64 {
81        self.evictions.load(Ordering::Relaxed)
82    }
83
84    /// Get number of entries
85    pub fn entries(&self) -> u64 {
86        self.entries.load(Ordering::Relaxed)
87    }
88
89    /// Get total size in bytes
90    pub fn total_size(&self) -> u64 {
91        self.total_size.load(Ordering::Relaxed)
92    }
93
94    /// Get expired count
95    pub fn expired(&self) -> u64 {
96        self.expired.load(Ordering::Relaxed)
97    }
98
99    /// Calculate hit rate (0.0 to 1.0)
100    pub fn hit_rate(&self) -> f64 {
101        let hits = self.hits();
102        let misses = self.misses();
103        let total = hits + misses;
104        if total == 0 {
105            0.0
106        } else {
107            hits as f64 / total as f64
108        }
109    }
110
111    /// Get total requests
112    pub fn total_requests(&self) -> u64 {
113        self.hits() + self.misses()
114    }
115
116    /// Reset statistics
117    pub fn reset(&self) {
118        self.hits.store(0, Ordering::Relaxed);
119        self.misses.store(0, Ordering::Relaxed);
120        self.evictions.store(0, Ordering::Relaxed);
121        self.entries.store(0, Ordering::Relaxed);
122        self.total_size.store(0, Ordering::Relaxed);
123        self.expired.store(0, Ordering::Relaxed);
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_cache_stats_creation() {
133        let stats = CacheStats::new();
134        assert_eq!(stats.hits(), 0);
135        assert_eq!(stats.misses(), 0);
136        assert_eq!(stats.evictions(), 0);
137        assert_eq!(stats.entries(), 0);
138        assert_eq!(stats.total_size(), 0);
139    }
140
141    #[test]
142    fn test_record_hit() {
143        let stats = CacheStats::new();
144        stats.record_hit();
145        stats.record_hit();
146        stats.record_hit();
147        assert_eq!(stats.hits(), 3);
148    }
149
150    #[test]
151    fn test_record_miss() {
152        let stats = CacheStats::new();
153        stats.record_miss();
154        stats.record_miss();
155        assert_eq!(stats.misses(), 2);
156    }
157
158    #[test]
159    fn test_record_eviction() {
160        let stats = CacheStats::new();
161        stats.record_eviction();
162        assert_eq!(stats.evictions(), 1);
163    }
164
165    #[test]
166    fn test_record_expired() {
167        let stats = CacheStats::new();
168        stats.record_expired();
169        stats.record_expired();
170        assert_eq!(stats.expired(), 2);
171    }
172
173    #[test]
174    fn test_add_remove_entry() {
175        let stats = CacheStats::new();
176        
177        stats.add_entry(100);
178        assert_eq!(stats.entries(), 1);
179        assert_eq!(stats.total_size(), 100);
180        
181        stats.add_entry(200);
182        assert_eq!(stats.entries(), 2);
183        assert_eq!(stats.total_size(), 300);
184        
185        stats.remove_entry(100);
186        assert_eq!(stats.entries(), 1);
187        assert_eq!(stats.total_size(), 200);
188    }
189
190    #[test]
191    fn test_update_size_positive() {
192        let stats = CacheStats::new();
193        stats.update_size(100);
194        assert_eq!(stats.total_size(), 100);
195    }
196
197    #[test]
198    fn test_update_size_negative() {
199        let stats = CacheStats::new();
200        stats.add_entry(200);
201        stats.update_size(-50);
202        assert_eq!(stats.total_size(), 150);
203    }
204
205    #[test]
206    fn test_hit_rate_no_requests() {
207        let stats = CacheStats::new();
208        assert_eq!(stats.hit_rate(), 0.0);
209    }
210
211    #[test]
212    fn test_hit_rate_all_hits() {
213        let stats = CacheStats::new();
214        stats.record_hit();
215        stats.record_hit();
216        assert_eq!(stats.hit_rate(), 1.0);
217    }
218
219    #[test]
220    fn test_hit_rate_all_misses() {
221        let stats = CacheStats::new();
222        stats.record_miss();
223        stats.record_miss();
224        assert_eq!(stats.hit_rate(), 0.0);
225    }
226
227    #[test]
228    fn test_hit_rate_mixed() {
229        let stats = CacheStats::new();
230        stats.record_hit();
231        stats.record_hit();
232        stats.record_miss();
233        stats.record_miss();
234        // 2 hits, 2 misses = 50% hit rate
235        assert!((stats.hit_rate() - 0.5).abs() < 0.001);
236    }
237
238    #[test]
239    fn test_total_requests() {
240        let stats = CacheStats::new();
241        stats.record_hit();
242        stats.record_hit();
243        stats.record_miss();
244        assert_eq!(stats.total_requests(), 3);
245    }
246
247    #[test]
248    fn test_reset() {
249        let stats = CacheStats::new();
250        stats.record_hit();
251        stats.record_miss();
252        stats.add_entry(100);
253        stats.record_eviction();
254        
255        stats.reset();
256        
257        assert_eq!(stats.hits(), 0);
258        assert_eq!(stats.misses(), 0);
259        assert_eq!(stats.entries(), 0);
260        assert_eq!(stats.total_size(), 0);
261        assert_eq!(stats.evictions(), 0);
262    }
263
264    #[test]
265    fn test_concurrent_access() {
266        use std::sync::Arc;
267        use std::thread;
268
269        let stats = Arc::new(CacheStats::new());
270        let mut handles = vec![];
271
272        for _ in 0..10 {
273            let stats_clone = Arc::clone(&stats);
274            handles.push(thread::spawn(move || {
275                stats_clone.record_hit();
276                stats_clone.record_miss();
277            }));
278        }
279
280        for handle in handles {
281            handle.join().unwrap();
282        }
283
284        assert_eq!(stats.hits(), 10);
285        assert_eq!(stats.misses(), 10);
286    }
287}