oximedia_cache/
cache_metrics.rs1use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::Arc;
8
9pub struct CacheMetrics {
26 hits: AtomicU64,
28 misses: AtomicU64,
30 evictions: AtomicU64,
32 total_hit_latency_ns: AtomicU64,
34 total_miss_latency_ns: AtomicU64,
36 latency_samples: AtomicU64,
38}
39
40impl std::fmt::Debug for CacheMetrics {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("CacheMetrics")
43 .field("hits", &self.hits.load(Ordering::Relaxed))
44 .field("misses", &self.misses.load(Ordering::Relaxed))
45 .field("evictions", &self.evictions.load(Ordering::Relaxed))
46 .finish()
47 }
48}
49
50impl Default for CacheMetrics {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56impl CacheMetrics {
57 pub fn new() -> Self {
59 Self {
60 hits: AtomicU64::new(0),
61 misses: AtomicU64::new(0),
62 evictions: AtomicU64::new(0),
63 total_hit_latency_ns: AtomicU64::new(0),
64 total_miss_latency_ns: AtomicU64::new(0),
65 latency_samples: AtomicU64::new(0),
66 }
67 }
68
69 pub fn new_shared() -> Arc<Self> {
71 Arc::new(Self::new())
72 }
73
74 pub fn record_hit(&self, latency_ns: u64) {
79 self.hits.fetch_add(1, Ordering::Relaxed);
80 self.total_hit_latency_ns
81 .fetch_add(latency_ns, Ordering::Relaxed);
82 self.latency_samples.fetch_add(1, Ordering::Relaxed);
83 }
84
85 pub fn record_miss(&self, latency_ns: u64) {
90 self.misses.fetch_add(1, Ordering::Relaxed);
91 self.total_miss_latency_ns
92 .fetch_add(latency_ns, Ordering::Relaxed);
93 self.latency_samples.fetch_add(1, Ordering::Relaxed);
94 }
95
96 pub fn record_eviction(&self) {
98 self.evictions.fetch_add(1, Ordering::Relaxed);
99 }
100
101 pub fn hit_rate(&self) -> f64 {
105 let h = self.hits.load(Ordering::Relaxed);
106 let m = self.misses.load(Ordering::Relaxed);
107 let total = h + m;
108 if total == 0 {
109 0.0
110 } else {
111 h as f64 / total as f64
112 }
113 }
114
115 pub fn miss_rate(&self) -> f64 {
119 let h = self.hits.load(Ordering::Relaxed);
120 let m = self.misses.load(Ordering::Relaxed);
121 let total = h + m;
122 if total == 0 {
123 0.0
124 } else {
125 m as f64 / total as f64
126 }
127 }
128
129 pub fn avg_latency_ns(&self) -> f64 {
134 let samples = self.latency_samples.load(Ordering::Relaxed);
135 if samples == 0 {
136 return 0.0;
137 }
138 let total_lat = self.total_hit_latency_ns.load(Ordering::Relaxed)
139 + self.total_miss_latency_ns.load(Ordering::Relaxed);
140 total_lat as f64 / samples as f64
141 }
142
143 pub fn total_hits(&self) -> u64 {
145 self.hits.load(Ordering::Relaxed)
146 }
147
148 pub fn total_misses(&self) -> u64 {
150 self.misses.load(Ordering::Relaxed)
151 }
152
153 pub fn total_evictions(&self) -> u64 {
155 self.evictions.load(Ordering::Relaxed)
156 }
157
158 pub fn eviction_rate(&self) -> f64 {
162 let h = self.hits.load(Ordering::Relaxed);
163 let m = self.misses.load(Ordering::Relaxed);
164 let evictions = self.evictions.load(Ordering::Relaxed);
165 let total = h + m;
166 if total == 0 {
167 0.0
168 } else {
169 evictions as f64 / total as f64
170 }
171 }
172
173 pub fn reset(&self) {
175 self.hits.store(0, Ordering::Relaxed);
176 self.misses.store(0, Ordering::Relaxed);
177 self.evictions.store(0, Ordering::Relaxed);
178 self.total_hit_latency_ns.store(0, Ordering::Relaxed);
179 self.total_miss_latency_ns.store(0, Ordering::Relaxed);
180 self.latency_samples.store(0, Ordering::Relaxed);
181 }
182
183 pub fn snapshot(&self) -> CacheMetricsSnapshot {
185 let total_hits = self.hits.load(Ordering::Relaxed);
186 let total_misses = self.misses.load(Ordering::Relaxed);
187 let total_evictions = self.evictions.load(Ordering::Relaxed);
188 let total_lat = self.total_hit_latency_ns.load(Ordering::Relaxed)
189 + self.total_miss_latency_ns.load(Ordering::Relaxed);
190 let samples = self.latency_samples.load(Ordering::Relaxed);
191
192 let total_ops = total_hits + total_misses;
193 let hit_rate = if total_ops == 0 {
194 0.0
195 } else {
196 total_hits as f64 / total_ops as f64
197 };
198 let miss_rate = if total_ops == 0 {
199 0.0
200 } else {
201 total_misses as f64 / total_ops as f64
202 };
203 let avg_latency_ns = if samples == 0 {
204 0.0
205 } else {
206 total_lat as f64 / samples as f64
207 };
208 let eviction_rate = if total_ops == 0 {
209 0.0
210 } else {
211 total_evictions as f64 / total_ops as f64
212 };
213
214 CacheMetricsSnapshot {
215 hit_rate,
216 miss_rate,
217 total_hits,
218 total_misses,
219 total_evictions,
220 eviction_rate,
221 avg_latency_ns,
222 }
223 }
224}
225
226#[derive(Debug, Clone)]
232pub struct CacheMetricsSnapshot {
233 pub hit_rate: f64,
235 pub miss_rate: f64,
237 pub total_hits: u64,
239 pub total_misses: u64,
241 pub total_evictions: u64,
243 pub eviction_rate: f64,
245 pub avg_latency_ns: f64,
247}
248
249impl CacheMetricsSnapshot {
250 pub fn is_hit_rate_above(&self, threshold: f64) -> bool {
252 self.hit_rate > threshold
253 }
254
255 pub fn total_ops(&self) -> u64 {
257 self.total_hits + self.total_misses
258 }
259}
260
261#[cfg(test)]
264mod tests {
265 use super::*;
266 use std::sync::Arc;
267 use std::thread;
268
269 #[test]
271 fn test_new_metrics_zeroed() {
272 let m = CacheMetrics::new();
273 assert_eq!(m.total_hits(), 0);
274 assert_eq!(m.total_misses(), 0);
275 assert_eq!(m.total_evictions(), 0);
276 assert_eq!(m.hit_rate(), 0.0);
277 assert_eq!(m.miss_rate(), 0.0);
278 assert_eq!(m.avg_latency_ns(), 0.0);
279 }
280
281 #[test]
283 fn test_record_hit() {
284 let m = CacheMetrics::new();
285 m.record_hit(100);
286 m.record_hit(200);
287 assert_eq!(m.total_hits(), 2);
288 assert_eq!(m.total_misses(), 0);
289 }
290
291 #[test]
293 fn test_record_miss() {
294 let m = CacheMetrics::new();
295 m.record_miss(500);
296 assert_eq!(m.total_misses(), 1);
297 assert_eq!(m.total_hits(), 0);
298 }
299
300 #[test]
302 fn test_record_eviction() {
303 let m = CacheMetrics::new();
304 m.record_eviction();
305 m.record_eviction();
306 m.record_eviction();
307 assert_eq!(m.total_evictions(), 3);
308 }
309
310 #[test]
312 fn test_hit_rate_equal() {
313 let m = CacheMetrics::new();
314 for _ in 0..50 {
315 m.record_hit(10);
316 }
317 for _ in 0..50 {
318 m.record_miss(10);
319 }
320 assert!((m.hit_rate() - 0.5).abs() < 1e-9);
321 }
322
323 #[test]
325 fn test_miss_rate_complement() {
326 let m = CacheMetrics::new();
327 m.record_hit(10);
328 m.record_hit(10);
329 m.record_hit(10);
330 m.record_miss(10);
331 let hr = m.hit_rate();
332 let mr = m.miss_rate();
333 assert!((hr + mr - 1.0).abs() < 1e-9, "hit+miss should equal 1.0");
334 }
335
336 #[test]
338 fn test_avg_latency_ns() {
339 let m = CacheMetrics::new();
340 m.record_hit(100);
341 m.record_hit(300);
342 m.record_miss(200);
343 let avg = m.avg_latency_ns();
345 assert!((avg - 200.0).abs() < 1e-9, "expected 200ns avg, got {avg}");
346 }
347
348 #[test]
350 fn test_snapshot_consistency() {
351 let m = CacheMetrics::new();
352 for i in 0u64..10 {
353 m.record_hit(i * 10);
354 }
355 for _ in 0..5 {
356 m.record_miss(50);
357 }
358 m.record_eviction();
359 let s = m.snapshot();
360 assert_eq!(s.total_hits, 10);
361 assert_eq!(s.total_misses, 5);
362 assert_eq!(s.total_evictions, 1);
363 assert!((s.hit_rate - 10.0 / 15.0).abs() < 1e-9);
364 assert!((s.miss_rate - 5.0 / 15.0).abs() < 1e-9);
365 assert!((s.hit_rate + s.miss_rate - 1.0).abs() < 1e-9);
366 }
367
368 #[test]
370 fn test_eviction_rate() {
371 let m = CacheMetrics::new();
372 m.record_hit(10);
373 m.record_miss(10);
374 m.record_eviction();
375 assert!((m.eviction_rate() - 0.5).abs() < 1e-9);
377 }
378
379 #[test]
381 fn test_reset() {
382 let m = CacheMetrics::new();
383 m.record_hit(1000);
384 m.record_miss(2000);
385 m.record_eviction();
386 m.reset();
387 assert_eq!(m.total_hits(), 0);
388 assert_eq!(m.total_misses(), 0);
389 assert_eq!(m.total_evictions(), 0);
390 assert_eq!(m.avg_latency_ns(), 0.0);
391 assert_eq!(m.hit_rate(), 0.0);
392 }
393
394 #[test]
396 fn test_snapshot_is_hit_rate_above() {
397 let m = CacheMetrics::new();
398 for _ in 0..90 {
399 m.record_hit(10);
400 }
401 for _ in 0..10 {
402 m.record_miss(10);
403 }
404 let s = m.snapshot();
405 assert!(s.is_hit_rate_above(0.80));
406 assert!(!s.is_hit_rate_above(0.95));
407 }
408
409 #[test]
411 fn test_snapshot_total_ops() {
412 let m = CacheMetrics::new();
413 m.record_hit(1);
414 m.record_hit(1);
415 m.record_miss(1);
416 let s = m.snapshot();
417 assert_eq!(s.total_ops(), 3);
418 }
419
420 #[test]
422 fn test_concurrent_recording() {
423 let m = Arc::new(CacheMetrics::new());
424 let threads: Vec<_> = (0..8)
425 .map(|_| {
426 let m2 = Arc::clone(&m);
427 thread::spawn(move || {
428 for _ in 0..1000 {
429 m2.record_hit(50);
430 m2.record_miss(100);
431 m2.record_eviction();
432 }
433 })
434 })
435 .collect();
436 for t in threads {
437 t.join().expect("thread panicked");
438 }
439 assert_eq!(m.total_hits(), 8 * 1000);
440 assert_eq!(m.total_misses(), 8 * 1000);
441 assert_eq!(m.total_evictions(), 8 * 1000);
442 }
443
444 #[test]
446 fn test_zero_ops_rates() {
447 let m = CacheMetrics::new();
448 assert_eq!(m.hit_rate(), 0.0);
449 assert_eq!(m.miss_rate(), 0.0);
450 assert_eq!(m.eviction_rate(), 0.0);
451 }
452
453 #[test]
455 fn test_new_shared() {
456 let m = CacheMetrics::new_shared();
457 m.record_hit(1);
458 assert_eq!(m.total_hits(), 1);
459 }
460}