diff --git a/docs/policies/car.md b/docs/policies/car.md index 90aa9fd..d0c3d4c 100644 --- a/docs/policies/car.md +++ b/docs/policies/car.md @@ -67,10 +67,10 @@ ghost hit in `ghost_frequent` decreases it. ## Example Usage ```rust -use cachekit::policy::car::CARCore; +use cachekit::policy::car::CarCore; use cachekit::traits::{CoreCache, ReadOnlyCache}; -let mut cache = CARCore::new(100); +let mut cache = CarCore::new(100); cache.insert("page1", "content1"); cache.insert("page2", "content2"); assert_eq!(cache.get(&"page1"), Some(&"content1")); diff --git a/src/policy/car.rs b/src/policy/car.rs index 06b0cad..f318c47 100644 --- a/src/policy/car.rs +++ b/src/policy/car.rs @@ -8,7 +8,7 @@ //! //! ```text //! ┌─────────────────────────────────────────────────────────────────────────────┐ -//! │ CARCore Layout │ +//! │ CarCore Layout │ //! │ │ //! │ Slot array with per-ring intrusive circular linked lists: │ //! │ │ @@ -36,7 +36,7 @@ //! //! ## Key Components //! -//! - [`CARCore`]: Main CAR cache implementation +//! - [`CarCore`]: Main CAR cache implementation //! - Two intrusive circular rings: Recent (seen once), Frequent (repeated access) //! - Ghost lists (ghost_recent / ghost_frequent) for evicted keys (ARC-style adaptation) //! - `target_recent_size`: target size for the Recent ring, adjusted by ghost hits @@ -64,10 +64,10 @@ //! ## Example Usage //! //! ``` -//! use cachekit::policy::car::CARCore; +//! use cachekit::policy::car::CarCore; //! use cachekit::traits::{CoreCache, ReadOnlyCache}; //! -//! let mut cache = CARCore::new(100); +//! let mut cache = CarCore::new(100); //! cache.insert("key1", "value1"); //! cache.insert("key2", "value2"); //! @@ -78,7 +78,7 @@ //! //! ## Thread Safety //! -//! - [`CARCore`]: Not thread-safe, designed for single-threaded use +//! - [`CarCore`]: Not thread-safe, designed for single-threaded use //! - For concurrent access, wrap in external synchronization //! //! ## Implementation Notes @@ -106,6 +106,7 @@ use crate::prelude::ReadOnlyCache; use crate::traits::{CoreCache, MutableCache}; use rustc_hash::FxHashMap; use std::hash::Hash; +use std::iter::FusedIterator; /// Which logical ring an entry resides in. #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -126,6 +127,15 @@ struct SlotPayload { value: V, } +impl Clone for SlotPayload { + fn clone(&self) -> Self { + Self { + key: self.key.clone(), + value: self.value.clone(), + } + } +} + /// Core Clock with Adaptive Replacement (CAR) implementation. /// /// Combines ARC's adaptivity (ghost lists + adaptation target) with Clock mechanics: @@ -141,10 +151,10 @@ struct SlotPayload { /// # Example /// /// ``` -/// use cachekit::policy::car::CARCore; +/// use cachekit::policy::car::CarCore; /// use cachekit::traits::{CoreCache, ReadOnlyCache}; /// -/// let mut cache = CARCore::new(100); +/// let mut cache = CarCore::new(100); /// cache.insert("key1", "value1"); /// assert_eq!(cache.get(&"key1"), Some(&"value1")); /// assert_eq!(cache.recent_len() + cache.frequent_len(), cache.len()); @@ -160,10 +170,7 @@ struct SlotPayload { /// - Ref=0: evict to ghost_frequent /// - Ref=1: clear ref, advance, continue #[must_use] -pub struct CARCore -where - K: Clone + Eq + Hash, -{ +pub struct CarCore { /// Key -> slot index. index: FxHashMap, @@ -213,7 +220,7 @@ where metrics: CarMetrics, } -impl CARCore +impl CarCore where K: Clone + Eq + Hash, { @@ -226,10 +233,10 @@ where /// # Example /// /// ``` - /// use cachekit::policy::car::CARCore; + /// use cachekit::policy::car::CarCore; /// use cachekit::traits::ReadOnlyCache; /// - /// let cache: CARCore = CARCore::new(100); + /// let cache: CarCore = CarCore::new(100); /// assert_eq!(cache.capacity(), 100); /// assert!(cache.is_empty()); /// assert_eq!(cache.target_recent_size(), 50); @@ -478,9 +485,9 @@ where /// # Example /// /// ``` - /// use cachekit::policy::car::CARCore; + /// use cachekit::policy::car::CarCore; /// - /// let cache: CARCore = CARCore::new(100); + /// let cache: CarCore = CarCore::new(100); /// assert_eq!(cache.target_recent_size(), 50); /// ``` pub fn target_recent_size(&self) -> usize { @@ -492,10 +499,10 @@ where /// # Example /// /// ``` - /// use cachekit::policy::car::CARCore; + /// use cachekit::policy::car::CarCore; /// use cachekit::traits::CoreCache; /// - /// let mut cache = CARCore::new(100); + /// let mut cache = CarCore::new(100); /// cache.insert("key", "value"); /// assert_eq!(cache.recent_len(), 1); /// ``` @@ -508,27 +515,107 @@ where /// # Example /// /// ``` - /// use cachekit::policy::car::CARCore; + /// use cachekit::policy::car::CarCore; /// use cachekit::traits::CoreCache; /// - /// let mut cache = CARCore::new(100); - /// cache.insert("key", "value"); - /// cache.get(&"key"); // CAR: ref bit set, but stays in recent ring + /// let mut cache = CarCore::new(3); + /// cache.insert("a", 1); + /// cache.insert("b", 2); + /// cache.insert("c", 3); + /// cache.get(&"a"); + /// cache.get(&"b"); + /// // Inserting "d" evicts an unreferenced entry and demotes referenced + /// // entries from Recent to Frequent. + /// cache.insert("d", 4); + /// assert!(cache.frequent_len() > 0); /// ``` pub fn frequent_len(&self) -> usize { self.frequent_len } /// Returns the number of keys in the ghost_recent list (evicted from recent ring). + /// + /// # Example + /// + /// ``` + /// use cachekit::policy::car::CarCore; + /// use cachekit::traits::CoreCache; + /// + /// let mut cache = CarCore::new(2); + /// cache.insert("a", 1); + /// cache.insert("b", 2); + /// cache.insert("c", 3); // evicts one entry to ghost_recent + /// assert!(cache.ghost_recent_len() >= 1); + /// ``` pub fn ghost_recent_len(&self) -> usize { self.ghost_recent.len() } /// Returns the number of keys in the ghost_frequent list (evicted from frequent ring). + /// + /// # Example + /// + /// ``` + /// use cachekit::policy::car::CarCore; + /// use cachekit::traits::CoreCache; + /// + /// let cache: CarCore = CarCore::new(10); + /// assert_eq!(cache.ghost_frequent_len(), 0); + /// ``` pub fn ghost_frequent_len(&self) -> usize { self.ghost_frequent.len() } + /// Returns an iterator over shared references to all cached key-value pairs. + /// + /// Iteration order is unspecified and not guaranteed to follow ring order. + /// + /// # Example + /// + /// ``` + /// use cachekit::policy::car::CarCore; + /// use cachekit::traits::CoreCache; + /// + /// let mut cache = CarCore::new(10); + /// cache.insert("a", 1); + /// cache.insert("b", 2); + /// + /// let entries: Vec<_> = cache.iter().collect(); + /// assert_eq!(entries.len(), 2); + /// ``` + pub fn iter(&self) -> Iter<'_, K, V> { + Iter { + slots: self.slots.iter(), + remaining: self.recent_len + self.frequent_len, + } + } + + /// Returns an iterator over cached keys (shared) and values (mutable). + /// + /// Iteration order is unspecified. + /// + /// # Example + /// + /// ``` + /// use cachekit::policy::car::CarCore; + /// use cachekit::traits::CoreCache; + /// + /// let mut cache = CarCore::new(10); + /// cache.insert("a", 1); + /// cache.insert("b", 2); + /// + /// for (_key, value) in cache.iter_mut() { + /// *value += 10; + /// } + /// assert_eq!(cache.get(&"a"), Some(&11)); + /// ``` + pub fn iter_mut(&mut self) -> IterMut<'_, K, V> { + IterMut { + slots: self.slots.iter_mut(), + remaining: self.recent_len + self.frequent_len, + } + } + /// Validates internal invariants. Available in debug/test builds. /// /// Panics if any invariant is violated. @@ -679,13 +766,13 @@ where } } -impl std::fmt::Debug for CARCore +impl std::fmt::Debug for CarCore where K: Clone + Eq + Hash + std::fmt::Debug, V: std::fmt::Debug, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CARCore") + f.debug_struct("CarCore") .field("capacity", &self.capacity) .field("recent_len", &self.recent_len) .field("frequent_len", &self.frequent_len) @@ -697,7 +784,35 @@ where } } -impl ReadOnlyCache for CARCore +impl Clone for CarCore +where + K: Clone + Eq + Hash, + V: Clone, +{ + fn clone(&self) -> Self { + Self { + index: self.index.clone(), + slots: self.slots.clone(), + referenced: self.referenced.clone(), + ring_kind: self.ring_kind.clone(), + ring_next: self.ring_next.clone(), + ring_prev: self.ring_prev.clone(), + hand_recent: self.hand_recent, + hand_frequent: self.hand_frequent, + free: self.free.clone(), + ghost_recent: self.ghost_recent.clone(), + ghost_frequent: self.ghost_frequent.clone(), + target_recent_size: self.target_recent_size, + recent_len: self.recent_len, + frequent_len: self.frequent_len, + capacity: self.capacity, + #[cfg(feature = "metrics")] + metrics: self.metrics, + } + } +} + +impl ReadOnlyCache for CarCore where K: Clone + Eq + Hash, { @@ -714,7 +829,7 @@ where } } -impl CoreCache for CARCore +impl CoreCache for CarCore where K: Clone + Eq + Hash, { @@ -820,7 +935,7 @@ where } } -impl MutableCache for CARCore +impl MutableCache for CarCore where K: Clone + Eq + Hash, { @@ -840,7 +955,7 @@ where } #[cfg(feature = "metrics")] -impl CARCore +impl CarCore where K: Clone + Eq + Hash, { @@ -868,7 +983,7 @@ where } #[cfg(feature = "metrics")] -impl MetricsSnapshotProvider for CARCore +impl MetricsSnapshotProvider for CarCore where K: Clone + Eq + Hash, { @@ -877,6 +992,127 @@ where } } +// ============================================================================= +// Iterators +// ============================================================================= + +/// Iterator over shared references to cached key-value pairs. +/// +/// Created by [`CarCore::iter`]. +pub struct Iter<'a, K, V> { + slots: std::slice::Iter<'a, Option>>, + remaining: usize, +} + +impl<'a, K, V> Iterator for Iter<'a, K, V> { + type Item = (&'a K, &'a V); + + fn next(&mut self) -> Option { + if let Some(payload) = self.slots.by_ref().flatten().next() { + self.remaining -= 1; + return Some((&payload.key, &payload.value)); + } + None + } + + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) + } +} + +impl ExactSizeIterator for Iter<'_, K, V> {} +impl FusedIterator for Iter<'_, K, V> {} + +/// Iterator over cached keys (shared) and values (mutable). +/// +/// Created by [`CarCore::iter_mut`]. +pub struct IterMut<'a, K, V> { + slots: std::slice::IterMut<'a, Option>>, + remaining: usize, +} + +impl<'a, K, V> Iterator for IterMut<'a, K, V> { + type Item = (&'a K, &'a mut V); + + fn next(&mut self) -> Option { + if let Some(payload) = self.slots.by_ref().flatten().next() { + self.remaining -= 1; + return Some((&payload.key, &mut payload.value)); + } + None + } + + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) + } +} + +impl ExactSizeIterator for IterMut<'_, K, V> {} +impl FusedIterator for IterMut<'_, K, V> {} + +/// Owning iterator over cached key-value pairs. +/// +/// Created by calling [`IntoIterator`] on a [`CarCore`]. +pub struct IntoIter { + slots: std::vec::IntoIter>>, + remaining: usize, +} + +impl Iterator for IntoIter { + type Item = (K, V); + + fn next(&mut self) -> Option { + if let Some(payload) = self.slots.by_ref().flatten().next() { + self.remaining -= 1; + return Some((payload.key, payload.value)); + } + None + } + + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) + } +} + +impl ExactSizeIterator for IntoIter {} +impl FusedIterator for IntoIter {} + +impl IntoIterator for CarCore { + type Item = (K, V); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + IntoIter { + remaining: self.recent_len + self.frequent_len, + slots: self.slots.into_iter(), + } + } +} + +impl<'a, K, V> IntoIterator for &'a CarCore +where + K: Clone + Eq + Hash, +{ + type Item = (&'a K, &'a V); + type IntoIter = Iter<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a, K, V> IntoIterator for &'a mut CarCore +where + K: Clone + Eq + Hash, +{ + type Item = (&'a K, &'a mut V); + type IntoIter = IterMut<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + // ============================================================================= // Tests // ============================================================================= @@ -887,7 +1123,7 @@ mod tests { #[test] fn car_new_cache() { - let cache: CARCore = CARCore::new(100); + let cache: CarCore = CarCore::new(100); assert_eq!(cache.capacity(), 100); assert_eq!(cache.len(), 0); assert!(cache.is_empty()); @@ -899,7 +1135,7 @@ mod tests { #[test] fn car_zero_capacity() { - let mut cache: CARCore<&str, i32> = CARCore::new(0); + let mut cache: CarCore<&str, i32> = CarCore::new(0); assert_eq!(cache.capacity(), 0); assert_eq!(cache.len(), 0); cache.insert("key", 1); @@ -909,7 +1145,7 @@ mod tests { #[test] fn car_insert_and_get() { - let mut cache = CARCore::new(10); + let mut cache = CarCore::new(10); cache.insert("key1", "value1"); assert_eq!(cache.len(), 1); assert_eq!(cache.recent_len(), 1); @@ -925,7 +1161,7 @@ mod tests { #[test] fn car_update_existing() { - let mut cache = CARCore::new(10); + let mut cache = CarCore::new(10); cache.insert("key1", "value1"); let old = cache.insert("key1", "new_value"); assert_eq!(old, Some("value1")); @@ -936,7 +1172,7 @@ mod tests { #[test] fn car_eviction_fills_ghost() { - let mut cache = CARCore::new(2); + let mut cache = CarCore::new(2); cache.insert("a", 1); cache.insert("b", 2); assert_eq!(cache.len(), 2); @@ -950,7 +1186,7 @@ mod tests { #[test] fn car_ghost_hit_ghost_recent() { - let mut cache = CARCore::new(2); + let mut cache = CarCore::new(2); cache.insert("a", 1); cache.insert("b", 2); cache.insert("c", 3); // Evicts "a" to ghost_recent @@ -966,7 +1202,7 @@ mod tests { #[test] fn car_ghost_hit_ghost_frequent() { - let mut cache = CARCore::new(3); + let mut cache = CarCore::new(3); // Fill the recent ring. cache.insert("a", 1); cache.insert("b", 2); @@ -995,7 +1231,7 @@ mod tests { #[test] fn car_remove() { - let mut cache = CARCore::new(10); + let mut cache = CarCore::new(10); cache.insert("key1", "value1"); cache.insert("key2", "value2"); assert_eq!(cache.remove(&"key1"), Some("value1")); @@ -1007,7 +1243,7 @@ mod tests { #[test] fn car_remove_nonexistent() { - let mut cache = CARCore::new(10); + let mut cache = CarCore::new(10); cache.insert("key1", "value1"); assert_eq!(cache.remove(&"missing"), None); assert_eq!(cache.len(), 1); @@ -1016,7 +1252,7 @@ mod tests { #[test] fn car_clear() { - let mut cache = CARCore::new(10); + let mut cache = CarCore::new(10); cache.insert("key1", "value1"); cache.insert("key2", "value2"); cache.get(&"key1"); @@ -1034,7 +1270,7 @@ mod tests { fn car_ref_bit_protects_from_eviction() { // Capacity 3, target=1. a,b,c in recent ring. get(b), get(c) set ref bits. // Insert d: evict from recent ring (|recent|=3>target). Unreferenced "a" evicted first. - let mut cache = CARCore::new(3); + let mut cache = CarCore::new(3); cache.insert("a", 1); cache.insert("b", 2); cache.insert("c", 3); @@ -1052,7 +1288,7 @@ mod tests { #[test] fn car_demotion_recent_to_frequent() { // Fill recent ring, set ref on all, then insert to trigger demotion. - let mut cache = CARCore::new(3); + let mut cache = CarCore::new(3); cache.insert("a", 1); cache.insert("b", 2); cache.insert("c", 3); @@ -1070,7 +1306,7 @@ mod tests { #[test] fn car_adaptation_increases_target_on_ghost_recent_hit() { - let mut cache = CARCore::new(4); + let mut cache = CarCore::new(4); let initial_p = cache.target_recent_size(); cache.insert("a", 1); @@ -1093,7 +1329,7 @@ mod tests { #[test] fn car_multiple_entries() { - let mut cache = CARCore::new(5); + let mut cache = CarCore::new(5); for i in 0..5 { cache.insert(i, i * 10); } @@ -1108,7 +1344,7 @@ mod tests { #[test] fn car_capacity_one() { - let mut cache = CARCore::new(1); + let mut cache = CarCore::new(1); cache.insert("a", 1); assert_eq!(cache.len(), 1); cache.debug_validate_invariants(); @@ -1122,7 +1358,7 @@ mod tests { #[test] fn car_heavy_churn() { - let mut cache = CARCore::new(10); + let mut cache = CarCore::new(10); for i in 0..1000 { cache.insert(i, i * 10); if i % 3 == 0 { @@ -1138,7 +1374,7 @@ mod tests { #[test] fn car_contains_after_eviction() { - let mut cache = CARCore::new(2); + let mut cache = CarCore::new(2); cache.insert("a", 1); cache.insert("b", 2); assert!(cache.contains(&"a")); @@ -1152,7 +1388,7 @@ mod tests { #[test] fn car_insert_after_remove_reuses_slot() { - let mut cache = CARCore::new(3); + let mut cache = CarCore::new(3); cache.insert("a", 1); cache.insert("b", 2); cache.insert("c", 3); @@ -1170,7 +1406,7 @@ mod tests { #[test] fn car_clear_then_reuse() { - let mut cache = CARCore::new(5); + let mut cache = CarCore::new(5); for i in 0..5 { cache.insert(i, i); } @@ -1201,7 +1437,7 @@ mod property_tests { capacity in 1usize..100, ops in prop::collection::vec((0u32..1000, 0u32..100), 0..200) ) { - let mut cache = CARCore::new(capacity); + let mut cache = CarCore::new(capacity); for (key, value) in ops { cache.insert(key, value); prop_assert!(cache.len() <= cache.capacity()); @@ -1215,7 +1451,7 @@ mod property_tests { capacity in 1usize..50, ops in prop::collection::vec((0u32..100, 0u32..100), 0..100) ) { - let mut cache = CARCore::new(capacity); + let mut cache = CarCore::new(capacity); for (key, value) in ops { cache.insert(key, value); cache.debug_validate_invariants(); @@ -1230,7 +1466,7 @@ mod property_tests { key in 0u32..100, value in 0u32..1000 ) { - let mut cache = CARCore::new(capacity); + let mut cache = CarCore::new(capacity); cache.insert(key, value); if cache.contains(&key) { prop_assert_eq!(cache.get(&key), Some(&value)); @@ -1244,7 +1480,7 @@ mod property_tests { capacity in 1usize..50, keys in prop::collection::vec(0u32..100, 1..50) ) { - let mut cache = CARCore::new(capacity); + let mut cache = CarCore::new(capacity); for &key in &keys { cache.insert(key, key * 10); } @@ -1267,7 +1503,7 @@ mod property_tests { capacity in 1usize..50, keys in prop::collection::vec(0u32..100, 1..50) ) { - let mut cache = CARCore::new(capacity); + let mut cache = CarCore::new(capacity); for key in keys { cache.insert(key, key * 10); } @@ -1284,7 +1520,7 @@ mod property_tests { fn prop_second_chance_behavior( capacity in 3usize..10 ) { - let mut cache = CARCore::new(capacity); + let mut cache = CarCore::new(capacity); for i in 0..capacity { cache.insert(i as u32, i as u32); } @@ -1307,7 +1543,7 @@ mod property_tests { key in 0u32..50, values in prop::collection::vec(0u32..100, 1..50) ) { - let mut cache = CARCore::new(capacity); + let mut cache = CarCore::new(capacity); cache.insert(key, 0); let len_after_first = cache.len(); for value in values { @@ -1345,7 +1581,7 @@ mod property_tests { capacity in 1usize..30, ops in prop::collection::vec(operation_strategy(), 0..200) ) { - let mut cache = CARCore::new(capacity); + let mut cache = CarCore::new(capacity); for op in ops { match op { @@ -1365,7 +1601,7 @@ mod property_tests { capacity in 1usize..30, ops in prop::collection::vec((0u32..50, any::()), 0..200) ) { - let mut cache = CARCore::new(capacity); + let mut cache = CarCore::new(capacity); for (key, should_insert) in ops { if should_insert { @@ -1384,7 +1620,7 @@ mod property_tests { fn prop_zero_capacity_noop( ops in prop::collection::vec((0u32..100, 0u32..100), 0..50) ) { - let mut cache = CARCore::::new(0); + let mut cache = CarCore::::new(0); for (key, value) in ops { cache.insert(key, value); prop_assert!(cache.is_empty()); @@ -1399,7 +1635,7 @@ mod property_tests { fn prop_capacity_one_single_entry( keys in prop::collection::vec(0u32..100, 1..50) ) { - let mut cache = CARCore::new(1); + let mut cache = CarCore::new(1); for key in keys { cache.insert(key, key * 10); prop_assert!(cache.len() <= 1); @@ -1419,7 +1655,7 @@ mod fuzz_tests { } let capacity = (data[0] as usize % 50).max(1); - let mut cache = CARCore::new(capacity); + let mut cache = CarCore::new(capacity); let mut idx = 1; while idx + 2 < data.len() { diff --git a/src/policy/clock_pro.rs b/src/policy/clock_pro.rs index a27cbd3..f520a86 100644 --- a/src/policy/clock_pro.rs +++ b/src/policy/clock_pro.rs @@ -140,7 +140,7 @@ enum PageStatus { } /// Entry in the resident buffer. -#[derive(Debug)] +#[derive(Debug, Clone)] struct Entry { key: K, value: V, @@ -149,7 +149,7 @@ struct Entry { } /// Ghost ring entry (key only, no value). -#[derive(Debug)] +#[derive(Debug, Clone)] struct GhostEntry { key: K, } @@ -159,10 +159,20 @@ struct GhostEntry { /// Improves on Clock by distinguishing hot (frequently accessed) and cold /// (candidates for eviction) pages, plus tracking ghost entries for recently /// evicted cold pages. -pub struct ClockProCache -where - K: Clone + Eq + Hash, -{ +/// +/// Implements [`CoreCache`], [`ReadOnlyCache`], and [`MutableCache`]. +/// +/// # Example +/// +/// ``` +/// use cachekit::policy::clock_pro::ClockProCache; +/// use cachekit::traits::{CoreCache, ReadOnlyCache}; +/// +/// let mut cache = ClockProCache::new(100); +/// cache.insert("key", 42); +/// assert_eq!(cache.get(&"key"), Some(&42)); +/// ``` +pub struct ClockProCache { /// Maps keys to their slot index in the entries buffer. index: FxHashMap, /// Circular buffer of resident entries. @@ -193,10 +203,6 @@ where metrics: ClockProMetrics, } -// Safety: ClockProCache uses no interior mutability or non-Send types -unsafe impl Send for ClockProCache where K: Clone + Eq + Hash {} -unsafe impl Sync for ClockProCache where K: Clone + Eq + Hash {} - impl ClockProCache where K: Clone + Eq + Hash, @@ -221,7 +227,19 @@ where /// Creates a new Clock-PRO cache with custom ghost capacity. /// /// A larger ghost capacity can improve hit rates on workloads with - /// periodic re-access patterns. + /// periodic re-access patterns. The ghost ring tracks keys of recently + /// evicted cold pages; a hit on a ghost entry promotes the next insert + /// of that key directly to hot status. + /// + /// # Example + /// + /// ``` + /// use cachekit::policy::clock_pro::ClockProCache; + /// use cachekit::traits::ReadOnlyCache; + /// + /// let cache: ClockProCache = ClockProCache::with_ghost_capacity(100, 200); + /// assert_eq!(cache.capacity(), 100); + /// ``` #[inline] pub fn with_ghost_capacity(capacity: usize, ghost_capacity: usize) -> Self { let mut entries = Vec::with_capacity(capacity); @@ -249,25 +267,75 @@ where } } - /// Returns `true` if the cache is empty. + /// Returns `true` if the cache contains no resident entries. + /// + /// # Example + /// + /// ``` + /// use cachekit::policy::clock_pro::ClockProCache; + /// use cachekit::traits::CoreCache; + /// + /// let mut cache = ClockProCache::new(10); + /// assert!(cache.is_empty()); + /// + /// cache.insert("a", 1); + /// assert!(!cache.is_empty()); + /// ``` #[inline] pub fn is_empty(&self) -> bool { self.len == 0 } - /// Returns the number of hot pages. + /// Returns the number of hot (protected) pages. + /// + /// Hot pages are shielded from eviction until demoted to cold. + /// + /// # Example + /// + /// ``` + /// use cachekit::policy::clock_pro::ClockProCache; + /// use cachekit::traits::CoreCache; + /// + /// let mut cache = ClockProCache::new(10); + /// cache.insert("a", 1); + /// assert_eq!(cache.hot_count(), 0); // new inserts start cold + /// ``` #[inline] pub fn hot_count(&self) -> usize { self.hot_count } - /// Returns the number of cold pages. + /// Returns the number of cold (eviction candidate) pages. + /// + /// # Example + /// + /// ``` + /// use cachekit::policy::clock_pro::ClockProCache; + /// use cachekit::traits::CoreCache; + /// + /// let mut cache = ClockProCache::new(10); + /// cache.insert("a", 1); + /// assert_eq!(cache.cold_count(), 1); // new inserts start cold + /// ``` #[inline] pub fn cold_count(&self) -> usize { self.len - self.hot_count } - /// Returns the number of ghost entries. + /// Returns the number of ghost entries (recently evicted cold page keys). + /// + /// # Example + /// + /// ``` + /// use cachekit::policy::clock_pro::ClockProCache; + /// use cachekit::traits::{CoreCache, ReadOnlyCache}; + /// + /// let mut cache = ClockProCache::new(2); + /// cache.insert("a", 1); + /// cache.insert("b", 2); + /// cache.insert("c", 3); // evicts one entry into the ghost ring + /// assert!(cache.ghost_count() > 0); + /// ``` #[inline] pub fn ghost_count(&self) -> usize { self.ghost_len @@ -551,7 +619,21 @@ where } } - /// Clears all entries from the cache. + /// Clears all entries, ghost state, and resets the adaptive hot ratio. + /// + /// # Example + /// + /// ``` + /// use cachekit::policy::clock_pro::ClockProCache; + /// use cachekit::traits::{CoreCache, ReadOnlyCache}; + /// + /// let mut cache = ClockProCache::new(10); + /// cache.insert("a", 1); + /// cache.insert("b", 2); + /// cache.clear(); + /// assert!(cache.is_empty()); + /// assert_eq!(cache.ghost_count(), 0); + /// ``` fn clear(&mut self) { #[cfg(feature = "metrics")] self.metrics.record_clear(); @@ -605,11 +687,7 @@ where } } -impl std::fmt::Debug for ClockProCache -where - K: Clone + Eq + Hash + std::fmt::Debug, - V: std::fmt::Debug, -{ +impl std::fmt::Debug for ClockProCache { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ClockProCache") .field("len", &self.len) @@ -621,12 +699,50 @@ where } } +impl Clone for ClockProCache +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 Default for ClockProCache +where + K: Clone + Eq + Hash, +{ + /// Creates a cache with default capacity of 64. + fn default() -> Self { + Self::new(64) + } +} + #[cfg(feature = "metrics")] impl ClockProCache where K: Clone + Eq + Hash, { /// Returns a snapshot of cache metrics. + /// + /// Requires the `metrics` feature. pub fn metrics_snapshot(&self) -> ClockProMetricsSnapshot { ClockProMetricsSnapshot { get_calls: self.metrics.get_calls, @@ -884,4 +1000,25 @@ mod tests { assert_send::>(); assert_sync::>(); } + + #[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 = ClockProCache::default(); + assert_eq!(cache.capacity(), 64); + assert!(cache.is_empty()); + } }