Skip to main content

cachekit/policy/
arc.rs

1//! Adaptive Replacement Cache (ARC) replacement policy.
2//!
3//! Implements the ARC algorithm, which automatically adapts between recency and
4//! frequency preferences by maintaining four lists and adjusting a dynamic target
5//! parameter based on access patterns.
6//!
7//! ## Architecture
8//!
9//! ```text
10//! ┌─────────────────────────────────────────────────────────────────────────────┐
11//! │                           ARCCore<K, V> Layout                              │
12//! │                                                                             │
13//! │   ┌─────────────────────────────────────────────────────────────────────┐   │
14//! │   │  index: HashMap<K, NonNull<Node>>    nodes: Allocated on heap       │   │
15//! │   │                                                                     │   │
16//! │   │  ┌──────────┬───────────┐           ┌────────┬──────────────────┐   │   │
17//! │   │  │   Key    │  NodePtr  │           │ Node   │ key,val,list     │   │   │
18//! │   │  ├──────────┼───────────┤           ├────────┼──────────────────┤   │   │
19//! │   │  │  "page1" │   ptr_0   │──────────►│ Node0  │ k,v,T1           │   │   │
20//! │   │  │  "page2" │   ptr_1   │──────────►│ Node1  │ k,v,T2           │   │   │
21//! │   │  │  "page3" │   ptr_2   │──────────►│ Node2  │ k,v,T1           │   │   │
22//! │   │  └──────────┴───────────┘           └────────┴──────────────────┘   │   │
23//! │   └─────────────────────────────────────────────────────────────────────┘   │
24//! │                                                                             │
25//! │   ┌─────────────────────────────────────────────────────────────────────┐   │
26//! │   │                        List Organization                            │   │
27//! │   │                                                                     │   │
28//! │   │   T1 (Recency - Recent Once)          T2 (Frequency - Repeated)     │   │
29//! │   │   ┌─────────────────────────┐          ┌─────────────────────────┐  │   │
30//! │   │   │ MRU               LRU   │          │ MRU               LRU   │  │   │
31//! │   │   │  ▼                  ▼   │          │  ▼                  ▼   │  │   │
32//! │   │   │ [ptr_2] ◄──► [ptr_0] ◄┤ │          │ [ptr_1] ◄──► [...] ◄┤   │  │   │
33//! │   │   │  new      older   evict │          │ hot          cold evict │  │   │
34//! │   │   └─────────────────────────┘          └─────────────────────────┘  │   │
35//! │   │                                                                     │   │
36//! │   │   B1 (Ghost - evicted from T1)       B2 (Ghost - evicted from T2)   │   │
37//! │   │   ┌─────────────────────────┐          ┌─────────────────────────┐  │   │
38//! │   │   │ Keys only (no values)   │          │ Keys only (no values)   │  │   │
39//! │   │   └─────────────────────────┘          └─────────────────────────┘  │   │
40//! │   │                                                                     │   │
41//! │   │   Adaptation Parameter: p (target size for T1)                      │   │
42//! │   │   • Hit in B1 → increase p (favor recency)                          │   │
43//! │   │   • Hit in B2 → decrease p (favor frequency)                        │   │
44//! │   └─────────────────────────────────────────────────────────────────────┘   │
45//! │                                                                             │
46//! └─────────────────────────────────────────────────────────────────────────────┘
47//!
48//! Insert Flow (new key, not in any list)
49//! ────────────────────────────────────────
50//!
51//!   insert("new_key", value):
52//!     1. Check index - not found
53//!     2. Check ghost lists (B1/B2) for adaptation
54//!     3. Create Node with ListKind::T1
55//!     4. Allocate on heap → get NonNull<Node>
56//!     5. Insert key→ptr into index
57//!     6. Attach ptr to T1 MRU
58//!     7. Evict if over capacity (replace algorithm)
59//!
60//! Access Flow (existing key in T1/T2)
61//! ────────────────────────────────────
62//!
63//!   get("existing_key"):
64//!     1. Lookup ptr in index
65//!     2. Check node's list:
66//!        - If T1: promote to T2 (move to T2 MRU)
67//!        - If T2: move to MRU position within T2
68//!     3. Return &value
69//!
70//! Ghost Hit Flow (key in B1/B2)
71//! ──────────────────────────────
72//!
73//!   get("ghost_key"):
74//!     1. Found in B1: increase p (favor recency)
75//!     2. Found in B2: decrease p (favor frequency)
76//!     3. Perform replacement to make space
77//!     4. Insert into T2 (proven reuse)
78//!     5. Remove from ghost list
79//!
80//! Eviction Flow (Replace Algorithm)
81//! ──────────────────────────────────
82//!
83//!   replace():
84//!     if |T1| >= max(1, p):
85//!       evict from T1 LRU → move key to B1
86//!     else:
87//!       evict from T2 LRU → move key to B2
88//! ```
89//!
90//! ## Key Components
91//!
92//! - [`ARCCore`]: Main ARC cache implementation
93//! - Four lists: T1 (recent once), T2 (frequent), B1 (ghost for T1), B2 (ghost for T2)
94//! - Adaptation parameter `p`: target size for T1 vs T2
95//!
96//! ## Operations
97//!
98//! | Operation   | Time   | Notes                                      |
99//! |-------------|--------|--------------------------------------------|
100//! | `get`       | O(1)   | May promote T1→T2 or adapt via ghost hit   |
101//! | `insert`    | O(1)*  | *Amortized, may trigger evictions          |
102//! | `contains`  | O(1)   | Index lookup only                          |
103//! | `len`       | O(1)   | Returns T1 + T2 entries                    |
104//! | `clear`     | O(n)   | Clears all structures                      |
105//!
106//! ## Algorithm Properties
107//!
108//! - **Adaptive**: Automatically balances recency vs frequency based on workload
109//! - **Scan Resistant**: Ghost lists prevent one-time scans from polluting cache
110//! - **Self-Tuning**: No manual parameter tuning required
111//! - **Competitive**: O(1) operations, proven optimal in certain workload classes
112//!
113//! ## Use Cases
114//!
115//! - Database buffer pools with mixed access patterns
116//! - File system caches
117//! - Web caches with varying temporal/frequency characteristics
118//! - Workloads where optimal recency/frequency balance is unknown
119//!
120//! ## Example Usage
121//!
122//! ```
123//! use cachekit::policy::arc::ARCCore;
124//! use cachekit::traits::{CoreCache, ReadOnlyCache};
125//!
126//! // Create ARC cache with 100 entry capacity
127//! let mut cache = ARCCore::new(100);
128//!
129//! // Insert items (go to T1 - recent list)
130//! cache.insert("page1", "content1");
131//! cache.insert("page2", "content2");
132//!
133//! // First access promotes to T2 (frequent list)
134//! assert_eq!(cache.get(&"page1"), Some(&"content1"));
135//!
136//! // Second access keeps in T2 (MRU position)
137//! assert_eq!(cache.get(&"page1"), Some(&"content1"));
138//!
139//! assert_eq!(cache.len(), 2);
140//! ```
141//!
142//! ## Thread Safety
143//!
144//! - [`ARCCore`]: Not thread-safe, designed for single-threaded use
145//! - For concurrent access, wrap in external synchronization
146//!
147//! ## Implementation Notes
148//!
149//! - T1 uses LRU ordering (recent once entries)
150//! - T2 uses LRU ordering (frequent entries)
151//! - B1/B2 are ghost lists (keys only, no values)
152//! - Default initial `p` is `capacity / 2`
153//! - Ghost list sizes are each up to `capacity`
154//! - Promotion from T1 to T2 happens on re-access
155//!
156//! ## References
157//!
158//! - Megiddo & Modha, "ARC: A Self-Tuning, Low Overhead Replacement Cache",
159//!   FAST 2003
160//! - Wikipedia: Cache replacement policies (ARC section)
161//!
162//! ## Performance Trade-offs
163//!
164//! - **When to Use**: Unknown or shifting workload patterns; need adaptive behavior
165//! - **Memory Overhead**: 4 lists + ghost entries (up to 2× capacity in keys)
166//! - **vs LRU**: Better on mixed workloads, slightly higher metadata overhead
167//! - **vs 2Q/SLRU**: More adaptive, no manual tuning needed
168
169use crate::ds::GhostList;
170#[cfg(feature = "metrics")]
171use crate::metrics::metrics_impl::ArcMetrics;
172#[cfg(feature = "metrics")]
173use crate::metrics::snapshot::ArcMetricsSnapshot;
174#[cfg(feature = "metrics")]
175use crate::metrics::traits::{ArcMetricsRecorder, CoreMetricsRecorder, MetricsSnapshotProvider};
176use crate::prelude::ReadOnlyCache;
177use crate::traits::{CoreCache, MutableCache};
178use rustc_hash::FxHashMap;
179use std::hash::Hash;
180use std::ptr::NonNull;
181
182/// Indicates which list an entry resides in.
183#[derive(Copy, Clone, Debug, Eq, PartialEq)]
184enum ListKind {
185    /// Entry is in T1 (recent once).
186    T1,
187    /// Entry is in T2 (frequent).
188    T2,
189}
190
191/// Node in the ARC linked list.
192///
193/// Cache-line optimized layout with pointers first.
194#[repr(C)]
195struct Node<K, V> {
196    prev: Option<NonNull<Node<K, V>>>,
197    next: Option<NonNull<Node<K, V>>>,
198    list: ListKind,
199    key: K,
200    value: V,
201}
202
203/// Core Adaptive Replacement Cache (ARC) implementation.
204///
205/// Implements the ARC replacement algorithm with automatic adaptation between
206/// recency and frequency preferences:
207/// - **T1**: Recently accessed once (recency list)
208/// - **T2**: Accessed multiple times (frequency list)
209/// - **B1**: Ghost list for items evicted from T1
210/// - **B2**: Ghost list for items evicted from T2
211///
212/// The cache maintains an adaptation parameter `p` that controls the target
213/// size of T1 vs T2. Ghost hits in B1/B2 adjust `p` to favor recency or frequency.
214///
215/// # Type Parameters
216///
217/// - `K`: Key type, must be `Clone + Eq + Hash`
218/// - `V`: Value type
219///
220/// # Example
221///
222/// ```
223/// use cachekit::policy::arc::ARCCore;
224/// use cachekit::traits::{CoreCache, ReadOnlyCache};
225///
226/// // 100 capacity ARC cache
227/// let mut cache = ARCCore::new(100);
228///
229/// // Insert goes to T1 (recent list)
230/// cache.insert("key1", "value1");
231/// assert!(cache.contains(&"key1"));
232///
233/// // First get promotes to T2 (frequent list)
234/// cache.get(&"key1");
235///
236/// // Update existing key
237/// cache.insert("key1", "new_value");
238/// assert_eq!(cache.get(&"key1"), Some(&"new_value"));
239/// ```
240///
241/// # Eviction Behavior
242///
243/// The replacement algorithm selects victims based on the adaptation parameter `p`:
244/// - If `|T1| >= max(1, p)`: evict from T1 LRU, move key to B1
245/// - Otherwise: evict from T2 LRU, move key to B2
246///
247/// # Implementation
248///
249/// Uses raw pointer linked lists for O(1) operations with minimal overhead.
250/// Ghost lists track recently evicted keys to enable adaptation.
251pub struct ARCCore<K, V>
252where
253    K: Clone + Eq + Hash,
254{
255    /// Direct key -> node pointer mapping
256    map: FxHashMap<K, NonNull<Node<K, V>>>,
257
258    /// T1 list (recent once): head=MRU, tail=LRU
259    t1_head: Option<NonNull<Node<K, V>>>,
260    t1_tail: Option<NonNull<Node<K, V>>>,
261    t1_len: usize,
262
263    /// T2 list (frequent): head=MRU, tail=LRU
264    t2_head: Option<NonNull<Node<K, V>>>,
265    t2_tail: Option<NonNull<Node<K, V>>>,
266    t2_len: usize,
267
268    /// B1 ghost list (evicted from T1)
269    b1: GhostList<K>,
270
271    /// B2 ghost list (evicted from T2)
272    b2: GhostList<K>,
273
274    /// Adaptation parameter: target size for T1
275    p: usize,
276
277    /// Maximum total cache capacity
278    capacity: usize,
279
280    #[cfg(feature = "metrics")]
281    metrics: ArcMetrics,
282}
283
284// SAFETY: ARCCore can be sent between threads if K and V are Send.
285unsafe impl<K, V> Send for ARCCore<K, V>
286where
287    K: Clone + Eq + Hash + Send,
288    V: Send,
289{
290}
291
292// SAFETY: ARCCore can be shared between threads if K and V are Sync.
293unsafe impl<K, V> Sync for ARCCore<K, V>
294where
295    K: Clone + Eq + Hash + Sync,
296    V: Sync,
297{
298}
299
300impl<K, V> ARCCore<K, V>
301where
302    K: Clone + Eq + Hash,
303{
304    /// Creates a new ARC cache with the specified capacity.
305    ///
306    /// # Arguments
307    ///
308    /// - `capacity`: Maximum number of entries in T1 + T2
309    ///
310    /// Ghost lists (B1/B2) can each hold up to `capacity` keys.
311    /// Initial adaptation parameter `p` is set to `capacity / 2`.
312    ///
313    /// # Example
314    ///
315    /// ```
316    /// use cachekit::policy::arc::ARCCore;
317    /// use cachekit::traits::ReadOnlyCache;
318    ///
319    /// // 100 capacity ARC cache
320    /// let cache: ARCCore<String, i32> = ARCCore::new(100);
321    /// assert_eq!(cache.capacity(), 100);
322    /// assert!(cache.is_empty());
323    /// ```
324    #[inline]
325    pub fn new(capacity: usize) -> Self {
326        Self {
327            map: FxHashMap::with_capacity_and_hasher(capacity, Default::default()),
328            t1_head: None,
329            t1_tail: None,
330            t1_len: 0,
331            t2_head: None,
332            t2_tail: None,
333            t2_len: 0,
334            b1: GhostList::new(capacity),
335            b2: GhostList::new(capacity),
336            p: capacity / 2,
337            capacity,
338            #[cfg(feature = "metrics")]
339            metrics: ArcMetrics::default(),
340        }
341    }
342
343    /// Detach a node from its current list (T1 or T2).
344    #[inline(always)]
345    fn detach(&mut self, node_ptr: NonNull<Node<K, V>>) {
346        unsafe {
347            let node = node_ptr.as_ref();
348            let prev = node.prev;
349            let next = node.next;
350            let list = node.list;
351
352            let (head, tail, len) = match list {
353                ListKind::T1 => (&mut self.t1_head, &mut self.t1_tail, &mut self.t1_len),
354                ListKind::T2 => (&mut self.t2_head, &mut self.t2_tail, &mut self.t2_len),
355            };
356
357            match prev {
358                Some(mut p) => p.as_mut().next = next,
359                None => *head = next,
360            }
361
362            match next {
363                Some(mut n) => n.as_mut().prev = prev,
364                None => *tail = prev,
365            }
366
367            *len -= 1;
368        }
369    }
370
371    /// Attach a node at the head of T1 list (MRU position).
372    #[inline(always)]
373    fn attach_t1_head(&mut self, mut node_ptr: NonNull<Node<K, V>>) {
374        unsafe {
375            let node = node_ptr.as_mut();
376            node.prev = None;
377            node.next = self.t1_head;
378            node.list = ListKind::T1;
379
380            match self.t1_head {
381                Some(mut h) => h.as_mut().prev = Some(node_ptr),
382                None => self.t1_tail = Some(node_ptr),
383            }
384
385            self.t1_head = Some(node_ptr);
386            self.t1_len += 1;
387        }
388    }
389
390    /// Attach a node at the head of T2 list (MRU position).
391    #[inline(always)]
392    fn attach_t2_head(&mut self, mut node_ptr: NonNull<Node<K, V>>) {
393        unsafe {
394            let node = node_ptr.as_mut();
395            node.prev = None;
396            node.next = self.t2_head;
397            node.list = ListKind::T2;
398
399            match self.t2_head {
400                Some(mut h) => h.as_mut().prev = Some(node_ptr),
401                None => self.t2_tail = Some(node_ptr),
402            }
403
404            self.t2_head = Some(node_ptr);
405            self.t2_len += 1;
406        }
407    }
408
409    /// Replace: select a victim from T1 or T2 based on adaptation parameter p.
410    ///
411    /// This is the core ARC replacement algorithm.
412    fn replace(&mut self, in_b2: bool) {
413        #[cfg(feature = "metrics")]
414        self.metrics.record_evict_call();
415
416        // Decide whether to evict from T1 or T2
417        let evict_from_t1 =
418            if self.t1_len > 0 && (self.t1_len > self.p || (self.t1_len == self.p && in_b2)) {
419                true
420            } else if self.t2_len > 0 {
421                false
422            } else {
423                self.t1_len > 0
424            };
425
426        if evict_from_t1 {
427            if let Some(victim_ptr) = self.t1_tail {
428                unsafe {
429                    let victim = victim_ptr.as_ref();
430                    let key = victim.key.clone();
431
432                    self.detach(victim_ptr);
433                    self.map.remove(&key);
434                    self.b1.record(key.clone());
435                    let _ = Box::from_raw(victim_ptr.as_ptr());
436
437                    #[cfg(feature = "metrics")]
438                    {
439                        self.metrics.record_evicted_entry();
440                        self.metrics.record_t1_eviction();
441                    }
442                }
443            }
444        } else if let Some(victim_ptr) = self.t2_tail {
445            unsafe {
446                let victim = victim_ptr.as_ref();
447                let key = victim.key.clone();
448
449                self.detach(victim_ptr);
450                self.map.remove(&key);
451                self.b2.record(key.clone());
452                let _ = Box::from_raw(victim_ptr.as_ptr());
453
454                #[cfg(feature = "metrics")]
455                {
456                    self.metrics.record_evicted_entry();
457                    self.metrics.record_t2_eviction();
458                }
459            }
460        }
461    }
462
463    /// Adapt parameter p based on ghost hit location.
464    fn adapt(&mut self, in_b1: bool, in_b2: bool) {
465        if in_b1 {
466            let delta = if self.b2.len() >= self.b1.len() {
467                1
468            } else if !self.b1.is_empty() {
469                ((self.b2.len() as f64 / self.b1.len() as f64).ceil() as usize).max(1)
470            } else {
471                1
472            };
473            self.p = (self.p + delta).min(self.capacity);
474
475            #[cfg(feature = "metrics")]
476            self.metrics.record_p_increase();
477        } else if in_b2 {
478            let delta = if self.b1.len() >= self.b2.len() {
479                1
480            } else if !self.b2.is_empty() {
481                ((self.b1.len() as f64 / self.b2.len() as f64).ceil() as usize).max(1)
482            } else {
483                1
484            };
485            self.p = self.p.saturating_sub(delta);
486
487            #[cfg(feature = "metrics")]
488            self.metrics.record_p_decrease();
489        }
490    }
491
492    /// Returns the current value of the adaptation parameter p.
493    ///
494    /// This represents the target size for T1. Higher values favor recency,
495    /// lower values favor frequency.
496    ///
497    /// # Example
498    ///
499    /// ```
500    /// use cachekit::policy::arc::ARCCore;
501    ///
502    /// let cache: ARCCore<String, i32> = ARCCore::new(100);
503    /// // Initial p is capacity / 2
504    /// assert_eq!(cache.p_value(), 50);
505    /// ```
506    pub fn p_value(&self) -> usize {
507        self.p
508    }
509
510    /// Returns the number of entries in T1 (recent once list).
511    ///
512    /// # Example
513    ///
514    /// ```
515    /// use cachekit::policy::arc::ARCCore;
516    /// use cachekit::traits::CoreCache;
517    ///
518    /// let mut cache = ARCCore::new(100);
519    /// cache.insert("key", "value");
520    /// assert_eq!(cache.t1_len(), 1);  // New entries go to T1
521    /// ```
522    pub fn t1_len(&self) -> usize {
523        self.t1_len
524    }
525
526    /// Returns the number of entries in T2 (frequent list).
527    ///
528    /// # Example
529    ///
530    /// ```
531    /// use cachekit::policy::arc::ARCCore;
532    /// use cachekit::traits::CoreCache;
533    ///
534    /// let mut cache = ARCCore::new(100);
535    /// cache.insert("key", "value");
536    /// cache.get(&"key");  // Promotes to T2
537    /// assert_eq!(cache.t2_len(), 1);
538    /// ```
539    pub fn t2_len(&self) -> usize {
540        self.t2_len
541    }
542
543    /// Returns the number of keys in B1 ghost list.
544    pub fn b1_len(&self) -> usize {
545        self.b1.len()
546    }
547
548    /// Returns the number of keys in B2 ghost list.
549    pub fn b2_len(&self) -> usize {
550        self.b2.len()
551    }
552
553    #[cfg(any(test, debug_assertions))]
554    /// Validates internal invariants of the ARC cache.
555    ///
556    /// Panics if any invariant is violated.
557    pub fn debug_validate_invariants(&self)
558    where
559        K: std::fmt::Debug,
560    {
561        // 1. Total length matches sum of T1 and T2
562        assert_eq!(
563            self.len(),
564            self.t1_len + self.t2_len,
565            "len() should equal t1_len + t2_len"
566        );
567
568        // 2. Map size equals total entries
569        assert_eq!(
570            self.map.len(),
571            self.t1_len + self.t2_len,
572            "map.len() should equal total entries"
573        );
574
575        // 3. Total entries don't exceed capacity
576        assert!(
577            self.t1_len + self.t2_len <= self.capacity,
578            "total entries ({}) exceed capacity ({})",
579            self.t1_len + self.t2_len,
580            self.capacity
581        );
582
583        // 4. p is within valid range
584        assert!(
585            self.p <= self.capacity,
586            "p ({}) exceeds capacity ({})",
587            self.p,
588            self.capacity
589        );
590
591        // 5. Ghost lists don't exceed capacity
592        assert!(
593            self.b1.len() <= self.capacity,
594            "B1 length ({}) exceeds capacity ({})",
595            self.b1.len(),
596            self.capacity
597        );
598        assert!(
599            self.b2.len() <= self.capacity,
600            "B2 length ({}) exceeds capacity ({})",
601            self.b2.len(),
602            self.capacity
603        );
604
605        // 6. Count actual T1 entries
606        let mut t1_count = 0;
607        let mut current = self.t1_head;
608        let mut visited_t1 = std::collections::HashSet::new();
609        while let Some(node_ptr) = current {
610            unsafe {
611                let node = node_ptr.as_ref();
612
613                // Check for cycles
614                assert!(visited_t1.insert(node_ptr), "Cycle detected in T1 list");
615
616                // Verify list kind
617                assert_eq!(node.list, ListKind::T1, "Node in T1 has wrong list kind");
618
619                // Verify node is in map
620                assert!(self.map.contains_key(&node.key), "T1 node key not in map");
621
622                t1_count += 1;
623                current = node.next;
624            }
625        }
626        assert_eq!(
627            t1_count, self.t1_len,
628            "T1 actual count doesn't match t1_len"
629        );
630
631        // 7. Count actual T2 entries
632        let mut t2_count = 0;
633        let mut current = self.t2_head;
634        let mut visited_t2 = std::collections::HashSet::new();
635        while let Some(node_ptr) = current {
636            unsafe {
637                let node = node_ptr.as_ref();
638
639                // Check for cycles
640                assert!(visited_t2.insert(node_ptr), "Cycle detected in T2 list");
641
642                // Verify list kind
643                assert_eq!(node.list, ListKind::T2, "Node in T2 has wrong list kind");
644
645                // Verify node is in map
646                assert!(self.map.contains_key(&node.key), "T2 node key not in map");
647
648                t2_count += 1;
649                current = node.next;
650            }
651        }
652        assert_eq!(
653            t2_count, self.t2_len,
654            "T2 actual count doesn't match t2_len"
655        );
656
657        // 8. Verify no overlap between T1 and T2
658        for t1_ptr in &visited_t1 {
659            assert!(
660                !visited_t2.contains(t1_ptr),
661                "Node appears in both T1 and T2"
662            );
663        }
664
665        // 9. Verify all map entries are accounted for in T1 or T2
666        assert_eq!(
667            visited_t1.len() + visited_t2.len(),
668            self.map.len(),
669            "Map contains entries not in T1 or T2"
670        );
671
672        // 10. Ghost lists don't contain keys that are in the cache
673        for key in self.map.keys() {
674            assert!(
675                !self.b1.contains(key),
676                "Key {:?} is in both cache and B1",
677                key
678            );
679            assert!(
680                !self.b2.contains(key),
681                "Key {:?} is in both cache and B2",
682                key
683            );
684        }
685    }
686}
687
688impl<K, V> std::fmt::Debug for ARCCore<K, V>
689where
690    K: Clone + Eq + Hash + std::fmt::Debug,
691    V: std::fmt::Debug,
692{
693    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
694        f.debug_struct("ARCCore")
695            .field("capacity", &self.capacity)
696            .field("t1_len", &self.t1_len)
697            .field("t2_len", &self.t2_len)
698            .field("b1_len", &self.b1.len())
699            .field("b2_len", &self.b2.len())
700            .field("p", &self.p)
701            .field("total_len", &(self.t1_len + self.t2_len))
702            .finish()
703    }
704}
705
706impl<K, V> ReadOnlyCache<K, V> for ARCCore<K, V>
707where
708    K: Clone + Eq + Hash,
709{
710    fn contains(&self, key: &K) -> bool {
711        self.map.contains_key(key)
712    }
713
714    fn len(&self) -> usize {
715        self.t1_len + self.t2_len
716    }
717
718    fn capacity(&self) -> usize {
719        self.capacity
720    }
721}
722
723impl<K, V> CoreCache<K, V> for ARCCore<K, V>
724where
725    K: Clone + Eq + Hash,
726{
727    fn get(&mut self, key: &K) -> Option<&V> {
728        let node_ptr = match self.map.get(key) {
729            Some(&ptr) => ptr,
730            None => {
731                #[cfg(feature = "metrics")]
732                self.metrics.record_get_miss();
733                return None;
734            },
735        };
736
737        #[cfg(feature = "metrics")]
738        self.metrics.record_get_hit();
739
740        unsafe {
741            let node = node_ptr.as_ref();
742
743            match node.list {
744                ListKind::T1 => {
745                    #[cfg(feature = "metrics")]
746                    self.metrics.record_t1_to_t2_promotion();
747
748                    self.detach(node_ptr);
749                    self.attach_t2_head(node_ptr);
750                },
751                ListKind::T2 => {
752                    self.detach(node_ptr);
753                    self.attach_t2_head(node_ptr);
754                },
755            }
756
757            Some(&node_ptr.as_ref().value)
758        }
759    }
760
761    fn insert(&mut self, key: K, value: V) -> Option<V> {
762        #[cfg(feature = "metrics")]
763        self.metrics.record_insert_call();
764
765        if self.capacity == 0 {
766            return None;
767        }
768
769        // Case 1: Key already in cache (T1 or T2)
770        if let Some(&node_ptr) = self.map.get(&key) {
771            #[cfg(feature = "metrics")]
772            self.metrics.record_insert_update();
773
774            unsafe {
775                let mut node_ptr = node_ptr;
776                let node = node_ptr.as_mut();
777                let old_value = std::mem::replace(&mut node.value, value);
778
779                match node.list {
780                    ListKind::T1 => {
781                        self.detach(node_ptr);
782                        self.attach_t2_head(node_ptr);
783                    },
784                    ListKind::T2 => {
785                        self.detach(node_ptr);
786                        self.attach_t2_head(node_ptr);
787                    },
788                }
789
790                return Some(old_value);
791            }
792        }
793
794        let in_b1 = self.b1.contains(&key);
795        let in_b2 = self.b2.contains(&key);
796
797        // Case 2: Ghost hit in B1
798        if in_b1 {
799            #[cfg(feature = "metrics")]
800            {
801                self.metrics.record_b1_ghost_hit();
802                self.metrics.record_insert_new();
803            }
804
805            self.adapt(true, false);
806            self.b1.remove(&key);
807
808            if self.t1_len + self.t2_len >= self.capacity {
809                self.replace(false);
810            }
811
812            let node = Box::new(Node {
813                prev: None,
814                next: None,
815                list: ListKind::T2,
816                key: key.clone(),
817                value,
818            });
819            let node_ptr = NonNull::new(Box::into_raw(node)).unwrap();
820            self.map.insert(key, node_ptr);
821            self.attach_t2_head(node_ptr);
822
823            return None;
824        }
825
826        // Case 3: Ghost hit in B2
827        if in_b2 {
828            #[cfg(feature = "metrics")]
829            {
830                self.metrics.record_b2_ghost_hit();
831                self.metrics.record_insert_new();
832            }
833
834            self.adapt(false, true);
835            self.b2.remove(&key);
836
837            if self.t1_len + self.t2_len >= self.capacity {
838                self.replace(true);
839            }
840
841            let node = Box::new(Node {
842                prev: None,
843                next: None,
844                list: ListKind::T2,
845                key: key.clone(),
846                value,
847            });
848            let node_ptr = NonNull::new(Box::into_raw(node)).unwrap();
849            self.map.insert(key, node_ptr);
850            self.attach_t2_head(node_ptr);
851
852            return None;
853        }
854
855        // Case 4: Complete miss
856        #[cfg(feature = "metrics")]
857        self.metrics.record_insert_new();
858
859        let l1_len = self.t1_len + self.b1.len();
860        if l1_len >= self.capacity {
861            if !self.b1.is_empty() {
862                self.b1.evict_lru();
863            }
864            if self.t1_len + self.t2_len >= self.capacity {
865                self.replace(false);
866            }
867        } else {
868            let total = self.t1_len + self.t2_len + self.b1.len() + self.b2.len();
869            if total >= 2 * self.capacity {
870                self.b2.evict_lru();
871            }
872            if self.t1_len + self.t2_len >= self.capacity {
873                self.replace(false);
874            }
875        }
876
877        let node = Box::new(Node {
878            prev: None,
879            next: None,
880            list: ListKind::T1,
881            key: key.clone(),
882            value,
883        });
884        let node_ptr = NonNull::new(Box::into_raw(node)).unwrap();
885        self.map.insert(key, node_ptr);
886        self.attach_t1_head(node_ptr);
887
888        None
889    }
890
891    fn clear(&mut self) {
892        #[cfg(feature = "metrics")]
893        self.metrics.record_clear();
894
895        let mut current = self.t1_head;
896        while let Some(node_ptr) = current {
897            unsafe {
898                let node = node_ptr.as_ref();
899                current = node.next;
900                let _ = Box::from_raw(node_ptr.as_ptr());
901            }
902        }
903
904        let mut current = self.t2_head;
905        while let Some(node_ptr) = current {
906            unsafe {
907                let node = node_ptr.as_ref();
908                current = node.next;
909                let _ = Box::from_raw(node_ptr.as_ptr());
910            }
911        }
912
913        self.map.clear();
914        self.t1_head = None;
915        self.t1_tail = None;
916        self.t1_len = 0;
917        self.t2_head = None;
918        self.t2_tail = None;
919        self.t2_len = 0;
920        self.b1.clear();
921        self.b2.clear();
922        self.p = self.capacity / 2;
923    }
924}
925
926impl<K, V> MutableCache<K, V> for ARCCore<K, V>
927where
928    K: Clone + Eq + Hash,
929{
930    fn remove(&mut self, key: &K) -> Option<V> {
931        let node_ptr = self.map.remove(key)?;
932
933        self.detach(node_ptr);
934
935        unsafe {
936            let node = Box::from_raw(node_ptr.as_ptr());
937            Some(node.value)
938        }
939    }
940}
941
942impl<K, V> Drop for ARCCore<K, V>
943where
944    K: Clone + Eq + Hash,
945{
946    fn drop(&mut self) {
947        self.clear();
948    }
949}
950
951#[cfg(feature = "metrics")]
952impl<K, V> ARCCore<K, V>
953where
954    K: Clone + Eq + Hash,
955{
956    /// Returns a snapshot of cache metrics.
957    pub fn metrics_snapshot(&self) -> ArcMetricsSnapshot {
958        ArcMetricsSnapshot {
959            get_calls: self.metrics.get_calls,
960            get_hits: self.metrics.get_hits,
961            get_misses: self.metrics.get_misses,
962            insert_calls: self.metrics.insert_calls,
963            insert_updates: self.metrics.insert_updates,
964            insert_new: self.metrics.insert_new,
965            evict_calls: self.metrics.evict_calls,
966            evicted_entries: self.metrics.evicted_entries,
967            t1_to_t2_promotions: self.metrics.t1_to_t2_promotions,
968            b1_ghost_hits: self.metrics.b1_ghost_hits,
969            b2_ghost_hits: self.metrics.b2_ghost_hits,
970            p_increases: self.metrics.p_increases,
971            p_decreases: self.metrics.p_decreases,
972            t1_evictions: self.metrics.t1_evictions,
973            t2_evictions: self.metrics.t2_evictions,
974            cache_len: self.len(),
975            capacity: self.capacity,
976        }
977    }
978}
979
980#[cfg(feature = "metrics")]
981impl<K, V> MetricsSnapshotProvider<ArcMetricsSnapshot> for ARCCore<K, V>
982where
983    K: Clone + Eq + Hash,
984{
985    fn snapshot(&self) -> ArcMetricsSnapshot {
986        self.metrics_snapshot()
987    }
988}
989
990#[cfg(test)]
991mod tests {
992    use super::*;
993
994    #[test]
995    fn arc_new_cache() {
996        let cache: ARCCore<String, i32> = ARCCore::new(100);
997        assert_eq!(cache.capacity(), 100);
998        assert_eq!(cache.len(), 0);
999        assert!(cache.is_empty());
1000        assert_eq!(cache.t1_len(), 0);
1001        assert_eq!(cache.t2_len(), 0);
1002        assert_eq!(cache.b1_len(), 0);
1003        assert_eq!(cache.b2_len(), 0);
1004        assert_eq!(cache.p_value(), 50); // Initial p = capacity / 2
1005    }
1006
1007    #[test]
1008    fn arc_insert_and_get() {
1009        let mut cache = ARCCore::new(10);
1010
1011        // First insert goes to T1
1012        cache.insert("key1", "value1");
1013        assert_eq!(cache.len(), 1);
1014        assert_eq!(cache.t1_len(), 1);
1015        assert_eq!(cache.t2_len(), 0);
1016
1017        // Get promotes to T2
1018        assert_eq!(cache.get(&"key1"), Some(&"value1"));
1019        assert_eq!(cache.t1_len(), 0);
1020        assert_eq!(cache.t2_len(), 1);
1021
1022        // Second get keeps in T2
1023        assert_eq!(cache.get(&"key1"), Some(&"value1"));
1024        assert_eq!(cache.t1_len(), 0);
1025        assert_eq!(cache.t2_len(), 1);
1026    }
1027
1028    #[test]
1029    fn arc_update_existing() {
1030        let mut cache = ARCCore::new(10);
1031
1032        cache.insert("key1", "value1");
1033        assert_eq!(cache.t1_len(), 1);
1034
1035        // Update in T1 promotes to T2
1036        let old = cache.insert("key1", "new_value");
1037        assert_eq!(old, Some("value1"));
1038        assert_eq!(cache.len(), 1);
1039        assert_eq!(cache.t1_len(), 0);
1040        assert_eq!(cache.t2_len(), 1);
1041
1042        assert_eq!(cache.get(&"key1"), Some(&"new_value"));
1043    }
1044
1045    #[test]
1046    fn arc_eviction_fills_ghost_lists() {
1047        let mut cache = ARCCore::new(2);
1048
1049        // Fill cache
1050        cache.insert("a", 1);
1051        cache.insert("b", 2);
1052        assert_eq!(cache.len(), 2);
1053        assert_eq!(cache.t1_len(), 2);
1054
1055        // Insert third item triggers eviction
1056        cache.insert("c", 3);
1057        assert_eq!(cache.len(), 2);
1058        assert_eq!(cache.t1_len(), 2); // c and b (a evicted)
1059        assert_eq!(cache.b1_len(), 1); // a moved to B1
1060        assert!(!cache.contains(&"a"));
1061    }
1062
1063    #[test]
1064    fn arc_ghost_hit_promotes_to_t2() {
1065        let mut cache = ARCCore::new(2);
1066
1067        // Fill and evict
1068        cache.insert("a", 1);
1069        cache.insert("b", 2);
1070        cache.insert("c", 3); // Evicts "a" to B1
1071
1072        cache.debug_validate_invariants();
1073
1074        assert!(!cache.contains(&"a"));
1075        assert_eq!(cache.b1_len(), 1);
1076        assert!(cache.b1.contains(&"a"));
1077
1078        // Ghost hit on "a"
1079        // This should: remove "a" from B1, make space (evicting something else), insert "a" to T2
1080        cache.insert("a", 10);
1081        cache.debug_validate_invariants();
1082
1083        println!(
1084            "After ghost hit: len={}, t1={}, t2={}, b1={}, b2={}",
1085            cache.len(),
1086            cache.t1_len(),
1087            cache.t2_len(),
1088            cache.b1_len(),
1089            cache.b2_len()
1090        );
1091
1092        assert_eq!(
1093            cache.len(),
1094            2,
1095            "Cache length should be 2, got {}",
1096            cache.len()
1097        );
1098        assert_eq!(cache.t2_len(), 1); // "a" goes to T2 (ghost hit)
1099        assert!(!cache.b1.contains(&"a")); // "a" removed from B1
1100        // Note: B1 may still have entries from the eviction that happened to make space for "a"
1101    }
1102
1103    #[test]
1104    fn arc_adaptation_increases_p() {
1105        let mut cache = ARCCore::new(4);
1106        let initial_p = cache.p_value();
1107
1108        // Create scenario with B1 ghost hit
1109        cache.insert("a", 1);
1110        cache.insert("b", 2);
1111        cache.insert("c", 3);
1112        cache.insert("d", 4);
1113        cache.insert("e", 5); // Evicts "a" to B1
1114
1115        println!(
1116            "Before ghost hit: p={}, t1={}, t2={}, b1={}, b2={}",
1117            cache.p_value(),
1118            cache.t1_len(),
1119            cache.t2_len(),
1120            cache.b1_len(),
1121            cache.b2_len()
1122        );
1123        println!("B1 contains a={}", cache.b1.contains(&"a"));
1124
1125        // Ghost hit in B1 should increase p
1126        cache.insert("a", 10);
1127
1128        println!(
1129            "After ghost hit: p={}, t1={}, t2={}, b1={}, b2={}",
1130            cache.p_value(),
1131            cache.t1_len(),
1132            cache.t2_len(),
1133            cache.b1_len(),
1134            cache.b2_len()
1135        );
1136
1137        // Note: If b1.len() == b2.len() initially, delta would be 1
1138        // p should increase by at least 1
1139        assert!(
1140            cache.p_value() > initial_p,
1141            "Expected p to increase from {} but got {}",
1142            initial_p,
1143            cache.p_value()
1144        );
1145    }
1146
1147    #[test]
1148    fn arc_remove() {
1149        let mut cache = ARCCore::new(10);
1150
1151        cache.insert("key1", "value1");
1152        cache.insert("key2", "value2");
1153        assert_eq!(cache.len(), 2);
1154
1155        let removed = cache.remove(&"key1");
1156        assert_eq!(removed, Some("value1"));
1157        assert_eq!(cache.len(), 1);
1158        assert!(!cache.contains(&"key1"));
1159        assert!(cache.contains(&"key2"));
1160    }
1161
1162    #[test]
1163    fn arc_clear() {
1164        let mut cache = ARCCore::new(10);
1165
1166        cache.insert("key1", "value1");
1167        cache.insert("key2", "value2");
1168        cache.get(&"key1"); // Promote to T2
1169
1170        cache.clear();
1171
1172        assert_eq!(cache.len(), 0);
1173        assert_eq!(cache.t1_len(), 0);
1174        assert_eq!(cache.t2_len(), 0);
1175        assert_eq!(cache.b1_len(), 0);
1176        assert_eq!(cache.b2_len(), 0);
1177        assert!(cache.is_empty());
1178    }
1179
1180    #[test]
1181    fn arc_contains() {
1182        let mut cache = ARCCore::new(10);
1183
1184        assert!(!cache.contains(&"key1"));
1185
1186        cache.insert("key1", "value1");
1187        assert!(cache.contains(&"key1"));
1188
1189        cache.remove(&"key1");
1190        assert!(!cache.contains(&"key1"));
1191    }
1192
1193    #[test]
1194    fn arc_promotion_t1_to_t2() {
1195        let mut cache = ARCCore::new(10);
1196
1197        // Insert into T1
1198        cache.insert("key", "value");
1199        assert_eq!(cache.t1_len(), 1);
1200        assert_eq!(cache.t2_len(), 0);
1201
1202        // First access promotes to T2
1203        cache.get(&"key");
1204        assert_eq!(cache.t1_len(), 0);
1205        assert_eq!(cache.t2_len(), 1);
1206
1207        // Second access stays in T2
1208        cache.get(&"key");
1209        assert_eq!(cache.t1_len(), 0);
1210        assert_eq!(cache.t2_len(), 1);
1211    }
1212
1213    #[test]
1214    fn arc_multiple_entries() {
1215        let mut cache = ARCCore::new(5);
1216
1217        for i in 0..5 {
1218            cache.insert(i, i * 10);
1219        }
1220
1221        assert_eq!(cache.len(), 5);
1222
1223        for i in 0..5 {
1224            assert_eq!(cache.get(&i), Some(&(i * 10)));
1225        }
1226
1227        // All should be promoted to T2 after access
1228        assert_eq!(cache.t2_len(), 5);
1229        assert_eq!(cache.t1_len(), 0);
1230    }
1231
1232    #[test]
1233    fn arc_eviction_and_ghost_tracking() {
1234        let mut cache = ARCCore::new(3);
1235
1236        // Fill cache
1237        cache.insert(1, 100);
1238        cache.insert(2, 200);
1239        cache.insert(3, 300);
1240
1241        // Access 1 and 2 to promote them to T2
1242        cache.get(&1);
1243        cache.get(&2);
1244
1245        assert_eq!(cache.t1_len(), 1); // 3 remains in T1
1246        assert_eq!(cache.t2_len(), 2); // 1, 2 promoted to T2
1247
1248        // Insert 4. With p=1, t1_len=1, t2_len=2:
1249        // Condition: t1_len > p → 1 > 1 is false
1250        // So we evict from T2 (not T1)
1251        // T2 has [2 (MRU), 1 (LRU)], so 1 gets evicted to B2
1252        cache.insert(4, 400);
1253
1254        assert_eq!(cache.len(), 3);
1255        assert!(!cache.contains(&1)); // 1 was evicted (LRU of T2)
1256        assert!(cache.contains(&2)); // 2 remains (MRU of T2)
1257        assert!(cache.contains(&3)); // 3 remains (in T1)
1258        assert!(cache.contains(&4)); // 4 just inserted
1259        assert_eq!(cache.b2_len(), 1); // 1 moved to B2 (evicted from T2)
1260    }
1261
1262    #[test]
1263    fn arc_zero_capacity() {
1264        let mut cache = ARCCore::new(0);
1265        assert_eq!(cache.capacity(), 0);
1266        assert_eq!(cache.len(), 0);
1267
1268        cache.insert("key", "value");
1269        assert_eq!(cache.len(), 0);
1270        assert!(!cache.contains(&"key"));
1271    }
1272
1273    // ==============================================
1274    // Regression Tests
1275    // ==============================================
1276
1277    #[test]
1278    fn ghost_directory_size_within_two_times_capacity() {
1279        let c = 5usize;
1280        let mut cache: ARCCore<u64, u64> = ARCCore::new(c);
1281
1282        for i in 0..c as u64 {
1283            cache.insert(i, i);
1284        }
1285        for i in 0..c as u64 {
1286            cache.get(&i);
1287        }
1288        for i in c as u64..2 * c as u64 {
1289            cache.insert(i, i);
1290        }
1291        for i in 2 * c as u64..3 * c as u64 {
1292            cache.insert(i, i);
1293        }
1294        for i in 2 * c as u64..3 * c as u64 {
1295            cache.get(&i);
1296        }
1297        for i in 3 * c as u64..8 * c as u64 {
1298            cache.insert(i, i);
1299        }
1300
1301        let t = cache.t1_len() + cache.t2_len();
1302        let b = cache.b1_len() + cache.b2_len();
1303        let total = t + b;
1304
1305        assert!(
1306            total <= 2 * c,
1307            "ARC directory size ({}) exceeds 2*capacity ({}). \
1308             B1={}, B2={}, T1={}, T2={}",
1309            total,
1310            2 * c,
1311            cache.b1_len(),
1312            cache.b2_len(),
1313            cache.t1_len(),
1314            cache.t2_len(),
1315        );
1316    }
1317
1318    #[test]
1319    fn ghost_lists_bounded_when_cache_full() {
1320        let c = 10usize;
1321        let mut cache: ARCCore<u64, u64> = ARCCore::new(c);
1322
1323        for i in 0..500u64 {
1324            cache.insert(i, i);
1325        }
1326
1327        let t = cache.t1_len() + cache.t2_len();
1328        let b1 = cache.b1_len();
1329        let b2 = cache.b2_len();
1330
1331        assert!(
1332            b1 + b2 <= c,
1333            "Ghost lists hold {} entries (B1={}, B2={}) while cache holds {} (T1+T2). \
1334             Paper requires B1+B2 <= {}",
1335            b1 + b2,
1336            b1,
1337            b2,
1338            t,
1339            c,
1340        );
1341    }
1342}