#![allow(dead_code)]
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct CacheEntry<T> {
pub data: T,
pub size_bytes: usize,
pub last_access: u64,
pub hit_count: u32,
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
}
pub struct AccelCache<T> {
capacity_bytes: usize,
entries: HashMap<u64, CacheEntry<T>>,
clock: u64,
stats: CacheStats,
current_usage: usize,
}
impl<T: Clone> AccelCache<T> {
#[must_use]
pub fn new(capacity_bytes: usize) -> Self {
Self {
capacity_bytes,
entries: HashMap::new(),
clock: 0,
stats: CacheStats::default(),
current_usage: 0,
}
}
pub fn get(&mut self, key: u64) -> Option<&T> {
if !self.entries.contains_key(&key) {
self.stats.misses += 1;
return None;
}
self.clock += 1;
let clock = self.clock;
let entry = self
.entries
.get_mut(&key)
.unwrap_or_else(|| unreachable!("key confirmed present via contains_key"));
entry.last_access = clock;
entry.hit_count += 1;
self.stats.hits += 1;
self.entries.get(&key).map(|e| &e.data)
}
pub fn insert(&mut self, key: u64, data: T, size: usize) {
if let Some(old) = self.entries.remove(&key) {
self.current_usage = self.current_usage.saturating_sub(old.size_bytes);
}
while self.current_usage + size > self.capacity_bytes && !self.entries.is_empty() {
self.evict_lru();
}
self.clock += 1;
self.current_usage += size;
self.entries.insert(
key,
CacheEntry {
data,
size_bytes: size,
last_access: self.clock,
hit_count: 0,
},
);
}
pub fn evict_lru(&mut self) {
if self.entries.is_empty() {
return;
}
let lru_key = self
.entries
.iter()
.min_by_key(|(_, e)| e.last_access)
.map(|(&k, _)| k)
.unwrap_or_else(|| unreachable!("entries is non-empty (checked above)"));
if let Some(evicted) = self.entries.remove(&lru_key) {
self.current_usage = self.current_usage.saturating_sub(evicted.size_bytes);
self.stats.evictions += 1;
}
}
#[must_use]
pub fn hit_rate(&self) -> f64 {
let total = self.stats.hits + self.stats.misses;
if total == 0 {
0.0
} else {
self.stats.hits as f64 / total as f64
}
}
#[must_use]
pub fn current_usage(&self) -> usize {
self.current_usage
}
#[must_use]
pub fn capacity_bytes(&self) -> usize {
self.capacity_bytes
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn stats(&self) -> CacheStats {
self.stats.clone()
}
pub fn clear(&mut self) {
self.entries.clear();
self.current_usage = 0;
}
#[must_use]
pub fn contains_key(&self, key: u64) -> bool {
self.entries.contains_key(&key)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_miss_on_empty() {
let mut cache: AccelCache<u32> = AccelCache::new(1024);
assert!(cache.get(42).is_none());
assert_eq!(cache.stats().misses, 1);
}
#[test]
fn test_cache_insert_and_hit() {
let mut cache: AccelCache<u32> = AccelCache::new(1024);
cache.insert(1, 99u32, 4);
let v = cache.get(1);
assert_eq!(v, Some(&99u32));
assert_eq!(cache.stats().hits, 1);
}
#[test]
fn test_hit_rate_pure_hits() {
let mut cache: AccelCache<u32> = AccelCache::new(1024);
cache.insert(1, 1u32, 4);
cache.get(1);
cache.get(1);
assert!((cache.hit_rate() - 1.0).abs() < 1e-9);
}
#[test]
fn test_hit_rate_zero_lookups() {
let cache: AccelCache<u32> = AccelCache::new(1024);
assert!((cache.hit_rate() - 0.0).abs() < 1e-9);
}
#[test]
fn test_current_usage_tracking() {
let mut cache: AccelCache<Vec<u8>> = AccelCache::new(1024);
cache.insert(1, vec![0u8; 100], 100);
cache.insert(2, vec![0u8; 200], 200);
assert_eq!(cache.current_usage(), 300);
}
#[test]
fn test_evict_lru_removes_oldest() {
let mut cache: AccelCache<u32> = AccelCache::new(8);
cache.insert(1, 10u32, 4);
cache.insert(2, 20u32, 4);
cache.get(1);
cache.insert(3, 30u32, 4);
assert!(!cache.contains_key(2));
assert!(cache.contains_key(1));
assert!(cache.contains_key(3));
assert_eq!(cache.stats().evictions, 1);
}
#[test]
fn test_evict_lru_on_empty_does_nothing() {
let mut cache: AccelCache<u32> = AccelCache::new(1024);
cache.evict_lru(); assert_eq!(cache.stats().evictions, 0);
}
#[test]
fn test_replace_existing_key() {
let mut cache: AccelCache<u32> = AccelCache::new(1024);
cache.insert(1, 10u32, 4);
cache.insert(1, 20u32, 4); assert_eq!(cache.len(), 1);
assert_eq!(cache.current_usage(), 4);
assert_eq!(cache.get(1), Some(&20u32));
}
#[test]
fn test_clear_resets_usage() {
let mut cache: AccelCache<u32> = AccelCache::new(1024);
cache.insert(1, 1u32, 100);
cache.clear();
assert_eq!(cache.current_usage(), 0);
assert!(cache.is_empty());
}
#[test]
fn test_len_and_is_empty() {
let mut cache: AccelCache<u32> = AccelCache::new(1024);
assert!(cache.is_empty());
cache.insert(1, 1u32, 1);
assert_eq!(cache.len(), 1);
assert!(!cache.is_empty());
}
#[test]
fn test_hit_count_increments() {
let mut cache: AccelCache<u32> = AccelCache::new(1024);
cache.insert(7, 777u32, 4);
cache.get(7);
cache.get(7);
let entry = &cache.entries[&7];
assert_eq!(entry.hit_count, 2);
}
#[test]
fn test_mixed_hit_rate() {
let mut cache: AccelCache<u32> = AccelCache::new(1024);
cache.insert(1, 10u32, 4);
cache.get(1); cache.get(2); cache.get(1); cache.get(3); let rate = cache.hit_rate();
assert!((rate - 0.5).abs() < 1e-9);
}
#[test]
fn test_stats_snapshot_reflects_current_state() {
let mut cache: AccelCache<u32> = AccelCache::new(1024);
cache.insert(10, 100u32, 4);
cache.get(10);
cache.get(99);
let s = cache.stats();
assert_eq!(s.hits, 1);
assert_eq!(s.misses, 1);
assert_eq!(s.evictions, 0);
}
}