#![forbid(unsafe_code)]
use ahash::AHashMap;
use std::collections::VecDeque;
use std::hash::Hash;
struct Entry<K, V> {
key: K,
value: V,
freq: u8,
}
pub struct S3Fifo<K, V> {
index: AHashMap<K, Location>,
entries: Vec<Option<Entry<K, V>>>,
free_indices: Vec<usize>,
small: VecDeque<usize>,
main: VecDeque<usize>,
ghost: VecDeque<K>,
small_cap: usize,
main_cap: usize,
ghost_cap: usize,
hits: u64,
misses: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Location {
Small(usize),
Main(usize),
}
impl Location {
fn idx(&self) -> usize {
match self {
Self::Small(i) | Self::Main(i) => *i,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct S3FifoStats {
pub hits: u64,
pub misses: u64,
pub small_size: usize,
pub main_size: usize,
pub ghost_size: usize,
pub capacity: usize,
}
impl<K, V> S3Fifo<K, V>
where
K: Hash + Eq + Clone,
{
pub fn new(capacity: usize) -> Self {
let capacity = capacity.max(2);
let small_cap = (capacity / 10).max(1);
let main_cap = capacity - small_cap;
let ghost_cap = small_cap;
Self {
index: AHashMap::with_capacity(capacity),
entries: Vec::with_capacity(capacity),
free_indices: Vec::new(),
small: VecDeque::with_capacity(small_cap),
main: VecDeque::with_capacity(main_cap),
ghost: VecDeque::with_capacity(ghost_cap),
small_cap,
main_cap,
ghost_cap,
hits: 0,
misses: 0,
}
}
pub fn get(&mut self, key: &K) -> Option<&V> {
if let Some(loc) = self.index.get(key) {
self.hits += 1;
let idx = loc.idx();
let entry = self.entries[idx]
.as_mut()
.expect("S3Fifo invariant violated: valid index required");
entry.freq = entry.freq.saturating_add(1).min(3);
Some(&entry.value)
} else {
None
}
}
pub fn insert(&mut self, key: K, value: V) -> Option<V> {
if let Some(loc) = self.index.get(&key) {
let idx = loc.idx();
let entry = self.entries[idx]
.as_mut()
.expect("S3Fifo invariant violated: valid index required");
let old = std::mem::replace(&mut entry.value, value);
entry.freq = entry.freq.saturating_add(1).min(3);
return Some(old);
}
self.misses += 1;
let in_ghost = self.remove_from_ghost(&key);
if in_ghost {
self.evict_main_if_full();
let idx = self.alloc_entry(key.clone(), value);
self.main.push_back(idx);
self.index.insert(key, Location::Main(idx));
} else {
self.evict_small_if_full();
let idx = self.alloc_entry(key.clone(), value);
self.small.push_back(idx);
self.index.insert(key, Location::Small(idx));
}
None
}
pub fn remove(&mut self, key: &K) -> Option<V> {
let loc = self.index.remove(key)?;
let idx = loc.idx();
match loc {
Location::Small(_) => {
if let Some(pos) = self.small.iter().position(|&i| i == idx) {
self.small.remove(pos);
}
}
Location::Main(_) => {
if let Some(pos) = self.main.iter().position(|&i| i == idx) {
self.main.remove(pos);
}
}
}
self.free_entry(idx)
}
pub fn len(&self) -> usize {
self.index.len()
}
pub fn is_empty(&self) -> bool {
self.index.is_empty()
}
pub fn capacity(&self) -> usize {
self.small_cap + self.main_cap
}
pub fn stats(&self) -> S3FifoStats {
S3FifoStats {
hits: self.hits,
misses: self.misses,
small_size: self.small.len(),
main_size: self.main.len(),
ghost_size: self.ghost.len(),
capacity: self.small_cap + self.main_cap,
}
}
pub fn clear(&mut self) {
self.index.clear();
self.entries.clear();
self.free_indices.clear();
self.small.clear();
self.main.clear();
self.ghost.clear();
self.hits = 0;
self.misses = 0;
}
pub fn contains_key(&self, key: &K) -> bool {
self.index.contains_key(key)
}
fn alloc_entry(&mut self, key: K, value: V) -> usize {
let entry = Some(Entry {
key,
value,
freq: 0,
});
if let Some(idx) = self.free_indices.pop() {
self.entries[idx] = entry;
idx
} else {
let idx = self.entries.len();
self.entries.push(entry);
idx
}
}
fn free_entry(&mut self, idx: usize) -> Option<V> {
let entry = self.entries[idx].take()?;
self.free_indices.push(idx);
Some(entry.value)
}
fn remove_from_ghost(&mut self, key: &K) -> bool {
if let Some(pos) = self.ghost.iter().position(|k| k == key) {
self.ghost.remove(pos);
true
} else {
false
}
}
fn evict_small_if_full(&mut self) {
while self.small.len() >= self.small_cap {
if let Some(idx) = self.small.pop_front() {
let freq = self.entries[idx]
.as_ref()
.expect("S3Fifo invariant violated: valid index required")
.freq;
if freq > 0 {
self.entries[idx]
.as_mut()
.expect("S3Fifo invariant violated: valid index required")
.freq = 0;
let key = self.entries[idx]
.as_ref()
.expect("S3Fifo invariant violated: valid index required")
.key
.clone();
self.evict_main_if_full();
self.index.insert(key, Location::Main(idx));
self.main.push_back(idx);
} else {
let entry = self.entries[idx]
.take()
.expect("S3Fifo invariant violated: valid index required");
self.free_indices.push(idx);
self.index.remove(&entry.key);
if self.ghost.len() >= self.ghost_cap {
self.ghost.pop_front();
}
self.ghost.push_back(entry.key);
}
}
}
}
fn evict_main_if_full(&mut self) {
while self.main.len() >= self.main_cap {
if let Some(idx) = self.main.pop_front() {
let freq = self.entries[idx]
.as_ref()
.expect("S3Fifo invariant violated: valid index required")
.freq;
if freq > 0 {
self.entries[idx]
.as_mut()
.expect("S3Fifo invariant violated: valid index required")
.freq -= 1;
self.main.push_back(idx);
} else {
let entry = self.entries[idx]
.take()
.expect("S3Fifo invariant violated: valid index required");
self.free_indices.push(idx);
self.index.remove(&entry.key);
}
}
}
}
}
impl<K, V> std::fmt::Debug for S3Fifo<K, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("S3Fifo")
.field("small", &self.small.len())
.field("main", &self.main.len())
.field("ghost", &self.ghost.len())
.field("hits", &self.hits)
.field("misses", &self.misses)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_cache() {
let cache: S3Fifo<&str, i32> = S3Fifo::new(10);
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
#[test]
fn insert_and_get() {
let mut cache = S3Fifo::new(10);
cache.insert("key1", 42);
assert_eq!(cache.get(&"key1"), Some(&42));
assert_eq!(cache.len(), 1);
}
#[test]
fn miss_returns_none() {
let mut cache: S3Fifo<&str, i32> = S3Fifo::new(10);
assert_eq!(cache.get(&"missing"), None);
}
#[test]
fn update_existing_key() {
let mut cache = S3Fifo::new(10);
cache.insert("key1", 1);
let old = cache.insert("key1", 2);
assert_eq!(old, Some(1));
assert_eq!(cache.get(&"key1"), Some(&2));
assert_eq!(cache.len(), 1);
}
#[test]
fn remove_key() {
let mut cache = S3Fifo::new(10);
cache.insert("key1", 42);
let removed = cache.remove(&"key1");
assert_eq!(removed, Some(42));
assert!(cache.is_empty());
assert_eq!(cache.get(&"key1"), None);
}
#[test]
fn remove_nonexistent() {
let mut cache: S3Fifo<&str, i32> = S3Fifo::new(10);
assert_eq!(cache.remove(&"missing"), None);
}
#[test]
fn eviction_at_capacity() {
let mut cache = S3Fifo::new(5);
for i in 0..10 {
cache.insert(i, i * 10);
}
assert!(cache.len() <= cache.capacity());
}
#[test]
fn small_to_main_promotion() {
let mut cache = S3Fifo::new(10);
cache.insert("keep", 1);
cache.get(&"keep");
cache.insert("new", 2);
assert_eq!(cache.get(&"keep"), Some(&1));
}
#[test]
fn ghost_readmission() {
let mut cache = S3Fifo::new(10);
cache.insert("ghost_key", 1);
cache.insert("displacer", 2);
assert_eq!(cache.get(&"ghost_key"), None);
cache.insert("ghost_key", 3);
assert_eq!(cache.get(&"ghost_key"), Some(&3));
}
#[test]
fn stats_tracking() {
let mut cache = S3Fifo::new(20);
cache.insert("a", 1);
cache.insert("b", 2);
cache.get(&"a"); cache.get(&"a"); cache.get(&"c");
let stats = cache.stats();
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 2); }
#[test]
fn clear_resets() {
let mut cache = S3Fifo::new(10);
cache.insert("a", 1);
cache.insert("b", 2);
cache.get(&"a");
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
assert_eq!(stats.ghost_size, 0);
}
#[test]
fn contains_key() {
let mut cache = S3Fifo::new(10);
cache.insert("a", 1);
assert!(cache.contains_key(&"a"));
assert!(!cache.contains_key(&"b"));
}
#[test]
fn capacity_split() {
let cache: S3Fifo<i32, i32> = S3Fifo::new(100);
assert_eq!(cache.capacity(), 100);
assert_eq!(cache.small_cap, 10);
assert_eq!(cache.main_cap, 90);
assert_eq!(cache.ghost_cap, 10);
}
#[test]
fn minimum_capacity() {
let cache: S3Fifo<i32, i32> = S3Fifo::new(0);
assert!(cache.capacity() >= 2);
}
#[test]
fn freq_capped_at_3() {
let mut cache = S3Fifo::new(10);
cache.insert("a", 1);
for _ in 0..10 {
cache.get(&"a");
}
assert_eq!(cache.get(&"a"), Some(&1));
}
#[test]
fn main_eviction_gives_second_chance() {
let mut cache = S3Fifo::new(5);
for i in 0..4 {
cache.insert(i, i);
cache.get(&i);
}
for i in 10..20 {
cache.insert(i, i);
}
assert!(cache.len() <= cache.capacity());
}
#[test]
fn debug_format() {
let cache: S3Fifo<&str, i32> = S3Fifo::new(10);
let debug = format!("{cache:?}");
assert!(debug.contains("S3Fifo"));
assert!(debug.contains("small"));
assert!(debug.contains("main"));
}
#[test]
fn large_workload() {
let mut cache = S3Fifo::new(100);
for i in 0..200 {
cache.insert(i, i * 10);
if i >= 50 {
for hot in 50..std::cmp::min(i, 100) {
cache.get(&hot);
}
}
}
let mut hot_hits = 0;
for i in 50..100 {
if cache.get(&i).is_some() {
hot_hits += 1;
}
}
assert!(hot_hits > 20, "hot set retention: {hot_hits}/50");
}
#[test]
fn scan_resistance() {
let mut cache = S3Fifo::new(100);
for i in 0..50 {
cache.insert(i, i);
cache.get(&i);
cache.get(&i);
}
for i in 1000..2000 {
cache.insert(i, i);
}
let mut survivors = 0;
for i in 0..50 {
if cache.get(&i).is_some() {
survivors += 1;
}
}
assert!(
survivors > 10,
"scan resistance: {survivors}/50 working set items survived"
);
}
#[test]
fn ghost_size_bounded() {
let mut cache = S3Fifo::new(10);
for i in 0..100 {
cache.insert(i, i);
}
let stats = cache.stats();
assert!(
stats.ghost_size <= cache.ghost_cap,
"ghost should be bounded: {} <= {}",
stats.ghost_size,
cache.ghost_cap
);
}
}