1use std::sync::atomic::{AtomicU64, Ordering};
11
12use ahash::AHashMap;
13use mentedb_core::MemoryNode;
14use parking_lot::Mutex;
15
16#[derive(Debug, Clone, Copy, Default)]
19pub struct BufferStats {
20 pub hits: u64,
21 pub misses: u64,
22 pub evictions: u64,
23 pub resident_pages: u64,
25 pub resident_bytes: u64,
27 pub capacity_bytes: u64,
29}
30
31struct Slot {
32 locator: u64,
33 node: MemoryNode,
34 charge: u64,
35 referenced: bool,
36}
37
38struct Inner {
39 slots: Vec<Option<Slot>>,
40 index: AHashMap<u64, usize>,
41 vacant: Vec<usize>,
42 hand: usize,
43 used: u64,
44 capacity: u64,
45}
46
47impl Inner {
48 fn remove(&mut self, locator: u64) {
49 if let Some(i) = self.index.remove(&locator)
50 && let Some(slot) = self.slots[i].take()
51 {
52 self.used -= slot.charge;
53 self.vacant.push(i);
54 }
55 }
56
57 fn make_room(&mut self, need: u64, evictions: &AtomicU64) {
60 let mut budget = self.slots.len() * 2 + 1;
61 while self.used + need > self.capacity && !self.index.is_empty() && budget > 0 {
62 budget -= 1;
63 let i = self.hand;
64 self.hand = (self.hand + 1) % self.slots.len();
65 let evict = match self.slots[i].as_mut() {
66 None => false,
67 Some(slot) if slot.referenced => {
68 slot.referenced = false;
69 false
70 }
71 Some(_) => true,
72 };
73 if evict && let Some(slot) = self.slots[i].take() {
74 self.index.remove(&slot.locator);
75 self.used -= slot.charge;
76 self.vacant.push(i);
77 evictions.fetch_add(1, Ordering::Relaxed);
78 }
79 }
80 }
81}
82
83pub struct RecordCache {
85 inner: Mutex<Inner>,
86 hits: AtomicU64,
87 misses: AtomicU64,
88 evictions: AtomicU64,
89}
90
91impl RecordCache {
92 pub fn new(capacity: u64) -> Self {
94 Self {
95 inner: Mutex::new(Inner {
96 slots: Vec::new(),
97 index: AHashMap::new(),
98 vacant: Vec::new(),
99 hand: 0,
100 used: 0,
101 capacity,
102 }),
103 hits: AtomicU64::new(0),
104 misses: AtomicU64::new(0),
105 evictions: AtomicU64::new(0),
106 }
107 }
108
109 pub fn get(&self, locator: u64) -> Option<MemoryNode> {
111 let mut inner = self.inner.lock();
112 let found = inner.index.get(&locator).copied();
113 match found.and_then(|i| inner.slots[i].as_mut()) {
114 Some(slot) => {
115 slot.referenced = true;
116 let node = slot.node.clone();
117 self.hits.fetch_add(1, Ordering::Relaxed);
118 Some(node)
119 }
120 None => {
121 self.misses.fetch_add(1, Ordering::Relaxed);
122 None
123 }
124 }
125 }
126
127 pub fn insert(&self, locator: u64, node: MemoryNode, charge: u64) {
130 let mut inner = self.inner.lock();
131 inner.remove(locator);
132 if charge > inner.capacity {
133 return;
134 }
135 inner.make_room(charge, &self.evictions);
136 if inner.used + charge > inner.capacity {
137 return;
138 }
139 let slot = Slot {
140 locator,
141 node,
142 charge,
143 referenced: false,
144 };
145 let i = match inner.vacant.pop() {
146 Some(i) => {
147 inner.slots[i] = Some(slot);
148 i
149 }
150 None => {
151 inner.slots.push(Some(slot));
152 inner.slots.len() - 1
153 }
154 };
155 inner.index.insert(locator, i);
156 inner.used += charge;
157 }
158
159 pub fn invalidate(&self, locator: u64) {
161 self.inner.lock().remove(locator);
162 }
163
164 pub fn set_capacity(&self, capacity: u64) {
166 let mut inner = self.inner.lock();
167 inner.capacity = capacity;
168 inner.make_room(0, &self.evictions);
169 }
170
171 pub fn capacity(&self) -> u64 {
173 self.inner.lock().capacity
174 }
175
176 pub fn stats(&self) -> BufferStats {
178 let inner = self.inner.lock();
179 BufferStats {
180 hits: self.hits.load(Ordering::Relaxed),
181 misses: self.misses.load(Ordering::Relaxed),
182 evictions: self.evictions.load(Ordering::Relaxed),
183 resident_pages: inner.index.len() as u64,
184 resident_bytes: inner.used,
185 capacity_bytes: inner.capacity,
186 }
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use mentedb_core::memory::MemoryType;
194 use mentedb_core::types::AgentId;
195
196 fn node(s: &str) -> MemoryNode {
197 MemoryNode::new(AgentId::new(), MemoryType::Semantic, s.into(), vec![])
198 }
199
200 #[test]
201 fn bounded_by_bytes() {
202 let c = RecordCache::new(1000);
203 for i in 0..10 {
204 c.insert(i, node(&i.to_string()), 300);
205 }
206 let s = c.stats();
207 assert!(s.resident_bytes <= 1000);
208 assert_eq!(s.resident_pages, 3);
209 assert!(s.evictions >= 7);
210 assert_eq!(c.get(9).unwrap().content, "9");
212 }
213
214 #[test]
215 fn referenced_entries_survive_a_sweep() {
216 let c = RecordCache::new(300);
217 c.insert(1, node("a"), 100);
218 c.insert(2, node("b"), 100);
219 c.insert(3, node("c"), 100);
220 c.get(1);
221 c.insert(4, node("d"), 100);
222 assert!(c.get(1).is_some(), "recently used entry kept");
223 assert!(c.get(2).is_none(), "unreferenced entry evicted");
224 }
225
226 #[test]
227 fn replace_invalidate_resize() {
228 let c = RecordCache::new(1000);
229 c.insert(1, node("a"), 400);
230 c.insert(1, node("b"), 500);
231 assert_eq!(c.stats().resident_bytes, 500);
232 assert_eq!(c.get(1).unwrap().content, "b");
233 c.invalidate(1);
234 assert!(c.get(1).is_none());
235 c.insert(2, node("x"), 2000);
236 assert!(c.get(2).is_none(), "larger than the cache");
237 c.insert(3, node("y"), 600);
238 c.set_capacity(100);
239 assert_eq!(c.stats().resident_bytes, 0);
240 let off = RecordCache::new(0);
241 off.insert(1, node("a"), 1);
242 assert!(off.get(1).is_none());
243 }
244}