Skip to main content

ember_core/
memory.rs

1//! Memory tracking for the keyspace.
2//!
3//! Provides byte-level accounting of memory used by entries. Updated
4//! on every mutation so the engine can enforce memory limits and
5//! report stats without scanning the entire keyspace.
6//!
7//! # Platform notes
8//!
9//! Overhead constants are empirical estimates for 64-bit platforms (x86-64,
10//! aarch64). On 32-bit systems these would be smaller; the effect is that
11//! we'd overestimate memory usage, which triggers eviction earlier than
12//! necessary but doesn't cause correctness issues.
13//!
14//! The constants assume Rust's standard library allocator. Custom allocators
15//! (jemalloc, mimalloc) may have different per-allocation overhead.
16
17use crate::types::Value;
18
19/// Estimated overhead per entry in the HashMap.
20///
21/// Accounts for: HashMap bucket pointer (8), Entry struct fields
22/// (Option<Instant> = 16, last_access Instant = 8, Value enum tag + padding),
23/// plus HashMap per-entry bookkeeping.
24///
25/// This is an approximation measured empirically on x86-64 linux. The exact
26/// value varies by platform and compiler version, but precision isn't critical —
27/// we use this for eviction triggers and memory reporting, not for correctness.
28/// Overestimating is fine (triggers eviction earlier); underestimating could
29/// theoretically let memory grow slightly beyond the configured limit.
30pub(crate) const ENTRY_OVERHEAD: usize = 96;
31
32/// Tracks memory usage for a single keyspace.
33///
34/// All updates are explicit — callers must call `add` / `remove` on every
35/// mutation. This avoids any hidden scanning cost.
36#[derive(Debug)]
37pub struct MemoryTracker {
38    used_bytes: usize,
39    key_count: usize,
40}
41
42impl MemoryTracker {
43    /// Creates a tracker with zero usage.
44    pub fn new() -> Self {
45        Self {
46            used_bytes: 0,
47            key_count: 0,
48        }
49    }
50
51    /// Resets tracking to zero. Used by FLUSHDB.
52    pub fn reset(&mut self) {
53        self.used_bytes = 0;
54        self.key_count = 0;
55    }
56
57    /// Returns the current estimated memory usage in bytes.
58    pub fn used_bytes(&self) -> usize {
59        self.used_bytes
60    }
61
62    /// Returns the number of tracked keys.
63    pub fn key_count(&self) -> usize {
64        self.key_count
65    }
66
67    /// Records the addition of a new entry.
68    pub fn add(&mut self, key: &str, value: &Value) {
69        self.used_bytes += entry_size(key, value);
70        self.key_count += 1;
71    }
72
73    /// Records the removal of an entry.
74    pub fn remove(&mut self, key: &str, value: &Value) {
75        let size = entry_size(key, value);
76        self.used_bytes = self.used_bytes.saturating_sub(size);
77        self.key_count = self.key_count.saturating_sub(1);
78    }
79
80    /// Adjusts tracking when a key's value is overwritten.
81    ///
82    /// Removes the old value's contribution and adds the new one.
83    /// Key count stays the same.
84    pub fn replace(&mut self, key: &str, old_value: &Value, new_value: &Value) {
85        let old_size = entry_size(key, old_value);
86        let new_size = entry_size(key, new_value);
87        self.used_bytes = self
88            .used_bytes
89            .saturating_sub(old_size)
90            .saturating_add(new_size);
91    }
92
93    /// Adjusts used bytes for an in-place mutation (e.g. list push/pop)
94    /// without changing the key count.
95    ///
96    /// `old_entry_size` and `new_entry_size` are the full entry sizes
97    /// (as returned by `entry_size`) before and after the mutation.
98    pub fn adjust(&mut self, old_entry_size: usize, new_entry_size: usize) {
99        self.used_bytes = self
100            .used_bytes
101            .saturating_sub(old_entry_size)
102            .saturating_add(new_entry_size);
103    }
104
105    /// Removes an entry with an explicit size, useful when the value has
106    /// already been mutated and the original size was captured beforehand.
107    pub fn remove_with_size(&mut self, size: usize) {
108        self.used_bytes = self.used_bytes.saturating_sub(size);
109        self.key_count = self.key_count.saturating_sub(1);
110    }
111}
112
113impl Default for MemoryTracker {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119/// Estimates the total memory footprint of a single entry.
120///
121/// key heap allocation + value bytes + fixed per-entry overhead.
122pub fn entry_size(key: &str, value: &Value) -> usize {
123    key.len() + value_size(value) + ENTRY_OVERHEAD
124}
125
126/// Estimated overhead per element in a VecDeque.
127///
128/// Each slot holds a `Bytes` (pointer + len + capacity = 24 bytes on 64-bit)
129/// plus VecDeque's internal bookkeeping per slot.
130pub(crate) const VECDEQUE_ELEMENT_OVERHEAD: usize = 32;
131
132/// Base overhead for an empty VecDeque (internal buffer pointer + head/len).
133pub(crate) const VECDEQUE_BASE_OVERHEAD: usize = 24;
134
135/// Estimated overhead per entry in a HashMap (for hash type).
136///
137/// Each entry has: key String (24 bytes ptr+len+cap), value Bytes (24 bytes),
138/// plus HashMap bucket overhead (~16 bytes for hash + next pointer).
139pub(crate) const HASHMAP_ENTRY_OVERHEAD: usize = 64;
140
141/// Base overhead for an empty HashMap (bucket array pointer + len + capacity).
142pub(crate) const HASHMAP_BASE_OVERHEAD: usize = 48;
143
144/// Estimated overhead per member in a HashSet.
145///
146/// Each member is a String (24 bytes ptr+len+cap) plus bucket overhead.
147pub(crate) const HASHSET_MEMBER_OVERHEAD: usize = 40;
148
149/// Base overhead for an empty HashSet (bucket array pointer + len + capacity).
150pub(crate) const HASHSET_BASE_OVERHEAD: usize = 48;
151
152/// Returns the byte size of a value's payload.
153pub fn value_size(value: &Value) -> usize {
154    match value {
155        Value::String(data) => data.len(),
156        Value::List(deque) => {
157            let element_bytes: usize = deque
158                .iter()
159                .map(|b| b.len() + VECDEQUE_ELEMENT_OVERHEAD)
160                .sum();
161            VECDEQUE_BASE_OVERHEAD + element_bytes
162        }
163        Value::SortedSet(ss) => ss.memory_usage(),
164        Value::Hash(map) => {
165            let entry_bytes: usize = map
166                .iter()
167                .map(|(k, v)| k.len() + v.len() + HASHMAP_ENTRY_OVERHEAD)
168                .sum();
169            HASHMAP_BASE_OVERHEAD + entry_bytes
170        }
171        Value::Set(set) => {
172            let member_bytes: usize = set.iter().map(|m| m.len() + HASHSET_MEMBER_OVERHEAD).sum();
173            HASHSET_BASE_OVERHEAD + member_bytes
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use bytes::Bytes;
182
183    fn string_val(s: &str) -> Value {
184        Value::String(Bytes::from(s.to_string()))
185    }
186
187    #[test]
188    fn new_tracker_is_empty() {
189        let t = MemoryTracker::new();
190        assert_eq!(t.used_bytes(), 0);
191        assert_eq!(t.key_count(), 0);
192    }
193
194    #[test]
195    fn add_increases_usage() {
196        let mut t = MemoryTracker::new();
197        let val = string_val("hello");
198        t.add("key", &val);
199        assert_eq!(t.key_count(), 1);
200        assert_eq!(t.used_bytes(), entry_size("key", &val));
201    }
202
203    #[test]
204    fn remove_decreases_usage() {
205        let mut t = MemoryTracker::new();
206        let val = string_val("data");
207        t.add("k", &val);
208        t.remove("k", &val);
209        assert_eq!(t.used_bytes(), 0);
210        assert_eq!(t.key_count(), 0);
211    }
212
213    #[test]
214    fn replace_adjusts_usage() {
215        let mut t = MemoryTracker::new();
216        let old = string_val("short");
217        let new = string_val("a much longer value here");
218        t.add("k", &old);
219
220        let before = t.used_bytes();
221        t.replace("k", &old, &new);
222
223        assert_eq!(t.key_count(), 1);
224        // new value is longer, so usage should increase
225        assert!(t.used_bytes() > before);
226        assert_eq!(t.used_bytes(), entry_size("k", &new),);
227    }
228
229    #[test]
230    fn remove_saturates_at_zero() {
231        let mut t = MemoryTracker::new();
232        let val = string_val("x");
233        // remove without add — should not underflow
234        t.remove("k", &val);
235        assert_eq!(t.used_bytes(), 0);
236        assert_eq!(t.key_count(), 0);
237    }
238
239    #[test]
240    fn entry_size_accounts_for_key_and_value() {
241        let val = string_val("test");
242        let size = entry_size("mykey", &val);
243        // 5 (key) + 4 (value) + 96 (overhead)
244        assert_eq!(size, 5 + 4 + ENTRY_OVERHEAD);
245    }
246
247    #[test]
248    fn list_value_size() {
249        let mut deque = std::collections::VecDeque::new();
250        deque.push_back(Bytes::from("hello"));
251        deque.push_back(Bytes::from("world"));
252        let val = Value::List(deque);
253
254        let size = value_size(&val);
255        // base overhead + 2 elements (each: data len + element overhead)
256        let expected = VECDEQUE_BASE_OVERHEAD
257            + (5 + VECDEQUE_ELEMENT_OVERHEAD)
258            + (5 + VECDEQUE_ELEMENT_OVERHEAD);
259        assert_eq!(size, expected);
260    }
261
262    #[test]
263    fn empty_list_value_size() {
264        let val = Value::List(std::collections::VecDeque::new());
265        assert_eq!(value_size(&val), VECDEQUE_BASE_OVERHEAD);
266    }
267
268    #[test]
269    fn multiple_entries() {
270        let mut t = MemoryTracker::new();
271        let v1 = string_val("aaa");
272        let v2 = string_val("bbbbb");
273        t.add("k1", &v1);
274        t.add("k2", &v2);
275
276        assert_eq!(t.key_count(), 2);
277        assert_eq!(
278            t.used_bytes(),
279            entry_size("k1", &v1) + entry_size("k2", &v2),
280        );
281
282        t.remove("k1", &v1);
283        assert_eq!(t.key_count(), 1);
284        assert_eq!(t.used_bytes(), entry_size("k2", &v2));
285    }
286}