akar_common/
memory_account.rs1use std::collections::HashMap;
9use std::sync::Mutex;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum MemoryAccountingClass {
15 BufferPool,
17 VectorIndex,
19 FtsIndex,
21 Graph,
23 CompiledPlan,
25 Other,
27}
28
29impl MemoryAccountingClass {
30 pub fn as_str(self) -> &'static str {
32 match self {
33 MemoryAccountingClass::BufferPool => "buffer_pool",
34 MemoryAccountingClass::VectorIndex => "vector_index",
35 MemoryAccountingClass::FtsIndex => "fts_index",
36 MemoryAccountingClass::Graph => "graph",
37 MemoryAccountingClass::CompiledPlan => "compiled_plan",
38 MemoryAccountingClass::Other => "other",
39 }
40 }
41
42 pub fn discriminant(self) -> u8 {
44 match self {
45 MemoryAccountingClass::BufferPool => 0,
46 MemoryAccountingClass::VectorIndex => 1,
47 MemoryAccountingClass::FtsIndex => 2,
48 MemoryAccountingClass::Graph => 3,
49 MemoryAccountingClass::CompiledPlan => 4,
50 MemoryAccountingClass::Other => 5,
51 }
52 }
53}
54
55impl From<u8> for MemoryAccountingClass {
56 fn from(v: u8) -> Self {
57 match v {
58 0 => MemoryAccountingClass::BufferPool,
59 1 => MemoryAccountingClass::VectorIndex,
60 2 => MemoryAccountingClass::FtsIndex,
61 3 => MemoryAccountingClass::Graph,
62 4 => MemoryAccountingClass::CompiledPlan,
63 _ => MemoryAccountingClass::Other,
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct MemoryAttribution {
71 pub domain: &'static str,
73 pub class: MemoryAccountingClass,
75}
76
77pub const BUFFER_POOL: MemoryAttribution = MemoryAttribution {
79 domain: "buffer_pool",
80 class: MemoryAccountingClass::BufferPool,
81};
82
83pub const VECTOR_INDEX: MemoryAttribution = MemoryAttribution {
85 domain: "vector_index",
86 class: MemoryAccountingClass::VectorIndex,
87};
88
89pub const FTS_INDEX: MemoryAttribution = MemoryAttribution {
91 domain: "fts_index",
92 class: MemoryAccountingClass::FtsIndex,
93};
94
95#[derive(Debug, Default)]
100pub struct MemoryAccountant {
101 total: AtomicU64,
102 by_class: Mutex<HashMap<MemoryAccountingClass, u64>>,
103}
104
105impl MemoryAccountant {
106 pub fn new() -> Self {
108 Self::default()
109 }
110
111 pub fn allocate(&self, attr: MemoryAttribution, amount: u64) {
113 self.total.fetch_add(amount, Ordering::Relaxed);
114 let mut by_class = self.by_class.lock().unwrap();
115 *by_class.entry(attr.class).or_insert(0) += amount;
116 }
117
118 pub fn deallocate(&self, attr: MemoryAttribution, amount: u64) {
121 self.total
122 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |prev| {
123 Some(prev.saturating_sub(amount))
124 })
125 .ok();
126 let mut by_class = self.by_class.lock().unwrap();
127 if let Some(bytes) = by_class.get_mut(&attr.class) {
128 *bytes = bytes.saturating_sub(amount);
129 }
130 }
131
132 pub fn total(&self) -> u64 {
134 self.total.load(Ordering::Relaxed)
135 }
136
137 pub fn class_usage(&self, class: MemoryAccountingClass) -> u64 {
139 self.by_class.lock().unwrap().get(&class).copied().unwrap_or(0)
140 }
141
142 pub fn snapshot(&self) -> Vec<(MemoryAccountingClass, u64)> {
144 let mut out: Vec<(MemoryAccountingClass, u64)> = self
145 .by_class
146 .lock()
147 .unwrap()
148 .iter()
149 .map(|(&class, &bytes)| (class, bytes))
150 .filter(|(_, bytes)| *bytes > 0)
151 .collect();
152 out.sort_by_key(|(class, _)| class.discriminant());
153 out
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_allocate_deallocate_roundtrip() {
163 let acc = MemoryAccountant::new();
164 acc.allocate(BUFFER_POOL, 4096);
165 acc.allocate(VECTOR_INDEX, 2048);
166 assert_eq!(acc.total(), 6144);
167 assert_eq!(acc.class_usage(MemoryAccountingClass::BufferPool), 4096);
168 acc.deallocate(BUFFER_POOL, 4096);
169 assert_eq!(acc.total(), 2048);
170 assert_eq!(acc.class_usage(MemoryAccountingClass::BufferPool), 0);
171 }
172
173 #[test]
174 fn test_deallocate_never_negative() {
175 let acc = MemoryAccountant::new();
176 acc.allocate(BUFFER_POOL, 100);
177 acc.deallocate(BUFFER_POOL, 10_000);
178 assert_eq!(acc.class_usage(MemoryAccountingClass::BufferPool), 0);
179 assert_eq!(acc.total(), 0);
180 }
181
182 #[test]
183 fn test_snapshot_sorted_non_zero() {
184 let acc = MemoryAccountant::new();
185 acc.allocate(FTS_INDEX, 128);
186 acc.allocate(BUFFER_POOL, 256);
187 let snap = acc.snapshot();
188 assert_eq!(
189 snap,
190 vec![
191 (MemoryAccountingClass::BufferPool, 256),
192 (MemoryAccountingClass::FtsIndex, 128),
193 ]
194 );
195 }
196
197 #[test]
198 fn test_class_discriminants_roundtrip() {
199 for class in [
200 MemoryAccountingClass::BufferPool,
201 MemoryAccountingClass::VectorIndex,
202 MemoryAccountingClass::FtsIndex,
203 MemoryAccountingClass::Graph,
204 MemoryAccountingClass::CompiledPlan,
205 MemoryAccountingClass::Other,
206 ] {
207 assert_eq!(MemoryAccountingClass::from(class.discriminant()), class);
208 }
209 }
210}