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}