1#![forbid(unsafe_code)]
2
3use ahash::AHashMap;
30use std::collections::VecDeque;
31use std::hash::Hash;
32
33struct Entry<K, V> {
35 key: K,
36 value: V,
37 freq: u8,
38}
39
40pub struct S3Fifo<K, V> {
42 index: AHashMap<K, Location>,
44 entries: Vec<Option<Entry<K, V>>>,
46 free_indices: Vec<usize>,
48 small: VecDeque<usize>,
50 main: VecDeque<usize>,
52 ghost: VecDeque<K>,
54 small_cap: usize,
56 main_cap: usize,
58 ghost_cap: usize,
60 hits: u64,
62 misses: u64,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67enum Location {
68 Small(usize),
69 Main(usize),
70}
71
72impl Location {
73 fn idx(&self) -> usize {
74 match self {
75 Self::Small(i) | Self::Main(i) => *i,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82pub struct S3FifoStats {
83 pub hits: u64,
85 pub misses: u64,
90 pub small_size: usize,
92 pub main_size: usize,
94 pub ghost_size: usize,
96 pub capacity: usize,
98}
99
100impl<K, V> S3Fifo<K, V>
101where
102 K: Hash + Eq + Clone,
103{
104 pub fn new(capacity: usize) -> Self {
109 let capacity = capacity.max(2);
110 let small_cap = (capacity / 10).max(1);
111 let main_cap = capacity - small_cap;
112 let ghost_cap = small_cap;
113
114 Self {
115 index: AHashMap::with_capacity(capacity),
116 entries: Vec::with_capacity(capacity),
117 free_indices: Vec::new(),
118 small: VecDeque::with_capacity(small_cap),
119 main: VecDeque::with_capacity(main_cap),
120 ghost: VecDeque::with_capacity(ghost_cap),
121 small_cap,
122 main_cap,
123 ghost_cap,
124 hits: 0,
125 misses: 0,
126 }
127 }
128
129 pub fn get(&mut self, key: &K) -> Option<&V> {
134 if let Some(loc) = self.index.get(key) {
135 self.hits += 1;
136 let idx = loc.idx();
137 let entry = self.entries[idx]
139 .as_mut()
140 .expect("S3Fifo invariant violated: valid index required");
141 entry.freq = entry.freq.saturating_add(1).min(3);
142 Some(&entry.value)
143 } else {
144 None
145 }
146 }
147
148 pub fn insert(&mut self, key: K, value: V) -> Option<V> {
151 if let Some(loc) = self.index.get(&key) {
153 let idx = loc.idx();
154 let entry = self.entries[idx]
155 .as_mut()
156 .expect("S3Fifo invariant violated: valid index required");
157 let old = std::mem::replace(&mut entry.value, value);
158 entry.freq = entry.freq.saturating_add(1).min(3);
159 return Some(old);
160 }
161
162 self.misses += 1;
163
164 let in_ghost = self.remove_from_ghost(&key);
166
167 if in_ghost {
168 self.evict_main_if_full();
170 let idx = self.alloc_entry(key.clone(), value);
171 self.main.push_back(idx);
172 self.index.insert(key, Location::Main(idx));
173 } else {
174 self.evict_small_if_full();
176 let idx = self.alloc_entry(key.clone(), value);
177 self.small.push_back(idx);
178 self.index.insert(key, Location::Small(idx));
179 }
180
181 None
182 }
183
184 pub fn remove(&mut self, key: &K) -> Option<V> {
186 let loc = self.index.remove(key)?;
187 let idx = loc.idx();
188
189 match loc {
193 Location::Small(_) => {
194 if let Some(pos) = self.small.iter().position(|&i| i == idx) {
195 self.small.remove(pos);
196 }
197 }
198 Location::Main(_) => {
199 if let Some(pos) = self.main.iter().position(|&i| i == idx) {
200 self.main.remove(pos);
201 }
202 }
203 }
204
205 self.free_entry(idx)
206 }
207
208 pub fn len(&self) -> usize {
210 self.index.len()
211 }
212
213 pub fn is_empty(&self) -> bool {
215 self.index.is_empty()
216 }
217
218 pub fn capacity(&self) -> usize {
220 self.small_cap + self.main_cap
221 }
222
223 pub fn stats(&self) -> S3FifoStats {
225 S3FifoStats {
226 hits: self.hits,
227 misses: self.misses,
228 small_size: self.small.len(),
229 main_size: self.main.len(),
230 ghost_size: self.ghost.len(),
231 capacity: self.small_cap + self.main_cap,
232 }
233 }
234
235 pub fn clear(&mut self) {
237 self.index.clear();
238 self.entries.clear();
239 self.free_indices.clear();
240 self.small.clear();
241 self.main.clear();
242 self.ghost.clear();
243 self.hits = 0;
244 self.misses = 0;
245 }
246
247 pub fn contains_key(&self, key: &K) -> bool {
249 self.index.contains_key(key)
250 }
251
252 fn alloc_entry(&mut self, key: K, value: V) -> usize {
256 let entry = Some(Entry {
257 key,
258 value,
259 freq: 0,
260 });
261
262 if let Some(idx) = self.free_indices.pop() {
263 self.entries[idx] = entry;
264 idx
265 } else {
266 let idx = self.entries.len();
267 self.entries.push(entry);
268 idx
269 }
270 }
271
272 fn free_entry(&mut self, idx: usize) -> Option<V> {
274 let entry = self.entries[idx].take()?;
275 self.free_indices.push(idx);
276 Some(entry.value)
277 }
278
279 fn remove_from_ghost(&mut self, key: &K) -> bool {
281 if let Some(pos) = self.ghost.iter().position(|k| k == key) {
282 self.ghost.remove(pos);
283 true
284 } else {
285 false
286 }
287 }
288
289 fn evict_small_if_full(&mut self) {
291 while self.small.len() >= self.small_cap {
292 if let Some(idx) = self.small.pop_front() {
293 let freq = self.entries[idx]
295 .as_ref()
296 .expect("S3Fifo invariant violated: valid index required")
297 .freq;
298
299 if freq > 0 {
300 self.entries[idx]
302 .as_mut()
303 .expect("S3Fifo invariant violated: valid index required")
304 .freq = 0;
305
306 let key = self.entries[idx]
308 .as_ref()
309 .expect("S3Fifo invariant violated: valid index required")
310 .key
311 .clone();
312
313 self.evict_main_if_full();
314
315 self.index.insert(key, Location::Main(idx));
316 self.main.push_back(idx);
317 } else {
318 let entry = self.entries[idx]
321 .take()
322 .expect("S3Fifo invariant violated: valid index required");
323 self.free_indices.push(idx);
324
325 self.index.remove(&entry.key);
326
327 if self.ghost.len() >= self.ghost_cap {
328 self.ghost.pop_front();
329 }
330 self.ghost.push_back(entry.key);
331 }
332 }
333 }
334 }
335
336 fn evict_main_if_full(&mut self) {
338 while self.main.len() >= self.main_cap {
339 if let Some(idx) = self.main.pop_front() {
340 let freq = self.entries[idx]
341 .as_ref()
342 .expect("S3Fifo invariant violated: valid index required")
343 .freq;
344
345 if freq > 0 {
346 self.entries[idx]
348 .as_mut()
349 .expect("S3Fifo invariant violated: valid index required")
350 .freq -= 1;
351 self.main.push_back(idx);
352 } else {
353 let entry = self.entries[idx]
355 .take()
356 .expect("S3Fifo invariant violated: valid index required");
357 self.free_indices.push(idx);
358 self.index.remove(&entry.key);
359 }
360 }
361 }
362 }
363}
364
365impl<K, V> std::fmt::Debug for S3Fifo<K, V> {
366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367 f.debug_struct("S3Fifo")
368 .field("small", &self.small.len())
369 .field("main", &self.main.len())
370 .field("ghost", &self.ghost.len())
371 .field("hits", &self.hits)
372 .field("misses", &self.misses)
373 .finish()
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[test]
382 fn empty_cache() {
383 let cache: S3Fifo<&str, i32> = S3Fifo::new(10);
384 assert!(cache.is_empty());
385 assert_eq!(cache.len(), 0);
386 }
387
388 #[test]
389 fn insert_and_get() {
390 let mut cache = S3Fifo::new(10);
391 cache.insert("key1", 42);
392 assert_eq!(cache.get(&"key1"), Some(&42));
393 assert_eq!(cache.len(), 1);
394 }
395
396 #[test]
397 fn miss_returns_none() {
398 let mut cache: S3Fifo<&str, i32> = S3Fifo::new(10);
399 assert_eq!(cache.get(&"missing"), None);
400 }
401
402 #[test]
403 fn update_existing_key() {
404 let mut cache = S3Fifo::new(10);
405 cache.insert("key1", 1);
406 let old = cache.insert("key1", 2);
407 assert_eq!(old, Some(1));
408 assert_eq!(cache.get(&"key1"), Some(&2));
409 assert_eq!(cache.len(), 1);
410 }
411
412 #[test]
413 fn remove_key() {
414 let mut cache = S3Fifo::new(10);
415 cache.insert("key1", 42);
416 let removed = cache.remove(&"key1");
417 assert_eq!(removed, Some(42));
418 assert!(cache.is_empty());
419 assert_eq!(cache.get(&"key1"), None);
420 }
421
422 #[test]
423 fn remove_nonexistent() {
424 let mut cache: S3Fifo<&str, i32> = S3Fifo::new(10);
425 assert_eq!(cache.remove(&"missing"), None);
426 }
427
428 #[test]
429 fn eviction_at_capacity() {
430 let mut cache = S3Fifo::new(5);
431 for i in 0..10 {
432 cache.insert(i, i * 10);
433 }
434 assert!(cache.len() <= cache.capacity());
436 }
437
438 #[test]
439 fn small_to_main_promotion() {
440 let mut cache = S3Fifo::new(10); cache.insert("keep", 1);
445 cache.get(&"keep"); cache.insert("new", 2);
449
450 assert_eq!(cache.get(&"keep"), Some(&1));
452 }
453
454 #[test]
455 fn ghost_readmission() {
456 let mut cache = S3Fifo::new(10); cache.insert("ghost_key", 1);
462 cache.insert("displacer", 2); assert_eq!(cache.get(&"ghost_key"), None);
466
467 cache.insert("ghost_key", 3);
469 assert_eq!(cache.get(&"ghost_key"), Some(&3));
470 }
471
472 #[test]
473 fn stats_tracking() {
474 let mut cache = S3Fifo::new(20);
476 cache.insert("a", 1);
477 cache.insert("b", 2);
478 cache.get(&"a"); cache.get(&"a"); cache.get(&"c"); let stats = cache.stats();
483 assert_eq!(stats.hits, 2);
484 assert_eq!(stats.misses, 2); }
487
488 #[test]
489 fn clear_resets() {
490 let mut cache = S3Fifo::new(10);
491 cache.insert("a", 1);
492 cache.insert("b", 2);
493 cache.get(&"a");
494 cache.clear();
495
496 assert!(cache.is_empty());
497 assert_eq!(cache.len(), 0);
498 let stats = cache.stats();
499 assert_eq!(stats.hits, 0);
500 assert_eq!(stats.misses, 0);
501 assert_eq!(stats.ghost_size, 0);
502 }
503
504 #[test]
505 fn contains_key() {
506 let mut cache = S3Fifo::new(10);
507 cache.insert("a", 1);
508 assert!(cache.contains_key(&"a"));
509 assert!(!cache.contains_key(&"b"));
510 }
511
512 #[test]
513 fn capacity_split() {
514 let cache: S3Fifo<i32, i32> = S3Fifo::new(100);
515 assert_eq!(cache.capacity(), 100);
516 assert_eq!(cache.small_cap, 10);
517 assert_eq!(cache.main_cap, 90);
518 assert_eq!(cache.ghost_cap, 10);
519 }
520
521 #[test]
522 fn minimum_capacity() {
523 let cache: S3Fifo<i32, i32> = S3Fifo::new(0);
524 assert!(cache.capacity() >= 2);
525 }
526
527 #[test]
528 fn freq_capped_at_3() {
529 let mut cache = S3Fifo::new(10);
530 cache.insert("a", 1);
531 for _ in 0..10 {
532 cache.get(&"a");
533 }
534 assert_eq!(cache.get(&"a"), Some(&1));
536 }
537
538 #[test]
539 fn main_eviction_gives_second_chance() {
540 let mut cache = S3Fifo::new(5); for i in 0..4 {
545 cache.insert(i, i);
546 cache.get(&i);
548 }
549
550 for i in 10..20 {
552 cache.insert(i, i);
553 }
554
555 assert!(cache.len() <= cache.capacity());
557 }
558
559 #[test]
560 fn debug_format() {
561 let cache: S3Fifo<&str, i32> = S3Fifo::new(10);
562 let debug = format!("{cache:?}");
563 assert!(debug.contains("S3Fifo"));
564 assert!(debug.contains("small"));
565 assert!(debug.contains("main"));
566 }
567
568 #[test]
569 fn large_workload() {
570 let mut cache = S3Fifo::new(100);
571
572 for i in 0..200 {
575 cache.insert(i, i * 10);
576 if i >= 50 {
578 for hot in 50..std::cmp::min(i, 100) {
579 cache.get(&hot);
580 }
581 }
582 }
583
584 let mut hot_hits = 0;
586 for i in 50..100 {
587 if cache.get(&i).is_some() {
588 hot_hits += 1;
589 }
590 }
591
592 assert!(hot_hits > 20, "hot set retention: {hot_hits}/50");
594 }
595
596 #[test]
597 fn scan_resistance() {
598 let mut cache = S3Fifo::new(100);
599
600 for i in 0..50 {
602 cache.insert(i, i);
603 cache.get(&i);
604 cache.get(&i);
605 }
606
607 for i in 1000..2000 {
609 cache.insert(i, i);
610 }
611
612 let mut survivors = 0;
614 for i in 0..50 {
615 if cache.get(&i).is_some() {
616 survivors += 1;
617 }
618 }
619
620 assert!(
622 survivors > 10,
623 "scan resistance: {survivors}/50 working set items survived"
624 );
625 }
626
627 #[test]
628 fn ghost_size_bounded() {
629 let mut cache = S3Fifo::new(10);
630
631 for i in 0..100 {
633 cache.insert(i, i);
634 }
635
636 let stats = cache.stats();
637 assert!(
638 stats.ghost_size <= cache.ghost_cap,
639 "ghost should be bounded: {} <= {}",
640 stats.ghost_size,
641 cache.ghost_cap
642 );
643 }
644}