use std::collections::{BTreeMap, HashMap};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
pub type CacheKey = (u8, u32);
const ENTRY_OVERHEAD: u64 = 96;
pub struct CacheEntry {
body: Arc<[u8]>,
runs: OnceLock<Arc<[usize]>>,
}
impl CacheEntry {
#[inline]
pub fn body(&self) -> &[u8] {
&self.body
}
#[inline]
pub fn body_arc(&self) -> Arc<[u8]> {
Arc::clone(&self.body)
}
#[inline]
pub fn runs(&self, compute: impl FnOnce(&[u8]) -> Vec<usize>) -> &[usize] {
if self.body.is_empty() {
return &[];
}
&self.runs.get_or_init(|| Arc::from(compute(&self.body)))[..]
}
#[inline]
fn charge(&self) -> u64 {
self.body.len() as u64 + ENTRY_OVERHEAD
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub decoded_chunks: u64,
pub decoded_bytes: u64,
pub evictions: u64,
pub resident_bytes: u64,
pub entries: u64,
}
struct Slot {
entry: Arc<CacheEntry>,
seq: u64,
charge: u64,
}
#[derive(Default)]
struct CacheInner {
map: HashMap<CacheKey, Slot>,
order: BTreeMap<u64, CacheKey>,
next_seq: u64,
resident_bytes: u64,
}
pub struct ChunkCache {
inner: Mutex<CacheInner>,
cap: AtomicU64,
hits: AtomicU64,
misses: AtomicU64,
decoded_chunks: AtomicU64,
decoded_bytes: AtomicU64,
evictions: AtomicU64,
}
impl std::fmt::Debug for ChunkCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = self.stats();
f.debug_struct("ChunkCache")
.field("cap", &self.cap.load(Ordering::Relaxed))
.field("resident_bytes", &s.resident_bytes)
.field("entries", &s.entries)
.finish()
}
}
impl ChunkCache {
pub fn new(cap: u64) -> Self {
ChunkCache {
inner: Mutex::new(CacheInner::default()),
cap: AtomicU64::new(cap),
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
decoded_chunks: AtomicU64::new(0),
decoded_bytes: AtomicU64::new(0),
evictions: AtomicU64::new(0),
}
}
pub fn unlimited() -> Self {
Self::new(u64::MAX)
}
pub fn unlimited_arc() -> Arc<Self> {
Arc::new(Self::unlimited())
}
pub fn set_cap(&self, cap: u64) {
self.cap.store(cap, Ordering::Relaxed);
let mut inner = self.inner.lock().unwrap();
self.evict_to_cap(&mut inner);
}
pub fn cap(&self) -> u64 {
self.cap.load(Ordering::Relaxed)
}
pub fn get(&self, key: CacheKey) -> Option<Arc<CacheEntry>> {
let mut inner = self.inner.lock().unwrap();
let seq = inner.next_seq;
if let Some(slot) = inner.map.get_mut(&key) {
let old = slot.seq;
slot.seq = seq;
let entry = Arc::clone(&slot.entry);
inner.order.remove(&old);
inner.order.insert(seq, key);
inner.next_seq += 1;
drop(inner);
self.hits.fetch_add(1, Ordering::Relaxed);
Some(entry)
} else {
drop(inner);
self.misses.fetch_add(1, Ordering::Relaxed);
None
}
}
pub fn contains(&self, key: CacheKey) -> bool {
self.inner.lock().unwrap().map.contains_key(&key)
}
pub fn insert(&self, key: CacheKey, body: Arc<[u8]>) -> Arc<CacheEntry> {
self.decoded_chunks.fetch_add(1, Ordering::Relaxed);
self.decoded_bytes
.fetch_add(body.len() as u64, Ordering::Relaxed);
let mut inner = self.inner.lock().unwrap();
let seq = inner.next_seq;
if let Some(slot) = inner.map.get_mut(&key) {
let old = slot.seq;
slot.seq = seq;
let entry = Arc::clone(&slot.entry);
inner.order.remove(&old);
inner.order.insert(seq, key);
inner.next_seq += 1;
return entry;
}
let entry = Arc::new(CacheEntry {
body,
runs: OnceLock::new(),
});
let charge = entry.charge();
inner.next_seq += 1;
inner.map.insert(
key,
Slot {
entry: Arc::clone(&entry),
seq,
charge,
},
);
inner.order.insert(seq, key);
inner.resident_bytes += charge;
self.evict_to_cap(&mut inner);
entry
}
pub fn get_or_fetch(
&self,
key: CacheKey,
fetch: impl FnOnce() -> Option<Arc<[u8]>>,
) -> Option<Arc<CacheEntry>> {
if let Some(e) = self.get(key) {
return Some(e);
}
let body = fetch()?;
Some(self.insert(key, body))
}
fn evict_to_cap(&self, inner: &mut CacheInner) {
let cap = self.cap.load(Ordering::Relaxed);
while inner.resident_bytes > cap {
let Some((&seq, &key)) = inner.order.iter().next() else {
break;
};
inner.order.remove(&seq);
if let Some(slot) = inner.map.remove(&key) {
inner.resident_bytes -= slot.charge;
}
self.evictions.fetch_add(1, Ordering::Relaxed);
}
}
pub fn stats(&self) -> CacheStats {
let inner = self.inner.lock().unwrap();
CacheStats {
hits: self.hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
decoded_chunks: self.decoded_chunks.load(Ordering::Relaxed),
decoded_bytes: self.decoded_bytes.load(Ordering::Relaxed),
evictions: self.evictions.load(Ordering::Relaxed),
resident_bytes: inner.resident_bytes,
entries: inner.map.len() as u64,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn body(n: usize, fill: u8) -> Arc<[u8]> {
Arc::from(vec![fill; n])
}
#[test]
fn insert_and_get_round_trip() {
let cache = ChunkCache::unlimited();
let e = cache.insert((0, 7), body(32, 0xAB));
assert_eq!(e.body(), &[0xAB; 32]);
let got = cache.get((0, 7)).expect("resident");
assert_eq!(got.body(), &[0xAB; 32]);
assert!(Arc::ptr_eq(&e, &got), "same entry handed out");
assert!(cache.get((0, 8)).is_none(), "absent key misses");
let s = cache.stats();
assert_eq!(s.entries, 1);
assert_eq!(s.evictions, 0);
}
#[test]
fn unlimited_cap_never_evicts() {
let cache = ChunkCache::unlimited();
for i in 0..1000u32 {
cache.insert((1, i), body(4096, i as u8));
}
let s = cache.stats();
assert_eq!(s.entries, 1000, "every insert stays resident");
assert_eq!(s.evictions, 0, "unlimited cap evicts nothing");
for i in 0..1000u32 {
assert!(cache.contains((1, i)), "chunk {i} still resident");
}
}
#[test]
fn arc_stays_valid_across_a_forced_eviction() {
let cache = ChunkCache::new(100 + ENTRY_OVERHEAD);
let first = cache.insert((0, 0), body(100, 1));
assert!(cache.contains((0, 0)));
let _second = cache.insert((0, 1), body(100, 2));
assert!(!cache.contains((0, 0)), "LRU key evicted from the map");
assert!(cache.contains((0, 1)), "newest key kept");
assert!(cache.stats().evictions >= 1, "an eviction happened");
assert_eq!(
first.body(),
&[1u8; 100],
"handed-out Arc survives eviction"
);
}
#[test]
fn recency_protects_the_hot_entry() {
let cache = ChunkCache::new(2 * (100 + ENTRY_OVERHEAD));
cache.insert((0, 0), body(100, 0));
cache.insert((0, 1), body(100, 1));
assert!(cache.get((0, 0)).is_some());
cache.insert((0, 2), body(100, 2)); assert!(cache.contains((0, 0)), "recently used key survives");
assert!(!cache.contains((0, 1)), "LRU key evicted");
assert!(cache.contains((0, 2)), "newest key present");
}
#[test]
fn runs_are_derived_once_and_cached() {
let cache = ChunkCache::unlimited();
let e = cache.insert((0, 0), body(10, 0));
let calls = std::cell::Cell::new(0);
let r1 = e.runs(|_| {
calls.set(calls.get() + 1);
vec![0, 4, 8]
});
assert_eq!(r1, &[0, 4, 8]);
let r2 = e.runs(|_| {
calls.set(calls.get() + 1);
vec![9, 9, 9]
});
assert_eq!(r2, &[0, 4, 8], "cached, not recomputed");
assert_eq!(calls.get(), 1, "compute ran exactly once");
let empty = cache.insert((0, 1), Arc::from(Vec::<u8>::new()));
assert!(empty.runs(|_| vec![1, 2, 3]).is_empty());
}
#[test]
fn get_or_fetch_faults_once_then_hits() {
let cache = ChunkCache::unlimited();
let calls = std::cell::Cell::new(0);
let e1 = cache
.get_or_fetch((2, 5), || {
calls.set(calls.get() + 1);
Some(body(16, 0x11))
})
.unwrap();
assert_eq!(e1.body(), &[0x11; 16]);
let e2 = cache
.get_or_fetch((2, 5), || {
calls.set(calls.get() + 1);
Some(body(16, 0x22))
})
.unwrap();
assert_eq!(e2.body(), &[0x11; 16], "second call hits the cache");
assert_eq!(calls.get(), 1, "fetch ran once");
assert!(cache.get_or_fetch((2, 6), || None).is_none());
assert!(!cache.contains((2, 6)));
}
}