use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
inner: Arc<StatsInner>,
}
#[derive(Debug, Default)]
struct StatsInner {
hits: AtomicU64,
misses: AtomicU64,
size: AtomicU64,
evictions: AtomicU64,
}
impl CacheStats {
pub fn new() -> Self {
Self::default()
}
pub fn record_hit(&self) {
self.inner.hits.fetch_add(1, Ordering::Relaxed);
}
pub fn record_miss(&self) {
self.inner.misses.fetch_add(1, Ordering::Relaxed);
}
pub fn record_eviction(&self) {
self.inner.evictions.fetch_add(1, Ordering::Relaxed);
}
pub fn set_size(&self, size: u64) {
self.inner.size.store(size, Ordering::Relaxed);
}
pub fn hits(&self) -> u64 {
self.inner.hits.load(Ordering::Relaxed)
}
pub fn misses(&self) -> u64 {
self.inner.misses.load(Ordering::Relaxed)
}
pub fn size(&self) -> u64 {
self.inner.size.load(Ordering::Relaxed)
}
pub fn evictions(&self) -> u64 {
self.inner.evictions.load(Ordering::Relaxed)
}
pub fn snapshot(&self) -> CacheStatsSnapshot {
CacheStatsSnapshot {
hits: self.hits(),
misses: self.misses(),
size: self.size(),
evictions: self.evictions(),
}
}
pub fn reset(&self) {
self.inner.hits.store(0, Ordering::Relaxed);
self.inner.misses.store(0, Ordering::Relaxed);
self.inner.size.store(0, Ordering::Relaxed);
self.inner.evictions.store(0, Ordering::Relaxed);
}
pub fn hit_ratio(&self) -> f64 {
let h = self.hits();
let m = self.misses();
let total = h + m;
if total == 0 {
0.0
} else {
#[allow(clippy::cast_precision_loss)]
let ratio = h as f64 / total as f64;
ratio
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CacheStatsSnapshot {
pub hits: u64,
pub misses: u64,
pub size: u64,
pub evictions: u64,
}
impl CacheStatsSnapshot {
pub fn hit_ratio(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
#[allow(clippy::cast_precision_loss)]
let ratio = self.hits as f64 / total as f64;
ratio
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counters_start_at_zero() {
let s = CacheStats::new();
assert_eq!(s.hits(), 0);
assert_eq!(s.misses(), 0);
assert!((s.hit_ratio() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn increments_and_ratio() {
let s = CacheStats::new();
s.record_hit();
s.record_hit();
s.record_miss();
let snap = s.snapshot();
assert_eq!(snap.hits, 2);
assert_eq!(snap.misses, 1);
let expected = 2.0 / 3.0;
assert!((snap.hit_ratio() - expected).abs() < 1e-9);
}
#[test]
fn clones_share_state() {
let s = CacheStats::new();
let s2 = s.clone();
s.record_hit();
s2.record_miss();
assert_eq!(s.hits(), 1);
assert_eq!(s.misses(), 1);
}
#[test]
fn reset_clears_counters() {
let s = CacheStats::new();
s.record_hit();
s.record_miss();
s.record_eviction();
s.set_size(5);
s.reset();
assert_eq!(s.hits(), 0);
assert_eq!(s.misses(), 0);
assert_eq!(s.size(), 0);
assert_eq!(s.evictions(), 0);
}
}