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