#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use core::{
hash::Hash,
num::NonZeroUsize,
sync::atomic::{AtomicBool, Ordering},
};
use hashbrown::HashMap;
type Hasher = ahash::RandomState;
struct Slot<K, V> {
key: K,
value: V,
referenced: AtomicBool,
live: bool,
}
pub struct Clock<K, V> {
index: HashMap<K, usize, Hasher>,
slots: Vec<Slot<K, V>>,
free: Vec<usize>,
hand: usize,
capacity: usize,
}
impl<K: Hash + Eq + Clone, V> Clock<K, V> {
pub fn new(capacity: NonZeroUsize) -> Self {
let capacity = capacity.get();
Self {
index: HashMap::with_capacity_and_hasher(capacity, Hasher::default()),
slots: Vec::with_capacity(capacity),
free: Vec::new(),
hand: 0,
capacity,
}
}
#[inline]
pub const fn capacity(&self) -> usize {
self.capacity
}
#[inline]
pub fn len(&self) -> usize {
self.index.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.index.is_empty()
}
#[inline]
pub fn contains(&self, key: &K) -> bool {
self.index.contains_key(key)
}
#[inline]
pub fn peek(&self, key: &K) -> Option<&V> {
let &slot = self.index.get(key)?;
Some(&self.slots[slot].value)
}
#[inline]
pub fn get(&self, key: &K) -> Option<&V> {
let &index = self.index.get(key)?;
let slot = &self.slots[index];
if !slot.referenced.load(Ordering::Relaxed) {
slot.referenced.store(true, Ordering::Relaxed);
}
Some(&slot.value)
}
#[inline]
pub fn get_at(&self, slot: usize, key: &K) -> Option<&V> {
let slot = self.slots.get(slot)?;
if !slot.live || slot.key != *key {
return None;
}
if !slot.referenced.load(Ordering::Relaxed) {
slot.referenced.store(true, Ordering::Relaxed);
}
Some(&slot.value)
}
#[inline]
pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
let &index = self.index.get(key)?;
let slot = &mut self.slots[index];
slot.referenced.store(true, Ordering::Relaxed);
Some(&mut slot.value)
}
pub fn put(&mut self, key: K, value: V) -> Option<V> {
if let Some(&index) = self.index.get(&key) {
let slot = &mut self.slots[index];
slot.referenced.store(true, Ordering::Relaxed);
return Some(core::mem::replace(&mut slot.value, value));
}
self.insert_value(key, value);
None
}
pub fn get_or_insert_with<F: FnOnce() -> V>(&mut self, key: K, f: F) -> &V {
let slot = match self.index.get(&key) {
Some(&slot) => {
self.slots[slot].referenced.store(true, Ordering::Relaxed);
slot
}
None => self.insert_value(key, f()),
};
&self.slots[slot].value
}
pub fn try_get_or_insert_with<F: FnOnce() -> Result<V, E>, E>(
&mut self,
key: K,
f: F,
) -> Result<&V, E> {
let slot = match self.index.get(&key) {
Some(&slot) => {
self.slots[slot].referenced.store(true, Ordering::Relaxed);
slot
}
None => self.insert_value(key, f()?),
};
Ok(&self.slots[slot].value)
}
pub fn get_or_insert_mut<F: FnOnce() -> V>(&mut self, key: K, make: F) -> (usize, &mut V) {
let slot = match self.index.get(&key) {
Some(&slot) => {
self.slots[slot].referenced.store(true, Ordering::Relaxed);
slot
}
None => match self.take_slot() {
Some(slot) => {
self.slots[slot].key = key.clone();
self.slots[slot].referenced.store(true, Ordering::Relaxed);
self.slots[slot].live = true;
self.index.insert(key, slot);
slot
}
None => {
let value = make();
self.grow(key, value)
}
},
};
(slot, &mut self.slots[slot].value)
}
pub fn remove(&mut self, key: &K) -> bool {
match self.index.remove(key) {
Some(slot) => {
self.slots[slot].referenced.store(false, Ordering::Relaxed);
self.slots[slot].live = false;
self.free.push(slot);
true
}
None => false,
}
}
pub fn retain<F: FnMut(&K, &V) -> bool>(&mut self, mut keep: F) {
let Self {
index, slots, free, ..
} = self;
index.retain(|key, &mut slot| {
let keep = keep(key, &slots[slot].value);
if !keep {
slots[slot].referenced.store(false, Ordering::Relaxed);
slots[slot].live = false;
free.push(slot);
}
keep
});
}
pub fn clear(&mut self) {
self.index.clear();
self.slots.clear();
self.free.clear();
self.hand = 0;
}
fn grow(&mut self, key: K, value: V) -> usize {
let slot = self.slots.len();
self.index.insert(key.clone(), slot);
self.slots.push(Slot {
key,
value,
referenced: AtomicBool::new(true),
live: true,
});
slot
}
fn insert_value(&mut self, key: K, value: V) -> usize {
match self.take_slot() {
Some(slot) => {
self.slots[slot].key = key.clone();
self.slots[slot].value = value;
self.slots[slot].referenced.store(true, Ordering::Relaxed);
self.slots[slot].live = true;
self.index.insert(key, slot);
slot
}
None => self.grow(key, value),
}
}
fn take_slot(&mut self) -> Option<usize> {
if let Some(slot) = self.free.pop() {
return Some(slot);
}
if self.slots.len() < self.capacity {
return None;
}
let len = self.slots.len();
while self.slots[self.hand].referenced.load(Ordering::Relaxed) {
self.slots[self.hand]
.referenced
.store(false, Ordering::Relaxed);
self.hand = (self.hand + 1) % len;
}
let slot = self.hand;
self.hand = (self.hand + 1) % len;
self.index.remove(&self.slots[slot].key);
Some(slot)
}
}
impl<K: Hash + Eq + Clone + Default, V> Clock<K, V> {
pub fn prefill<F: FnMut() -> V>(&mut self, mut make: F) {
let start = self.free.len();
while self.slots.len() < self.capacity {
let slot = self.slots.len();
self.slots.push(Slot {
key: K::default(),
value: make(),
referenced: AtomicBool::new(false),
live: false,
});
self.free.push(slot);
}
self.free[start..].reverse();
}
}
impl<K, V> core::fmt::Debug for Clock<K, V> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Clock")
.field("len", &self.index.len())
.field("capacity", &self.capacity)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::NZUsize;
use core::cell::Cell;
use proptest::prelude::*;
use std::{collections::HashMap, rc::Rc, thread};
impl<K: Hash + Eq + Clone, V> Clock<K, V> {
fn check_invariants(&self) {
assert!(self.slots.len() <= self.capacity);
assert_eq!(self.index.len() + self.free.len(), self.slots.len());
if self.slots.is_empty() {
assert_eq!(self.hand, 0);
} else {
assert!(self.hand < self.slots.len());
}
let free: std::collections::HashSet<usize> = self.free.iter().copied().collect();
assert_eq!(free.len(), self.free.len(), "duplicate free slot");
let mut seen = std::collections::HashSet::new();
for (key, &slot) in &self.index {
assert!(slot < self.slots.len());
assert!(!free.contains(&slot), "slot {slot} both live and free");
assert!(seen.insert(slot), "slot {slot} mapped twice");
assert!(self.slots[slot].key == *key);
assert!(self.slots[slot].live, "indexed slot {slot} not live");
}
for &slot in &self.free {
assert!(!self.slots[slot].live, "free slot {slot} still live");
}
}
}
#[test]
fn test_basic_put_get_peek() {
let mut cache = Clock::new(NZUsize!(2));
assert!(cache.is_empty());
assert_eq!(cache.capacity(), 2);
assert_eq!(cache.put(1u64, 10u64), None);
assert_eq!(cache.put(2, 20), None);
assert_eq!(cache.len(), 2);
assert_eq!(cache.get(&1).copied(), Some(10));
assert_eq!(cache.peek(&2).copied(), Some(20));
assert!(cache.contains(&1));
assert!(!cache.contains(&3));
assert_eq!(cache.get(&3), None);
cache.check_invariants();
}
#[test]
fn test_put_replaces_existing() {
let mut cache = Clock::new(NZUsize!(2));
assert_eq!(cache.put(1u64, 10u64), None);
assert_eq!(cache.put(1, 11), Some(10));
assert_eq!(cache.get(&1).copied(), Some(11));
assert_eq!(cache.len(), 1);
cache.check_invariants();
}
#[test]
fn test_capacity_one_eviction() {
let mut cache = Clock::new(NZUsize!(1));
cache.put(1u64, 10u64);
cache.put(2, 20);
assert!(!cache.contains(&1));
assert_eq!(cache.get(&2).copied(), Some(20));
assert_eq!(cache.len(), 1);
cache.check_invariants();
}
#[test]
fn test_second_chance_protects_referenced_entry() {
let mut cache = Clock::new(NZUsize!(3));
cache.put(1u64, 10u64); cache.put(2, 20); cache.put(3, 30);
cache.put(4, 40);
assert!(!cache.contains(&1));
assert_eq!(cache.get(&2).copied(), Some(20));
cache.put(5, 50);
assert!(cache.contains(&2));
assert!(!cache.contains(&3));
assert!(cache.contains(&4));
assert!(cache.contains(&5));
cache.check_invariants();
}
#[test]
fn test_all_referenced_evicts_hand_position() {
let mut cache = Clock::new(NZUsize!(3));
cache.put(1u64, 10u64);
cache.put(2, 20);
cache.put(3, 30);
assert!(cache.get(&1).is_some());
assert!(cache.get(&2).is_some());
assert!(cache.get(&3).is_some());
cache.put(4, 40);
assert!(!cache.contains(&1));
assert!(cache.contains(&2));
assert!(cache.contains(&3));
assert!(cache.contains(&4));
cache.check_invariants();
}
#[test]
fn test_get_or_insert_with_calls_f_only_on_miss() {
let mut cache = Clock::new(NZUsize!(2));
let calls = Cell::new(0);
let compute = |k: u64| {
calls.set(calls.get() + 1);
k * 100
};
assert_eq!(*cache.get_or_insert_with(1, || compute(1)), 100);
assert_eq!(calls.get(), 1);
assert_eq!(*cache.get_or_insert_with(1, || compute(1)), 100);
assert_eq!(calls.get(), 1);
cache.check_invariants();
}
#[test]
fn test_try_get_or_insert_with_does_not_cache_errors() {
let mut cache = Clock::new(NZUsize!(2));
let err: Result<&u64, &str> = cache.try_get_or_insert_with(1u64, || Err("bad"));
assert_eq!(err, Err("bad"));
assert!(!cache.contains(&1));
let ok: Result<&u64, &str> = cache.try_get_or_insert_with(1, || Ok(10));
assert_eq!(ok, Ok(&10));
assert!(cache.contains(&1));
cache.check_invariants();
}
#[test]
fn test_remove_keeps_slot_for_reuse() {
let makes = Cell::new(0);
let mut cache: Clock<u64, u64> = Clock::new(NZUsize!(2));
cache.get_or_insert_mut(1, || {
makes.set(makes.get() + 1);
10
});
cache.get_or_insert_mut(2, || {
makes.set(makes.get() + 1);
20
});
assert_eq!(makes.get(), 2);
assert_eq!(cache.slots.len(), 2);
assert!(cache.remove(&1));
assert!(!cache.contains(&1));
assert_eq!(cache.len(), 1);
*cache
.get_or_insert_mut(3, || {
makes.set(makes.get() + 1);
30
})
.1 = 30;
assert_eq!(
makes.get(),
2,
"freed slot should be reused, factory not called"
);
assert_eq!(cache.slots.len(), 2);
assert_eq!(cache.get(&3).copied(), Some(30));
assert!(!cache.remove(&999));
cache.check_invariants();
}
#[test]
fn test_retain() {
let mut cache = Clock::new(NZUsize!(4));
for i in 0..4u64 {
cache.put(i, i * 10);
}
cache.retain(|k, _| k % 2 == 0);
assert_eq!(cache.len(), 2);
assert!(cache.contains(&0));
assert!(cache.contains(&2));
assert!(!cache.contains(&1));
assert!(!cache.contains(&3));
cache.put(10, 100);
cache.put(12, 120);
assert_eq!(cache.slots.len(), 4);
assert_eq!(cache.len(), 4);
cache.check_invariants();
}
#[test]
fn test_get_or_insert_mut_reuses_allocations() {
let makes = Cell::new(0);
let mut cache: Clock<u64, u64> = Clock::new(NZUsize!(3));
for k in 0..100u64 {
let (_, v) = cache.get_or_insert_mut(k, || {
makes.set(makes.get() + 1);
0
});
*v = k; }
assert_eq!(makes.get(), 3, "factory should run only during growth");
assert_eq!(cache.slots.len(), 3);
assert_eq!(cache.len(), 3);
cache.check_invariants();
}
#[test]
fn test_prefill_allocates_once_and_reuses() {
let makes = Cell::new(0);
let mut cache: Clock<u64, u64> = Clock::new(NZUsize!(3));
cache.prefill(|| {
makes.set(makes.get() + 1);
0
});
assert_eq!(makes.get(), 3);
assert_eq!(cache.slots.len(), 3);
assert!(cache.is_empty());
cache.check_invariants();
for k in 0..100u64 {
*cache
.get_or_insert_mut(k, || {
makes.set(makes.get() + 1);
0
})
.1 = k;
}
assert_eq!(makes.get(), 3, "prefilled slots must be reused");
assert_eq!(cache.slots.len(), 3);
assert_eq!(cache.len(), 3);
cache.check_invariants();
}
#[test]
fn test_clear() {
let mut cache = Clock::new(NZUsize!(4));
for i in 0..4u64 {
cache.put(i, i);
}
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
cache.put(9, 9);
assert_eq!(cache.get(&9).copied(), Some(9));
cache.check_invariants();
}
#[derive(Clone)]
struct Tracked {
_counter: Rc<Cell<usize>>,
}
impl Drop for Tracked {
fn drop(&mut self) {
self._counter.set(self._counter.get() + 1);
}
}
#[test]
fn test_values_dropped_on_eviction_and_clear() {
let drops = Rc::new(Cell::new(0));
let mut cache: Clock<u64, Tracked> = Clock::new(NZUsize!(2));
for i in 0..2u64 {
cache.put(
i,
Tracked {
_counter: drops.clone(),
},
);
}
assert_eq!(drops.get(), 0);
cache.put(
2,
Tracked {
_counter: drops.clone(),
},
);
assert_eq!(drops.get(), 1);
cache.put(
2,
Tracked {
_counter: drops.clone(),
},
);
assert_eq!(drops.get(), 2);
cache.clear();
assert_eq!(drops.get(), 4);
}
#[test]
fn test_remove_retains_value_until_reuse() {
let drops = Rc::new(Cell::new(0));
let mut cache: Clock<u64, Tracked> = Clock::new(NZUsize!(2));
cache.put(
1,
Tracked {
_counter: drops.clone(),
},
);
assert!(cache.remove(&1));
assert_eq!(drops.get(), 0, "remove must not drop the value");
cache.put(
2,
Tracked {
_counter: drops.clone(),
},
);
assert_eq!(drops.get(), 1);
}
#[test]
fn test_get_at_validates_key() {
let mut cache = Clock::new(NZUsize!(2));
let (slot1, v) = cache.get_or_insert_mut(1u64, || 0u64);
*v = 10;
let (slot2, v) = cache.get_or_insert_mut(2u64, || 0u64);
*v = 20;
assert_eq!(cache.get_at(slot1, &1).copied(), Some(10));
assert_eq!(cache.get_at(slot2, &2).copied(), Some(20));
assert_eq!(cache.get_at(slot1, &2), None);
assert_eq!(cache.get_at(cache.capacity(), &1), None);
cache.check_invariants();
}
#[test]
fn test_get_at_stale_slot_after_eviction() {
let mut cache = Clock::new(NZUsize!(1));
let (slot, v) = cache.get_or_insert_mut(1u64, || 0u64);
*v = 10;
let (reused, v) = cache.get_or_insert_mut(3u64, || 0u64);
*v = 30;
assert_eq!(slot, reused);
assert_eq!(cache.get_at(slot, &1), None);
assert_eq!(cache.get_at(slot, &3).copied(), Some(30));
cache.check_invariants();
}
#[test]
fn test_get_at_freed_slot_is_a_miss() {
let mut cache = Clock::new(NZUsize!(2));
let (slot, v) = cache.get_or_insert_mut(1u64, || 0u64);
*v = 10;
assert!(cache.remove(&1));
assert_eq!(cache.get_at(slot, &1), None);
let (reused, v) = cache.get_or_insert_mut(2u64, || 0u64);
*v = 20;
assert_eq!(reused, slot);
assert_eq!(cache.get_at(slot, &1), None);
assert_eq!(cache.get_at(slot, &2).copied(), Some(20));
cache.check_invariants();
}
#[test]
fn test_get_at_records_use() {
let mut c = Clock::new(NZUsize!(3));
c.put(1u64, 10u64);
c.put(2, 20);
c.put(3, 30);
c.put(4, 40);
let slot = *c.index.get(&2).unwrap();
assert_eq!(c.get_at(slot, &2).copied(), Some(20));
c.put(5, 50);
assert!(c.contains(&2), "get_at must protect key2 from eviction");
assert!(!c.contains(&3));
c.check_invariants();
}
#[test]
fn test_get_or_insert_mut_slot_stable_on_hit() {
let mut cache = Clock::new(NZUsize!(2));
let (slot, v) = cache.get_or_insert_mut(1u64, || 0u64);
*v = 10;
let (hit_slot, v) = cache.get_or_insert_mut(1u64, || unreachable!());
assert_eq!(slot, hit_slot);
assert_eq!(*v, 10);
cache.check_invariants();
}
#[test]
fn test_get_mut() {
let mut cache = Clock::new(NZUsize!(2));
cache.put(1u64, 10u64);
assert_eq!(cache.get_mut(&2), None);
*cache.get_mut(&1).unwrap() = 11;
assert_eq!(cache.get(&1).copied(), Some(11));
cache.check_invariants();
}
#[test]
fn test_peek_does_not_record_use() {
fn setup() -> Clock<u64, u64> {
let mut c = Clock::new(NZUsize!(3));
c.put(1, 10);
c.put(2, 20);
c.put(3, 30);
c.put(4, 40); c
}
let mut c = setup();
assert_eq!(c.peek(&2).copied(), Some(20));
c.put(5, 50);
assert!(!c.contains(&2), "peek must not protect key2 from eviction");
assert!(c.contains(&3));
let mut c = setup();
assert_eq!(c.get(&2).copied(), Some(20));
c.put(5, 50);
assert!(c.contains(&2), "get must protect key2 from eviction");
assert!(!c.contains(&3));
}
#[test]
fn test_concurrent_get_is_sound() {
let mut cache: Clock<u64, u64> = Clock::new(NZUsize!(64));
for i in 0..64u64 {
cache.put(i, i * 10);
}
let cache = &cache;
thread::scope(|s| {
for _ in 0..4 {
s.spawn(move || {
for _ in 0..2_000 {
for i in 0..64u64 {
assert_eq!(cache.get(&i).copied(), Some(i * 10));
}
}
});
}
});
for i in 0..64u64 {
assert_eq!(cache.get(&i).copied(), Some(i * 10));
}
cache.check_invariants();
}
#[derive(Clone, Debug)]
enum Op {
Get(u8),
Peek(u8),
Put(u8, u16),
GetOrInsert(u8, u16),
GetOrInsertMut(u8, u16),
GetMut(u8, u16),
Remove(u8),
Retain(u8),
}
fn op_strategy() -> impl Strategy<Value = Op> {
prop_oneof![
(0u8..16).prop_map(Op::Get),
(0u8..16).prop_map(Op::Peek),
(0u8..16, any::<u16>()).prop_map(|(k, v)| Op::Put(k, v)),
(0u8..16, any::<u16>()).prop_map(|(k, v)| Op::GetOrInsert(k, v)),
(0u8..16, any::<u16>()).prop_map(|(k, v)| Op::GetOrInsertMut(k, v)),
(0u8..16, any::<u16>()).prop_map(|(k, v)| Op::GetMut(k, v)),
(0u8..16).prop_map(Op::Remove),
(0u8..16).prop_map(Op::Retain),
]
}
const KEY_SPACE: u8 = 16;
proptest! {
#[test]
fn prop_invariants_hold(
cap in 1usize..8,
prefill in any::<bool>(),
ops in proptest::collection::vec(op_strategy(), 0..256),
) {
let mut cache: Clock<u8, u16> = Clock::new(NonZeroUsize::new(cap).unwrap());
if prefill {
cache.prefill(|| 0u16);
}
let mut model: HashMap<u8, u16> = HashMap::new();
for op in ops {
match op {
Op::Get(k) => {
let got = cache.get(&k).copied();
prop_assert_eq!(got, cache.peek(&k).copied());
}
Op::Peek(k) => {
let _ = cache.peek(&k);
}
Op::Put(k, v) => {
cache.put(k, v);
model.insert(k, v);
prop_assert_eq!(cache.peek(&k).copied(), Some(v));
}
Op::GetOrInsert(k, v) => {
let stored = *cache.get_or_insert_with(k, || v);
model.insert(k, stored);
prop_assert_eq!(cache.peek(&k).copied(), Some(stored));
}
Op::GetOrInsertMut(k, v) => {
*cache.get_or_insert_mut(k, || v).1 = v;
model.insert(k, v);
prop_assert_eq!(cache.peek(&k).copied(), Some(v));
}
Op::GetMut(k, v) => {
if let Some(slot) = cache.get_mut(&k) {
*slot = v;
model.insert(k, v);
}
}
Op::Remove(k) => {
let had = cache.contains(&k);
prop_assert_eq!(cache.remove(&k), had);
model.remove(&k);
prop_assert!(!cache.contains(&k));
}
Op::Retain(k) => {
cache.retain(|key, _| *key < k);
model.retain(|key, _| *key < k);
prop_assert!(cache.len() <= usize::from(k).min(cap));
}
}
prop_assert!(cache.len() <= cap);
prop_assert!(cache.slots.len() <= cap);
for k in 0..KEY_SPACE {
let present = cache.contains(&k);
prop_assert_eq!(present, cache.peek(&k).is_some());
if present {
prop_assert_eq!(cache.peek(&k).copied(), model.get(&k).copied());
}
}
cache.check_invariants();
}
}
}
}