1use std::collections::VecDeque;
5
6const MIN_COMPACTION_POSITIONS: usize = 16;
9
10#[derive(Debug)]
20pub struct Recency<K> {
21 order: VecDeque<(K, u64)>,
22 counter: u64,
23}
24
25impl<K> Default for Recency<K> {
26 fn default() -> Self {
27 Self {
28 order: VecDeque::new(),
29 counter: 0,
30 }
31 }
32}
33
34impl<K: Clone> Recency<K> {
35 pub fn touch(&mut self, key: &K) -> u64 {
42 self.counter = self.counter.saturating_add(1);
43 self.order.push_back((key.clone(), self.counter));
44 self.counter
45 }
46
47 pub fn pop_oldest(&mut self, mut is_live: impl FnMut(&K, u64) -> bool) -> Option<K> {
52 while let Some((key, stamp)) = self.order.pop_front() {
53 if is_live(&key, stamp) {
54 return Some(key);
55 }
56 }
57 None
58 }
59
60 pub fn compact(&mut self, live_len: usize, mut is_live: impl FnMut(&K, u64) -> bool) {
63 let positions_before_compacting = live_len.saturating_mul(2).max(MIN_COMPACTION_POSITIONS);
64 if self.order.len() <= positions_before_compacting {
65 return;
66 }
67 self.order.retain(|(key, stamp)| is_live(key, *stamp));
68 }
69
70 pub fn positions(&self) -> usize {
72 self.order.len()
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::Recency;
79 use std::collections::HashMap;
80
81 #[derive(Default)]
84 struct Entries(HashMap<&'static str, u64>);
85
86 impl Entries {
87 fn is_live(&self, key: &&'static str, stamp: u64) -> bool {
88 self.0.get(key).is_some_and(|live| *live == stamp)
89 }
90 }
91
92 fn touch(recency: &mut Recency<&'static str>, entries: &mut Entries, key: &'static str) {
93 let stamp = recency.touch(&key);
94 entries.0.insert(key, stamp);
95 recency.compact(entries.0.len(), |key, stamp| entries.is_live(key, stamp));
96 }
97
98 #[test]
99 fn eviction_returns_keys_least_recently_touched_first() {
100 let mut recency = Recency::default();
101 let mut entries = Entries::default();
102 for key in ["a", "b", "c"] {
103 touch(&mut recency, &mut entries, key);
104 }
105 touch(&mut recency, &mut entries, "a");
106
107 let evicted = recency
108 .pop_oldest(|key, stamp| entries.is_live(key, stamp))
109 .expect("a live position");
110 assert_eq!(evicted, "b");
111 }
112
113 #[test]
114 fn eviction_skips_the_ghosts_a_re_touch_left_behind() {
115 let mut recency = Recency::default();
116 let mut entries = Entries::default();
117 touch(&mut recency, &mut entries, "a");
118 touch(&mut recency, &mut entries, "a");
119 touch(&mut recency, &mut entries, "b");
120
121 let evicted = recency
122 .pop_oldest(|key, stamp| entries.is_live(key, stamp))
123 .expect("a live position");
124 assert_eq!(evicted, "a", "the stale position for a must not evict b");
125 }
126
127 #[test]
130 fn eviction_reports_a_queue_of_ghosts_as_empty() {
131 let mut recency = Recency::default();
132 let mut entries = Entries::default();
133 touch(&mut recency, &mut entries, "a");
134 entries.0.clear();
135
136 assert!(recency
137 .pop_oldest(|key, stamp| entries.is_live(key, stamp))
138 .is_none());
139 }
140
141 #[test]
142 fn repeated_hits_on_one_key_keep_the_queue_bounded() {
143 let mut recency = Recency::default();
144 let mut entries = Entries::default();
145 for key in ["a", "b", "c"] {
146 touch(&mut recency, &mut entries, key);
147 }
148 for _ in 0..10_000 {
149 touch(&mut recency, &mut entries, "a");
150 }
151
152 assert_eq!(entries.0.len(), 3);
153 assert!(
154 recency.positions() <= (entries.0.len() * 2).max(16),
155 "compaction must bound the queue, positions = {}",
156 recency.positions()
157 );
158 }
159
160 #[test]
161 fn compaction_holds_off_until_ghosts_outnumber_live_entries() {
162 let mut recency = Recency::default();
163 let mut entries = Entries::default();
164 touch(&mut recency, &mut entries, "a");
165 assert_eq!(recency.positions(), 1);
166
167 for _ in 0..15 {
169 touch(&mut recency, &mut entries, "a");
170 }
171 assert_eq!(recency.positions(), 16);
172
173 touch(&mut recency, &mut entries, "a");
174 assert_eq!(recency.positions(), 1, "crossing the floor drops ghosts");
175 }
176}