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}