#[cfg(feature = "metrics")]
use crate::metrics::metrics_impl::ClockProMetrics;
#[cfg(feature = "metrics")]
use crate::metrics::snapshot::ClockProMetricsSnapshot;
#[cfg(feature = "metrics")]
use crate::metrics::traits::{
ClockProMetricsRecorder, CoreMetricsRecorder, MetricsSnapshotProvider,
};
use crate::prelude::ReadOnlyCache;
use crate::traits::{CoreCache, MutableCache};
use rustc_hash::FxHashMap;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PageStatus {
Cold,
Hot,
}
#[derive(Debug, Clone)]
struct Entry<K, V> {
key: K,
value: V,
status: PageStatus,
referenced: bool,
}
#[derive(Debug, Clone)]
struct GhostEntry<K> {
key: K,
}
pub struct ClockProCache<K, V> {
index: FxHashMap<K, usize>,
entries: Vec<Option<Entry<K, V>>>,
ghost: Vec<Option<GhostEntry<K>>>,
ghost_index: FxHashMap<K, usize>,
hand_cold: usize,
hand_hot: usize,
ghost_hand: usize,
len: usize,
hot_count: usize,
ghost_len: usize,
capacity: usize,
ghost_capacity: usize,
target_hot_ratio: f64,
#[cfg(feature = "metrics")]
metrics: ClockProMetrics,
}
impl<K, V> ClockProCache<K, V>
where
K: Clone + Eq + Hash,
{
#[inline]
pub fn new(capacity: usize) -> Self {
Self::with_ghost_capacity(capacity, capacity)
}
#[inline]
pub fn with_ghost_capacity(capacity: usize, ghost_capacity: usize) -> Self {
let mut entries = Vec::with_capacity(capacity);
entries.resize_with(capacity, || None);
let mut ghost = Vec::with_capacity(ghost_capacity);
ghost.resize_with(ghost_capacity, || None);
Self {
index: FxHashMap::with_capacity_and_hasher(capacity, Default::default()),
entries,
ghost,
ghost_index: FxHashMap::with_capacity_and_hasher(ghost_capacity, Default::default()),
hand_cold: 0,
hand_hot: 0,
ghost_hand: 0,
len: 0,
hot_count: 0,
ghost_len: 0,
capacity,
ghost_capacity,
target_hot_ratio: 0.5, #[cfg(feature = "metrics")]
metrics: ClockProMetrics::default(),
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
pub fn hot_count(&self) -> usize {
self.hot_count
}
#[inline]
pub fn cold_count(&self) -> usize {
self.len - self.hot_count
}
#[inline]
pub fn ghost_count(&self) -> usize {
self.ghost_len
}
#[inline]
fn is_ghost(&self, key: &K) -> bool {
self.ghost_index.contains_key(key)
}
#[inline]
fn remove_ghost(&mut self, key: &K) {
if let Some(slot) = self.ghost_index.remove(key) {
self.ghost[slot] = None;
self.ghost_len -= 1;
}
}
#[inline]
fn add_ghost(&mut self, key: K) {
if self.ghost_index.contains_key(&key) {
return;
}
let slot = self.ghost_hand;
if let Some(old) = self.ghost[slot].take() {
self.ghost_index.remove(&old.key);
self.ghost_len -= 1;
}
self.ghost[slot] = Some(GhostEntry { key: key.clone() });
self.ghost_index.insert(key, slot);
self.ghost_len += 1;
self.ghost_hand = (self.ghost_hand + 1) % self.ghost_capacity;
}
#[inline]
fn find_slot(&mut self) -> usize {
if self.len < self.capacity {
for _ in 0..self.capacity {
if self.entries[self.hand_cold].is_none() {
let slot = self.hand_cold;
self.hand_cold = (self.hand_cold + 1) % self.capacity;
return slot;
}
self.hand_cold = (self.hand_cold + 1) % self.capacity;
}
}
self.evict()
}
#[inline]
fn evict(&mut self) -> usize {
#[cfg(feature = "metrics")]
self.metrics.record_evict_call();
let max_iterations = self.capacity * 2;
let max_hot = ((self.capacity as f64) * self.target_hot_ratio).ceil() as usize;
let max_hot = max_hot.max(1).min(self.capacity.saturating_sub(1));
for _ in 0..max_iterations {
if let Some(entry) = &mut self.entries[self.hand_cold] {
match entry.status {
PageStatus::Cold => {
if entry.referenced {
entry.status = PageStatus::Hot;
entry.referenced = false;
self.hot_count += 1;
#[cfg(feature = "metrics")]
self.metrics.record_cold_to_hot_promotion();
} else {
let slot = self.hand_cold;
let key = entry.key.clone();
self.index.remove(&key);
self.entries[slot] = None;
self.len -= 1;
self.add_ghost(key);
#[cfg(feature = "metrics")]
{
self.metrics.record_evicted_entry();
self.metrics.record_test_insertion();
}
self.hand_cold = (self.hand_cold + 1) % self.capacity;
return slot;
}
},
PageStatus::Hot => {
if self.hot_count > max_hot {
if entry.referenced {
entry.referenced = false;
} else {
entry.status = PageStatus::Cold;
self.hot_count -= 1;
#[cfg(feature = "metrics")]
self.metrics.record_hot_to_cold_demotion();
}
} else if entry.referenced {
entry.referenced = false;
}
},
}
}
self.hand_cold = (self.hand_cold + 1) % self.capacity;
}
let slot = self.hand_cold;
if let Some(entry) = &self.entries[slot] {
let key = entry.key.clone();
self.index.remove(&key);
if entry.status == PageStatus::Hot {
self.hot_count -= 1;
}
self.add_ghost(key);
#[cfg(feature = "metrics")]
{
self.metrics.record_evicted_entry();
self.metrics.record_test_insertion();
}
}
self.entries[slot] = None;
self.len -= 1;
self.hand_cold = (self.hand_cold + 1) % self.capacity;
slot
}
}
impl<K, V> ReadOnlyCache<K, V> for ClockProCache<K, V>
where
K: Clone + Eq + Hash,
{
#[inline]
fn contains(&self, key: &K) -> bool {
self.index.contains_key(key)
}
#[inline]
fn len(&self) -> usize {
self.len
}
#[inline]
fn capacity(&self) -> usize {
self.capacity
}
}
impl<K, V> CoreCache<K, V> for ClockProCache<K, V>
where
K: Clone + Eq + Hash,
{
#[inline]
fn insert(&mut self, key: K, value: V) -> Option<V> {
#[cfg(feature = "metrics")]
self.metrics.record_insert_call();
if self.capacity == 0 {
return None;
}
if let Some(&slot) = self.index.get(&key) {
let entry = self.entries[slot].as_mut().unwrap();
let old = std::mem::replace(&mut entry.value, value);
entry.referenced = true;
#[cfg(feature = "metrics")]
self.metrics.record_insert_update();
return Some(old);
}
let was_ghost = self.is_ghost(&key);
if was_ghost {
self.remove_ghost(&key);
self.target_hot_ratio = (self.target_hot_ratio + 0.05).min(0.9);
}
let slot = self.find_slot();
let status = if was_ghost {
self.hot_count += 1;
PageStatus::Hot } else {
PageStatus::Cold };
self.entries[slot] = Some(Entry {
key: key.clone(),
value,
status,
referenced: false,
});
self.index.insert(key, slot);
self.len += 1;
#[cfg(feature = "metrics")]
self.metrics.record_insert_new();
None
}
#[inline]
fn get(&mut self, key: &K) -> Option<&V> {
if let Some(&slot) = self.index.get(key) {
let entry = self.entries[slot].as_mut()?;
entry.referenced = true;
#[cfg(feature = "metrics")]
self.metrics.record_get_hit();
Some(&entry.value)
} else {
#[cfg(feature = "metrics")]
{
self.metrics.record_get_miss();
if self.ghost_index.contains_key(key) {
self.metrics.record_test_hit();
}
}
None
}
}
fn clear(&mut self) {
#[cfg(feature = "metrics")]
self.metrics.record_clear();
self.index.clear();
self.ghost_index.clear();
for entry in &mut self.entries {
*entry = None;
}
for ghost in &mut self.ghost {
*ghost = None;
}
self.len = 0;
self.hot_count = 0;
self.ghost_len = 0;
self.hand_cold = 0;
self.hand_hot = 0;
self.ghost_hand = 0;
self.target_hot_ratio = 0.5;
}
}
impl<K, V> MutableCache<K, V> for ClockProCache<K, V>
where
K: Clone + Eq + Hash,
{
#[inline]
fn remove(&mut self, key: &K) -> Option<V> {
let slot = self.index.remove(key)?;
let entry = self.entries[slot].take()?;
self.len -= 1;
if entry.status == PageStatus::Hot {
self.hot_count -= 1;
}
Some(entry.value)
}
}
impl<K, V> std::fmt::Debug for ClockProCache<K, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClockProCache")
.field("len", &self.len)
.field("capacity", &self.capacity)
.field("hot_count", &self.hot_count)
.field("ghost_len", &self.ghost_len)
.field("target_hot_ratio", &self.target_hot_ratio)
.finish()
}
}
impl<K, V> Clone for ClockProCache<K, V>
where
K: Clone + Eq + Hash,
V: Clone,
{
fn clone(&self) -> Self {
Self {
index: self.index.clone(),
entries: self.entries.clone(),
ghost: self.ghost.clone(),
ghost_index: self.ghost_index.clone(),
hand_cold: self.hand_cold,
hand_hot: self.hand_hot,
ghost_hand: self.ghost_hand,
len: self.len,
hot_count: self.hot_count,
ghost_len: self.ghost_len,
capacity: self.capacity,
ghost_capacity: self.ghost_capacity,
target_hot_ratio: self.target_hot_ratio,
#[cfg(feature = "metrics")]
metrics: self.metrics,
}
}
}
impl<K, V> Default for ClockProCache<K, V>
where
K: Clone + Eq + Hash,
{
fn default() -> Self {
Self::new(64)
}
}
#[cfg(feature = "metrics")]
impl<K, V> ClockProCache<K, V>
where
K: Clone + Eq + Hash,
{
pub fn metrics_snapshot(&self) -> ClockProMetricsSnapshot {
ClockProMetricsSnapshot {
get_calls: self.metrics.get_calls,
get_hits: self.metrics.get_hits,
get_misses: self.metrics.get_misses,
insert_calls: self.metrics.insert_calls,
insert_updates: self.metrics.insert_updates,
insert_new: self.metrics.insert_new,
evict_calls: self.metrics.evict_calls,
evicted_entries: self.metrics.evicted_entries,
cold_to_hot_promotions: self.metrics.cold_to_hot_promotions,
hot_to_cold_demotions: self.metrics.hot_to_cold_demotions,
test_insertions: self.metrics.test_insertions,
test_hits: self.metrics.test_hits,
cache_len: self.len,
capacity: self.capacity,
}
}
}
#[cfg(feature = "metrics")]
impl<K, V> MetricsSnapshotProvider<ClockProMetricsSnapshot> for ClockProCache<K, V>
where
K: Clone + Eq + Hash,
{
fn snapshot(&self) -> ClockProMetricsSnapshot {
self.metrics_snapshot()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::MutableCache;
#[test]
fn test_basic_operations() {
let mut cache = ClockProCache::new(3);
assert!(cache.insert("a", 1).is_none());
assert!(cache.insert("b", 2).is_none());
assert!(cache.insert("c", 3).is_none());
assert_eq!(cache.len(), 3);
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.get(&"b"), Some(&2));
assert_eq!(cache.get(&"c"), Some(&3));
}
#[test]
fn test_update_existing() {
let mut cache = ClockProCache::new(3);
cache.insert("a", 1);
let old = cache.insert("a", 10);
assert_eq!(old, Some(1));
assert_eq!(cache.get(&"a"), Some(&10));
assert_eq!(cache.len(), 1);
}
#[test]
fn test_eviction() {
let mut cache = ClockProCache::new(3);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("c", 3);
cache.insert("d", 4);
assert_eq!(cache.len(), 3);
let count = [
cache.contains(&"a"),
cache.contains(&"b"),
cache.contains(&"c"),
]
.iter()
.filter(|&&x| x)
.count();
assert_eq!(count, 2);
assert!(cache.contains(&"d"));
}
#[test]
fn test_hot_cold_promotion() {
let mut cache = ClockProCache::new(4);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("c", 3);
cache.insert("d", 4);
assert_eq!(cache.cold_count(), 4);
assert_eq!(cache.hot_count(), 0);
cache.get(&"a");
cache.get(&"a");
cache.insert("e", 5);
cache.insert("f", 6);
assert!(cache.contains(&"a"));
}
#[test]
fn test_ghost_hit() {
let mut cache = ClockProCache::new(2);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("c", 3);
assert!(!cache.contains(&"a"));
assert!(cache.is_ghost(&"a"));
cache.insert("a", 10);
assert!(cache.contains(&"a"));
assert!(!cache.is_ghost(&"a"));
assert!(cache.hot_count() >= 1); }
#[test]
fn test_scan_resistance() {
let mut cache = ClockProCache::new(100);
for i in 0..50 {
cache.insert(i, i);
cache.get(&i); }
for i in 1000..2000 {
cache.insert(i, i);
}
let survived: usize = (0..50).filter(|i| cache.contains(i)).count();
assert!(
survived > 10,
"Expected scan resistance: {} of 50 survived",
survived
);
}
#[test]
fn test_remove() {
let mut cache = ClockProCache::new(3);
cache.insert("a", 1);
cache.insert("b", 2);
let removed = cache.remove(&"a");
assert_eq!(removed, Some(1));
assert!(!cache.contains(&"a"));
assert_eq!(cache.len(), 1);
assert!(cache.remove(&"z").is_none());
}
#[test]
fn test_clear() {
let mut cache = ClockProCache::new(3);
cache.insert("a", 1);
cache.insert("b", 2);
cache.get(&"a");
cache.clear();
assert_eq!(cache.len(), 0);
assert_eq!(cache.hot_count(), 0);
assert_eq!(cache.ghost_count(), 0);
assert!(cache.is_empty());
}
#[test]
fn test_capacity_one() {
let mut cache = ClockProCache::new(1);
cache.insert("a", 1);
assert_eq!(cache.get(&"a"), Some(&1));
cache.insert("b", 2);
assert_eq!(cache.len(), 1);
assert!(cache.contains(&"b"));
}
#[test]
fn test_contains_no_side_effect() {
let mut cache = ClockProCache::new(3);
cache.insert("a", 1);
let _ = cache.contains(&"a");
let _ = cache.contains(&"a");
assert_eq!(cache.cold_count(), 1);
}
#[test]
fn test_ghost_capacity() {
let mut cache: ClockProCache<u64, u64> = ClockProCache::with_ghost_capacity(2, 5);
cache.insert(0, 1);
cache.insert(1, 2);
for i in 10..15 {
cache.insert(i, i);
}
assert!(cache.ghost_count() > 0);
assert!(cache.ghost_count() <= 5);
}
#[test]
fn test_debug_impl() {
let mut cache = ClockProCache::new(10);
cache.insert("a", 1);
cache.insert("b", 2);
let debug_str = format!("{:?}", cache);
assert!(debug_str.contains("ClockProCache"));
assert!(debug_str.contains("len"));
assert!(debug_str.contains("capacity"));
}
#[test]
fn test_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<ClockProCache<String, i32>>();
assert_sync::<ClockProCache<String, i32>>();
}
#[test]
fn test_clone() {
let mut cache = ClockProCache::new(5);
cache.insert("a", 1);
cache.insert("b", 2);
cache.get(&"a");
let cloned = cache.clone();
assert_eq!(cloned.len(), 2);
assert_eq!(cloned.capacity(), 5);
assert_eq!(cloned.hot_count(), cache.hot_count());
assert_eq!(cloned.ghost_count(), cache.ghost_count());
}
#[test]
fn test_default() {
let cache: ClockProCache<String, i32> = ClockProCache::default();
assert_eq!(cache.capacity(), 64);
assert!(cache.is_empty());
}
}