ngdp_cache/
stats.rs

1//! Cache statistics tracking and reporting
2//!
3//! This module provides comprehensive statistics tracking for cache operations,
4//! including hit/miss ratios, bandwidth savings, and performance metrics.
5
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::time::{Duration, Instant};
10
11/// Cache statistics for tracking performance and effectiveness
12#[derive(Debug, Clone)]
13pub struct CacheStats {
14    /// Total number of cache hits
15    hits: Arc<AtomicU64>,
16    /// Total number of cache misses
17    misses: Arc<AtomicU64>,
18    /// Total number of cache evictions
19    evictions: Arc<AtomicU64>,
20    /// Total bytes served from cache (bandwidth saved)
21    bytes_saved: Arc<AtomicU64>,
22    /// Total bytes written to cache
23    bytes_written: Arc<AtomicU64>,
24    /// Total bytes evicted from cache
25    bytes_evicted: Arc<AtomicU64>,
26    /// Number of read operations
27    read_operations: Arc<AtomicU64>,
28    /// Number of write operations
29    write_operations: Arc<AtomicU64>,
30    /// Number of delete operations
31    delete_operations: Arc<AtomicU64>,
32    /// Start time for calculating uptime
33    start_time: Instant,
34}
35
36/// Snapshot of cache statistics at a point in time
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct CacheStatsSnapshot {
39    /// Total cache hits
40    pub hits: u64,
41    /// Total cache misses
42    pub misses: u64,
43    /// Total cache evictions
44    pub evictions: u64,
45    /// Total bytes saved by cache hits
46    pub bytes_saved: u64,
47    /// Total bytes written to cache
48    pub bytes_written: u64,
49    /// Total bytes evicted from cache
50    pub bytes_evicted: u64,
51    /// Total read operations
52    pub read_operations: u64,
53    /// Total write operations
54    pub write_operations: u64,
55    /// Total delete operations
56    pub delete_operations: u64,
57    /// Cache hit rate as a percentage (0.0 to 100.0)
58    pub hit_rate: f64,
59    /// Cache miss rate as a percentage (0.0 to 100.0)
60    pub miss_rate: f64,
61    /// Total cache operations (hits + misses)
62    pub total_operations: u64,
63    /// Cache uptime in seconds
64    pub uptime_seconds: u64,
65}
66
67/// Detailed cache performance report
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CacheReport {
70    /// Basic statistics snapshot
71    pub stats: CacheStatsSnapshot,
72    /// Average bytes per hit
73    pub avg_bytes_per_hit: f64,
74    /// Average bytes per write
75    pub avg_bytes_per_write: f64,
76    /// Bandwidth savings ratio (0.0 to 1.0)
77    pub bandwidth_savings_ratio: f64,
78    /// Operations per second
79    pub operations_per_second: f64,
80    /// Bytes per second served from cache
81    pub bytes_per_second_saved: f64,
82    /// Cache effectiveness score (0.0 to 100.0)
83    pub effectiveness_score: f64,
84}
85
86impl Default for CacheStats {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92impl CacheStats {
93    /// Create a new cache statistics tracker
94    pub fn new() -> Self {
95        Self {
96            hits: Arc::new(AtomicU64::new(0)),
97            misses: Arc::new(AtomicU64::new(0)),
98            evictions: Arc::new(AtomicU64::new(0)),
99            bytes_saved: Arc::new(AtomicU64::new(0)),
100            bytes_written: Arc::new(AtomicU64::new(0)),
101            bytes_evicted: Arc::new(AtomicU64::new(0)),
102            read_operations: Arc::new(AtomicU64::new(0)),
103            write_operations: Arc::new(AtomicU64::new(0)),
104            delete_operations: Arc::new(AtomicU64::new(0)),
105            start_time: Instant::now(),
106        }
107    }
108
109    /// Record a cache hit with the number of bytes served
110    pub fn record_hit(&self, bytes: u64) {
111        self.hits.fetch_add(1, Ordering::Relaxed);
112        self.bytes_saved.fetch_add(bytes, Ordering::Relaxed);
113        self.read_operations.fetch_add(1, Ordering::Relaxed);
114    }
115
116    /// Record a cache miss
117    pub fn record_miss(&self) {
118        self.misses.fetch_add(1, Ordering::Relaxed);
119        self.read_operations.fetch_add(1, Ordering::Relaxed);
120    }
121
122    /// Record a cache eviction with the number of bytes evicted
123    pub fn record_eviction(&self, bytes: u64) {
124        self.evictions.fetch_add(1, Ordering::Relaxed);
125        self.bytes_evicted.fetch_add(bytes, Ordering::Relaxed);
126    }
127
128    /// Record a write operation with the number of bytes written
129    pub fn record_write(&self, bytes: u64) {
130        self.bytes_written.fetch_add(bytes, Ordering::Relaxed);
131        self.write_operations.fetch_add(1, Ordering::Relaxed);
132    }
133
134    /// Record a delete operation
135    pub fn record_delete(&self) {
136        self.delete_operations.fetch_add(1, Ordering::Relaxed);
137    }
138
139    /// Get current hit count
140    pub fn hits(&self) -> u64 {
141        self.hits.load(Ordering::Relaxed)
142    }
143
144    /// Get current miss count
145    pub fn misses(&self) -> u64 {
146        self.misses.load(Ordering::Relaxed)
147    }
148
149    /// Get current eviction count
150    pub fn evictions(&self) -> u64 {
151        self.evictions.load(Ordering::Relaxed)
152    }
153
154    /// Get total bytes saved by cache hits
155    pub fn bytes_saved(&self) -> u64 {
156        self.bytes_saved.load(Ordering::Relaxed)
157    }
158
159    /// Get total bytes written to cache
160    pub fn bytes_written(&self) -> u64 {
161        self.bytes_written.load(Ordering::Relaxed)
162    }
163
164    /// Get total bytes evicted from cache
165    pub fn bytes_evicted(&self) -> u64 {
166        self.bytes_evicted.load(Ordering::Relaxed)
167    }
168
169    /// Calculate hit rate as a percentage (0.0 to 100.0)
170    pub fn hit_rate(&self) -> f64 {
171        let hits = self.hits();
172        let total = hits + self.misses();
173
174        if total == 0 {
175            0.0
176        } else {
177            (hits as f64 / total as f64) * 100.0
178        }
179    }
180
181    /// Calculate miss rate as a percentage (0.0 to 100.0)
182    pub fn miss_rate(&self) -> f64 {
183        let misses = self.misses();
184        let total = self.hits() + misses;
185
186        if total == 0 {
187            0.0
188        } else {
189            (misses as f64 / total as f64) * 100.0
190        }
191    }
192
193    /// Get total cache operations (hits + misses)
194    pub fn total_operations(&self) -> u64 {
195        self.hits() + self.misses()
196    }
197
198    /// Get cache uptime
199    pub fn uptime(&self) -> Duration {
200        self.start_time.elapsed()
201    }
202
203    /// Reset all statistics
204    pub fn reset(&self) {
205        self.hits.store(0, Ordering::Relaxed);
206        self.misses.store(0, Ordering::Relaxed);
207        self.evictions.store(0, Ordering::Relaxed);
208        self.bytes_saved.store(0, Ordering::Relaxed);
209        self.bytes_written.store(0, Ordering::Relaxed);
210        self.bytes_evicted.store(0, Ordering::Relaxed);
211        self.read_operations.store(0, Ordering::Relaxed);
212        self.write_operations.store(0, Ordering::Relaxed);
213        self.delete_operations.store(0, Ordering::Relaxed);
214    }
215
216    /// Get a snapshot of current statistics
217    pub fn snapshot(&self) -> CacheStatsSnapshot {
218        let hits = self.hits();
219        let misses = self.misses();
220        let total_ops = hits + misses;
221        let uptime_secs = self.uptime().as_secs();
222
223        CacheStatsSnapshot {
224            hits,
225            misses,
226            evictions: self.evictions(),
227            bytes_saved: self.bytes_saved(),
228            bytes_written: self.bytes_written(),
229            bytes_evicted: self.bytes_evicted(),
230            read_operations: self.read_operations.load(Ordering::Relaxed),
231            write_operations: self.write_operations.load(Ordering::Relaxed),
232            delete_operations: self.delete_operations.load(Ordering::Relaxed),
233            hit_rate: self.hit_rate(),
234            miss_rate: self.miss_rate(),
235            total_operations: total_ops,
236            uptime_seconds: uptime_secs,
237        }
238    }
239
240    /// Generate a comprehensive performance report
241    pub fn report(&self) -> CacheReport {
242        let stats = self.snapshot();
243        let uptime_secs = stats.uptime_seconds as f64;
244
245        // Calculate averages
246        let avg_bytes_per_hit = if stats.hits > 0 {
247            stats.bytes_saved as f64 / stats.hits as f64
248        } else {
249            0.0
250        };
251
252        let avg_bytes_per_write = if stats.write_operations > 0 {
253            stats.bytes_written as f64 / stats.write_operations as f64
254        } else {
255            0.0
256        };
257
258        // Calculate bandwidth savings ratio
259        let total_bytes_served = stats.bytes_saved + (stats.misses * avg_bytes_per_hit as u64);
260        let bandwidth_savings_ratio = if total_bytes_served > 0 {
261            stats.bytes_saved as f64 / total_bytes_served as f64
262        } else {
263            0.0
264        };
265
266        // Calculate rates
267        let operations_per_second = if uptime_secs > 0.0 {
268            stats.total_operations as f64 / uptime_secs
269        } else {
270            0.0
271        };
272
273        let bytes_per_second_saved = if uptime_secs > 0.0 {
274            stats.bytes_saved as f64 / uptime_secs
275        } else {
276            0.0
277        };
278
279        // Calculate effectiveness score (weighted combination of hit rate and bandwidth savings)
280        let hit_rate_score = stats.hit_rate;
281        let bandwidth_score = bandwidth_savings_ratio * 100.0;
282        let effectiveness_score = (hit_rate_score * 0.7) + (bandwidth_score * 0.3);
283
284        CacheReport {
285            stats,
286            avg_bytes_per_hit,
287            avg_bytes_per_write,
288            bandwidth_savings_ratio,
289            operations_per_second,
290            bytes_per_second_saved,
291            effectiveness_score,
292        }
293    }
294}
295
296impl CacheStatsSnapshot {
297    /// Format bytes as human-readable string (KB, MB, GB)
298    pub fn format_bytes(bytes: u64) -> String {
299        const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
300        let mut size = bytes as f64;
301        let mut unit_index = 0;
302
303        while size >= 1024.0 && unit_index < UNITS.len() - 1 {
304            size /= 1024.0;
305            unit_index += 1;
306        }
307
308        if unit_index == 0 {
309            format!("{} {}", bytes, UNITS[unit_index])
310        } else {
311            format!("{:.2} {}", size, UNITS[unit_index])
312        }
313    }
314
315    /// Format duration as human-readable string
316    pub fn format_uptime(&self) -> String {
317        let secs = self.uptime_seconds;
318        let days = secs / 86400;
319        let hours = (secs % 86400) / 3600;
320        let minutes = (secs % 3600) / 60;
321        let seconds = secs % 60;
322
323        if days > 0 {
324            format!("{days}d {hours}h {minutes}m {seconds}s")
325        } else if hours > 0 {
326            format!("{hours}h {minutes}m {seconds}s")
327        } else if minutes > 0 {
328            format!("{minutes}m {seconds}s")
329        } else {
330            format!("{seconds}s")
331        }
332    }
333}
334
335impl std::fmt::Display for CacheStatsSnapshot {
336    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337        writeln!(f, "Cache Statistics:")?;
338        writeln!(
339            f,
340            "  Operations: {} hits, {} misses ({:.1}% hit rate)",
341            self.hits, self.misses, self.hit_rate
342        )?;
343        writeln!(
344            f,
345            "  Bandwidth: {} saved, {} written",
346            Self::format_bytes(self.bytes_saved),
347            Self::format_bytes(self.bytes_written)
348        )?;
349        writeln!(
350            f,
351            "  Evictions: {} entries ({} bytes)",
352            self.evictions,
353            Self::format_bytes(self.bytes_evicted)
354        )?;
355        writeln!(f, "  Uptime: {}", self.format_uptime())?;
356        Ok(())
357    }
358}
359
360impl std::fmt::Display for CacheReport {
361    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
362        writeln!(f, "Cache Performance Report:")?;
363        writeln!(f, "{}", self.stats)?;
364        writeln!(f, "Performance Metrics:")?;
365        writeln!(f, "  Average bytes per hit: {:.1}", self.avg_bytes_per_hit)?;
366        writeln!(
367            f,
368            "  Average bytes per write: {:.1}",
369            self.avg_bytes_per_write
370        )?;
371        writeln!(
372            f,
373            "  Bandwidth savings: {:.1}%",
374            self.bandwidth_savings_ratio * 100.0
375        )?;
376        writeln!(
377            f,
378            "  Operations per second: {:.1}",
379            self.operations_per_second
380        )?;
381        writeln!(
382            f,
383            "  Bytes per second saved: {}",
384            CacheStatsSnapshot::format_bytes(self.bytes_per_second_saved as u64)
385        )?;
386        writeln!(
387            f,
388            "  Effectiveness score: {:.1}/100",
389            self.effectiveness_score
390        )?;
391        Ok(())
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use std::thread;
399    use std::time::Duration as StdDuration;
400
401    #[test]
402    fn test_cache_stats_creation() {
403        let stats = CacheStats::new();
404        assert_eq!(stats.hits(), 0);
405        assert_eq!(stats.misses(), 0);
406        assert_eq!(stats.evictions(), 0);
407        assert_eq!(stats.bytes_saved(), 0);
408    }
409
410    #[test]
411    fn test_record_operations() {
412        let stats = CacheStats::new();
413
414        // Record some hits
415        stats.record_hit(1024);
416        stats.record_hit(2048);
417        assert_eq!(stats.hits(), 2);
418        assert_eq!(stats.bytes_saved(), 3072);
419
420        // Record some misses
421        stats.record_miss();
422        stats.record_miss();
423        assert_eq!(stats.misses(), 2);
424
425        // Record evictions
426        stats.record_eviction(512);
427        assert_eq!(stats.evictions(), 1);
428        assert_eq!(stats.bytes_evicted(), 512);
429
430        // Record writes
431        stats.record_write(4096);
432        assert_eq!(stats.bytes_written(), 4096);
433    }
434
435    #[test]
436    fn test_hit_miss_rates() {
437        let stats = CacheStats::new();
438
439        // No operations - should be 0%
440        assert_eq!(stats.hit_rate(), 0.0);
441        assert_eq!(stats.miss_rate(), 0.0);
442
443        // 3 hits, 1 miss = 75% hit rate, 25% miss rate
444        stats.record_hit(100);
445        stats.record_hit(200);
446        stats.record_hit(300);
447        stats.record_miss();
448
449        assert!((stats.hit_rate() - 75.0).abs() < 0.001);
450        assert!((stats.miss_rate() - 25.0).abs() < 0.001);
451        assert_eq!(stats.total_operations(), 4);
452    }
453
454    #[test]
455    fn test_stats_reset() {
456        let stats = CacheStats::new();
457
458        // Record some operations
459        stats.record_hit(1000);
460        stats.record_miss();
461        stats.record_eviction(500);
462        stats.record_write(2000);
463
464        // Verify values are set
465        assert_eq!(stats.hits(), 1);
466        assert_eq!(stats.misses(), 1);
467        assert_eq!(stats.evictions(), 1);
468        assert_eq!(stats.bytes_saved(), 1000);
469        assert_eq!(stats.bytes_written(), 2000);
470
471        // Reset and verify all are zero
472        stats.reset();
473        assert_eq!(stats.hits(), 0);
474        assert_eq!(stats.misses(), 0);
475        assert_eq!(stats.evictions(), 0);
476        assert_eq!(stats.bytes_saved(), 0);
477        assert_eq!(stats.bytes_written(), 0);
478    }
479
480    #[test]
481    fn test_snapshot() {
482        let stats = CacheStats::new();
483
484        // Record some operations
485        stats.record_hit(500);
486        stats.record_hit(1500);
487        stats.record_miss();
488        stats.record_write(1000);
489        stats.record_eviction(200);
490
491        let snapshot = stats.snapshot();
492        assert_eq!(snapshot.hits, 2);
493        assert_eq!(snapshot.misses, 1);
494        assert_eq!(snapshot.evictions, 1);
495        assert_eq!(snapshot.bytes_saved, 2000);
496        assert_eq!(snapshot.bytes_written, 1000);
497        assert_eq!(snapshot.bytes_evicted, 200);
498        assert!((snapshot.hit_rate - 66.666).abs() < 0.01);
499        assert_eq!(snapshot.total_operations, 3);
500    }
501
502    #[test]
503    fn test_report_generation() {
504        let stats = CacheStats::new();
505
506        // Record realistic operations
507        stats.record_hit(1024); // 1KB hit
508        stats.record_hit(2048); // 2KB hit
509        stats.record_hit(4096); // 4KB hit
510        stats.record_miss(); // 1 miss
511        stats.record_write(8192); // 8KB write
512
513        let report = stats.report();
514
515        // Check basic stats
516        assert_eq!(report.stats.hits, 3);
517        assert_eq!(report.stats.misses, 1);
518        assert_eq!(report.stats.bytes_saved, 7168); // 1KB + 2KB + 4KB
519
520        // Check calculated metrics
521        assert!((report.avg_bytes_per_hit - 2389.33).abs() < 0.01); // 7168/3
522        assert_eq!(report.avg_bytes_per_write, 8192.0);
523        assert!(report.effectiveness_score > 0.0);
524        assert!(report.effectiveness_score <= 100.0);
525    }
526
527    #[test]
528    fn test_format_bytes() {
529        assert_eq!(CacheStatsSnapshot::format_bytes(0), "0 B");
530        assert_eq!(CacheStatsSnapshot::format_bytes(512), "512 B");
531        assert_eq!(CacheStatsSnapshot::format_bytes(1024), "1.00 KB");
532        assert_eq!(CacheStatsSnapshot::format_bytes(1536), "1.50 KB");
533        assert_eq!(CacheStatsSnapshot::format_bytes(1048576), "1.00 MB");
534        assert_eq!(CacheStatsSnapshot::format_bytes(1073741824), "1.00 GB");
535    }
536
537    #[test]
538    fn test_concurrent_access() {
539        let stats = Arc::new(CacheStats::new());
540        let mut handles = vec![];
541
542        // Spawn multiple threads to test concurrent access
543        for i in 0..10 {
544            let stats_clone = Arc::clone(&stats);
545            let handle = thread::spawn(move || {
546                for j in 0..100 {
547                    stats_clone.record_hit((i * 100 + j) as u64);
548                    stats_clone.record_miss();
549                    stats_clone.record_write(1000);
550                }
551            });
552            handles.push(handle);
553        }
554
555        // Wait for all threads to complete
556        for handle in handles {
557            handle.join().unwrap();
558        }
559
560        // Verify final counts (10 threads * 100 operations each)
561        assert_eq!(stats.hits(), 1000);
562        assert_eq!(stats.misses(), 1000);
563        assert_eq!(stats.bytes_written(), 1000000); // 10 threads * 100 writes * 1000 bytes
564        assert!((stats.hit_rate() - 50.0).abs() < 0.001);
565    }
566
567    #[test]
568    fn test_uptime_tracking() {
569        let stats = CacheStats::new();
570
571        // Sleep for a short time to get measurable uptime
572        thread::sleep(StdDuration::from_millis(10));
573
574        let uptime = stats.uptime();
575        assert!(uptime.as_millis() >= 10);
576
577        let snapshot = stats.snapshot();
578        // uptime_seconds is u64, so it's always >= 0, just verify it exists
579        assert!(snapshot.uptime_seconds < 3600); // Should be less than 1 hour for test
580    }
581
582    #[test]
583    fn test_display_formatting() {
584        let stats = CacheStats::new();
585        stats.record_hit(1024);
586        stats.record_miss();
587
588        let snapshot = stats.snapshot();
589        let display_output = format!("{snapshot}");
590
591        assert!(display_output.contains("Cache Statistics:"));
592        assert!(display_output.contains("1 hits, 1 misses"));
593        assert!(display_output.contains("1.00 KB saved"));
594
595        let report = stats.report();
596        let report_output = format!("{report}");
597
598        assert!(report_output.contains("Cache Performance Report:"));
599        assert!(report_output.contains("Performance Metrics:"));
600    }
601}