1use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::{Duration, Instant};
8
9use super::CacheLevel;
10
11#[derive(Debug)]
13pub struct CacheMetrics {
14 l1: CacheStats,
16
17 l2: CacheStats,
19
20 l3: CacheStats,
22
23 misses: AtomicU64,
25
26 skips: AtomicU64,
28
29 puts: AtomicU64,
31
32 invalidations: AtomicU64,
34
35 tables_invalidated: AtomicU64,
37
38 clears: AtomicU64,
40
41 size_exceeded: AtomicU64,
43
44 created_at: Instant,
46}
47
48#[derive(Debug, Default)]
50pub struct CacheStats {
51 hits: AtomicU64,
53
54 total_latency_us: AtomicU64,
56
57 min_latency_us: AtomicU64,
59
60 max_latency_us: AtomicU64,
62
63 entry_count: AtomicU64,
65
66 memory_bytes: AtomicU64,
68
69 evictions: AtomicU64,
71}
72
73impl CacheStats {
74 fn new() -> Self {
75 Self {
76 hits: AtomicU64::new(0),
77 total_latency_us: AtomicU64::new(0),
78 min_latency_us: AtomicU64::new(u64::MAX),
79 max_latency_us: AtomicU64::new(0),
80 entry_count: AtomicU64::new(0),
81 memory_bytes: AtomicU64::new(0),
82 evictions: AtomicU64::new(0),
83 }
84 }
85
86 fn record_hit(&self, latency: Duration) {
87 self.hits.fetch_add(1, Ordering::Relaxed);
88
89 let latency_us = latency.as_micros() as u64;
90 self.total_latency_us
91 .fetch_add(latency_us, Ordering::Relaxed);
92
93 let mut current = self.min_latency_us.load(Ordering::Relaxed);
95 while latency_us < current {
96 match self.min_latency_us.compare_exchange_weak(
97 current,
98 latency_us,
99 Ordering::Relaxed,
100 Ordering::Relaxed,
101 ) {
102 Ok(_) => break,
103 Err(c) => current = c,
104 }
105 }
106
107 let mut current = self.max_latency_us.load(Ordering::Relaxed);
109 while latency_us > current {
110 match self.max_latency_us.compare_exchange_weak(
111 current,
112 latency_us,
113 Ordering::Relaxed,
114 Ordering::Relaxed,
115 ) {
116 Ok(_) => break,
117 Err(c) => current = c,
118 }
119 }
120 }
121
122 fn snapshot(&self) -> CacheStatsLevelSnapshot {
123 let hits = self.hits.load(Ordering::Relaxed);
124 let total_latency = self.total_latency_us.load(Ordering::Relaxed);
125 let min_latency = self.min_latency_us.load(Ordering::Relaxed);
126 let max_latency = self.max_latency_us.load(Ordering::Relaxed);
127
128 CacheStatsLevelSnapshot {
129 hits,
130 avg_latency_us: total_latency.checked_div(hits).unwrap_or(0),
131 min_latency_us: if min_latency == u64::MAX {
132 0
133 } else {
134 min_latency
135 },
136 max_latency_us: max_latency,
137 entry_count: self.entry_count.load(Ordering::Relaxed),
138 memory_bytes: self.memory_bytes.load(Ordering::Relaxed),
139 evictions: self.evictions.load(Ordering::Relaxed),
140 }
141 }
142}
143
144impl CacheMetrics {
145 pub fn new() -> Self {
147 Self {
148 l1: CacheStats::new(),
149 l2: CacheStats::new(),
150 l3: CacheStats::new(),
151 misses: AtomicU64::new(0),
152 skips: AtomicU64::new(0),
153 puts: AtomicU64::new(0),
154 invalidations: AtomicU64::new(0),
155 tables_invalidated: AtomicU64::new(0),
156 clears: AtomicU64::new(0),
157 size_exceeded: AtomicU64::new(0),
158 created_at: Instant::now(),
159 }
160 }
161
162 pub fn record_hit(&self, level: CacheLevel, latency: Duration) {
164 match level {
165 CacheLevel::L1Hot => self.l1.record_hit(latency),
166 CacheLevel::L2Warm => self.l2.record_hit(latency),
167 CacheLevel::L3Semantic => self.l3.record_hit(latency),
168 }
169 }
170
171 pub fn record_miss(&self, _latency: Duration) {
173 self.misses.fetch_add(1, Ordering::Relaxed);
174 }
175
176 pub fn record_skip(&self) {
178 self.skips.fetch_add(1, Ordering::Relaxed);
179 }
180
181 pub fn record_put(&self) {
183 self.puts.fetch_add(1, Ordering::Relaxed);
184 }
185
186 pub fn record_invalidation(&self, table_count: usize) {
188 self.invalidations.fetch_add(1, Ordering::Relaxed);
189 self.tables_invalidated
190 .fetch_add(table_count as u64, Ordering::Relaxed);
191 }
192
193 pub fn record_clear(&self) {
195 self.clears.fetch_add(1, Ordering::Relaxed);
196 }
197
198 pub fn record_size_exceeded(&self) {
200 self.size_exceeded.fetch_add(1, Ordering::Relaxed);
201 }
202
203 pub fn record_eviction(&self, level: CacheLevel) {
205 match level {
206 CacheLevel::L1Hot => self.l1.evictions.fetch_add(1, Ordering::Relaxed),
207 CacheLevel::L2Warm => self.l2.evictions.fetch_add(1, Ordering::Relaxed),
208 CacheLevel::L3Semantic => self.l3.evictions.fetch_add(1, Ordering::Relaxed),
209 };
210 }
211
212 pub fn set_entry_count(&self, level: CacheLevel, count: u64) {
214 match level {
215 CacheLevel::L1Hot => self.l1.entry_count.store(count, Ordering::Relaxed),
216 CacheLevel::L2Warm => self.l2.entry_count.store(count, Ordering::Relaxed),
217 CacheLevel::L3Semantic => self.l3.entry_count.store(count, Ordering::Relaxed),
218 }
219 }
220
221 pub fn set_memory_bytes(&self, level: CacheLevel, bytes: u64) {
223 match level {
224 CacheLevel::L1Hot => self.l1.memory_bytes.store(bytes, Ordering::Relaxed),
225 CacheLevel::L2Warm => self.l2.memory_bytes.store(bytes, Ordering::Relaxed),
226 CacheLevel::L3Semantic => self.l3.memory_bytes.store(bytes, Ordering::Relaxed),
227 }
228 }
229
230 pub fn snapshot(&self) -> CacheStatsSnapshot {
232 let l1 = self.l1.snapshot();
233 let l2 = self.l2.snapshot();
234 let l3 = self.l3.snapshot();
235 let misses = self.misses.load(Ordering::Relaxed);
236 let skips = self.skips.load(Ordering::Relaxed);
237
238 let total_hits = l1.hits + l2.hits + l3.hits;
239 let total_requests = total_hits + misses;
240
241 CacheStatsSnapshot {
242 l1,
243 l2,
244 l3,
245 total_hits,
246 total_misses: misses,
247 total_skips: skips,
248 hit_rate: if total_requests > 0 {
249 (total_hits as f64 / total_requests as f64) * 100.0
250 } else {
251 0.0
252 },
253 puts: self.puts.load(Ordering::Relaxed),
254 invalidations: self.invalidations.load(Ordering::Relaxed),
255 tables_invalidated: self.tables_invalidated.load(Ordering::Relaxed),
256 clears: self.clears.load(Ordering::Relaxed),
257 size_exceeded: self.size_exceeded.load(Ordering::Relaxed),
258 uptime_secs: self.created_at.elapsed().as_secs(),
259 }
260 }
261
262 pub fn total_hits(&self) -> u64 {
264 self.l1.hits.load(Ordering::Relaxed)
265 + self.l2.hits.load(Ordering::Relaxed)
266 + self.l3.hits.load(Ordering::Relaxed)
267 }
268
269 pub fn total_misses(&self) -> u64 {
271 self.misses.load(Ordering::Relaxed)
272 }
273
274 pub fn hit_rate(&self) -> f64 {
276 let hits = self.total_hits();
277 let misses = self.total_misses();
278 let total = hits + misses;
279
280 if total > 0 {
281 (hits as f64 / total as f64) * 100.0
282 } else {
283 0.0
284 }
285 }
286}
287
288impl Default for CacheMetrics {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294#[derive(Debug, Clone)]
296pub struct CacheStatsLevelSnapshot {
297 pub hits: u64,
299
300 pub avg_latency_us: u64,
302
303 pub min_latency_us: u64,
305
306 pub max_latency_us: u64,
308
309 pub entry_count: u64,
311
312 pub memory_bytes: u64,
314
315 pub evictions: u64,
317}
318
319#[derive(Debug, Clone)]
321pub struct CacheStatsSnapshot {
322 pub l1: CacheStatsLevelSnapshot,
324
325 pub l2: CacheStatsLevelSnapshot,
327
328 pub l3: CacheStatsLevelSnapshot,
330
331 pub total_hits: u64,
333
334 pub total_misses: u64,
336
337 pub total_skips: u64,
339
340 pub hit_rate: f64,
342
343 pub puts: u64,
345
346 pub invalidations: u64,
348
349 pub tables_invalidated: u64,
351
352 pub clears: u64,
354
355 pub size_exceeded: u64,
357
358 pub uptime_secs: u64,
360}
361
362impl CacheStatsSnapshot {
363 pub fn total_memory_bytes(&self) -> u64 {
365 self.l1.memory_bytes + self.l2.memory_bytes + self.l3.memory_bytes
366 }
367
368 pub fn total_entries(&self) -> u64 {
370 self.l1.entry_count + self.l2.entry_count + self.l3.entry_count
371 }
372
373 pub fn format(&self) -> String {
375 format!(
376 "Cache Stats:\n\
377 ├─ Hit Rate: {:.2}%\n\
378 ├─ Total Hits: {} (L1: {}, L2: {}, L3: {})\n\
379 ├─ Total Misses: {}\n\
380 ├─ Total Entries: {} ({} bytes)\n\
381 ├─ L1 Avg Latency: {}μs\n\
382 ├─ L2 Avg Latency: {}μs\n\
383 ├─ L3 Avg Latency: {}μs\n\
384 ├─ Invalidations: {} ({} tables)\n\
385 └─ Uptime: {}s",
386 self.hit_rate,
387 self.total_hits,
388 self.l1.hits,
389 self.l2.hits,
390 self.l3.hits,
391 self.total_misses,
392 self.total_entries(),
393 self.total_memory_bytes(),
394 self.l1.avg_latency_us,
395 self.l2.avg_latency_us,
396 self.l3.avg_latency_us,
397 self.invalidations,
398 self.tables_invalidated,
399 self.uptime_secs
400 )
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 #[test]
409 fn test_metrics_creation() {
410 let metrics = CacheMetrics::new();
411 assert_eq!(metrics.total_hits(), 0);
412 assert_eq!(metrics.total_misses(), 0);
413 }
414
415 #[test]
416 fn test_record_hit() {
417 let metrics = CacheMetrics::new();
418
419 metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(100));
420 metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(200));
421 metrics.record_hit(CacheLevel::L2Warm, Duration::from_micros(500));
422
423 let snapshot = metrics.snapshot();
424 assert_eq!(snapshot.l1.hits, 2);
425 assert_eq!(snapshot.l2.hits, 1);
426 assert_eq!(snapshot.total_hits, 3);
427 }
428
429 #[test]
430 fn test_record_miss() {
431 let metrics = CacheMetrics::new();
432
433 metrics.record_miss(Duration::from_micros(100));
434 metrics.record_miss(Duration::from_micros(100));
435
436 assert_eq!(metrics.total_misses(), 2);
437 }
438
439 #[test]
440 fn test_hit_rate() {
441 let metrics = CacheMetrics::new();
442
443 metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(100));
445 metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(100));
446 metrics.record_hit(CacheLevel::L2Warm, Duration::from_micros(100));
447 metrics.record_miss(Duration::from_micros(100));
448
449 let rate = metrics.hit_rate();
450 assert!((rate - 75.0).abs() < 0.01);
451 }
452
453 #[test]
454 fn test_latency_tracking() {
455 let metrics = CacheMetrics::new();
456
457 metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(100));
458 metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(300));
459 metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(200));
460
461 let snapshot = metrics.snapshot();
462 assert_eq!(snapshot.l1.min_latency_us, 100);
463 assert_eq!(snapshot.l1.max_latency_us, 300);
464 assert_eq!(snapshot.l1.avg_latency_us, 200); }
466
467 #[test]
468 fn test_invalidation_tracking() {
469 let metrics = CacheMetrics::new();
470
471 metrics.record_invalidation(3);
472 metrics.record_invalidation(2);
473
474 let snapshot = metrics.snapshot();
475 assert_eq!(snapshot.invalidations, 2);
476 assert_eq!(snapshot.tables_invalidated, 5);
477 }
478
479 #[test]
480 fn test_entry_count_tracking() {
481 let metrics = CacheMetrics::new();
482
483 metrics.set_entry_count(CacheLevel::L1Hot, 100);
484 metrics.set_entry_count(CacheLevel::L2Warm, 500);
485 metrics.set_entry_count(CacheLevel::L3Semantic, 50);
486
487 let snapshot = metrics.snapshot();
488 assert_eq!(snapshot.l1.entry_count, 100);
489 assert_eq!(snapshot.l2.entry_count, 500);
490 assert_eq!(snapshot.l3.entry_count, 50);
491 assert_eq!(snapshot.total_entries(), 650);
492 }
493
494 #[test]
495 fn test_memory_tracking() {
496 let metrics = CacheMetrics::new();
497
498 metrics.set_memory_bytes(CacheLevel::L1Hot, 1024);
499 metrics.set_memory_bytes(CacheLevel::L2Warm, 1024 * 1024);
500
501 let snapshot = metrics.snapshot();
502 assert_eq!(snapshot.l1.memory_bytes, 1024);
503 assert_eq!(snapshot.l2.memory_bytes, 1024 * 1024);
504 }
505
506 #[test]
507 fn test_snapshot_format() {
508 let metrics = CacheMetrics::new();
509 metrics.record_hit(CacheLevel::L1Hot, Duration::from_micros(100));
510 metrics.record_miss(Duration::from_micros(100));
511
512 let snapshot = metrics.snapshot();
513 let formatted = snapshot.format();
514
515 assert!(formatted.contains("Hit Rate:"));
516 assert!(formatted.contains("Total Hits:"));
517 }
518
519 #[test]
520 fn test_eviction_tracking() {
521 let metrics = CacheMetrics::new();
522
523 metrics.record_eviction(CacheLevel::L1Hot);
524 metrics.record_eviction(CacheLevel::L1Hot);
525 metrics.record_eviction(CacheLevel::L2Warm);
526
527 let snapshot = metrics.snapshot();
528 assert_eq!(snapshot.l1.evictions, 2);
529 assert_eq!(snapshot.l2.evictions, 1);
530 }
531}