Skip to main content

mentedb_storage/
cache.rs

1//! Record cache: decoded memory records, bounded by bytes, CLOCK eviction.
2//!
3//! The cache holds decoded [`MemoryNode`]s keyed by record locator, so a hit
4//! costs neither a read nor a decode. Capacity is in bytes (each entry is
5//! charged its encoded record size), which lets a host with spare memory size
6//! it to hold an entire database: every scan after the first is then served
7//! from memory. Entries are always clean; writes go to the file and the WAL
8//! before the cache is updated.
9
10use std::sync::atomic::{AtomicU64, Ordering};
11
12use ahash::AHashMap;
13use mentedb_core::MemoryNode;
14use parking_lot::Mutex;
15
16/// A snapshot of cache activity, for metrics. `hits / (hits + misses)` is the
17/// hit ratio, the primary read performance signal.
18#[derive(Debug, Clone, Copy, Default)]
19pub struct BufferStats {
20    pub hits: u64,
21    pub misses: u64,
22    pub evictions: u64,
23    /// Records currently cached.
24    pub resident_pages: u64,
25    /// Bytes charged by the cached records.
26    pub resident_bytes: u64,
27    /// Configured capacity in bytes.
28    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    /// Evict until `need` more bytes fit. Every entry is evictable (no pins),
58    /// so two sweeps always free enough when the cache is not empty.
59    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
83/// Thread safe, byte bounded record cache.
84pub struct RecordCache {
85    inner: Mutex<Inner>,
86    hits: AtomicU64,
87    misses: AtomicU64,
88    evictions: AtomicU64,
89}
90
91impl RecordCache {
92    /// A cache holding up to `capacity` bytes of records. Zero disables it.
93    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    /// The cached node for `locator`, counting a hit or a miss.
110    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    /// Cache `node` for `locator`, charged `charge` bytes, replacing any
128    /// previous entry. A record larger than the whole cache is not cached.
129    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    /// Drop the entry for `locator`, if cached.
160    pub fn invalidate(&self, locator: u64) {
161        self.inner.lock().remove(locator);
162    }
163
164    /// Change the capacity, evicting as needed to fit the new bound.
165    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    /// Configured capacity in bytes.
172    pub fn capacity(&self) -> u64 {
173        self.inner.lock().capacity
174    }
175
176    /// Activity counters and residency.
177    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        // The newest entry is resident.
211        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}