use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MemoryAccountingClass {
BufferPool,
VectorIndex,
FtsIndex,
Graph,
CompiledPlan,
Other,
}
impl MemoryAccountingClass {
pub fn as_str(self) -> &'static str {
match self {
MemoryAccountingClass::BufferPool => "buffer_pool",
MemoryAccountingClass::VectorIndex => "vector_index",
MemoryAccountingClass::FtsIndex => "fts_index",
MemoryAccountingClass::Graph => "graph",
MemoryAccountingClass::CompiledPlan => "compiled_plan",
MemoryAccountingClass::Other => "other",
}
}
pub fn discriminant(self) -> u8 {
match self {
MemoryAccountingClass::BufferPool => 0,
MemoryAccountingClass::VectorIndex => 1,
MemoryAccountingClass::FtsIndex => 2,
MemoryAccountingClass::Graph => 3,
MemoryAccountingClass::CompiledPlan => 4,
MemoryAccountingClass::Other => 5,
}
}
}
impl From<u8> for MemoryAccountingClass {
fn from(v: u8) -> Self {
match v {
0 => MemoryAccountingClass::BufferPool,
1 => MemoryAccountingClass::VectorIndex,
2 => MemoryAccountingClass::FtsIndex,
3 => MemoryAccountingClass::Graph,
4 => MemoryAccountingClass::CompiledPlan,
_ => MemoryAccountingClass::Other,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MemoryAttribution {
pub domain: &'static str,
pub class: MemoryAccountingClass,
}
pub const BUFFER_POOL: MemoryAttribution = MemoryAttribution {
domain: "buffer_pool",
class: MemoryAccountingClass::BufferPool,
};
pub const VECTOR_INDEX: MemoryAttribution = MemoryAttribution {
domain: "vector_index",
class: MemoryAccountingClass::VectorIndex,
};
pub const FTS_INDEX: MemoryAttribution = MemoryAttribution {
domain: "fts_index",
class: MemoryAccountingClass::FtsIndex,
};
#[derive(Debug, Default)]
pub struct MemoryAccountant {
total: AtomicU64,
by_class: Mutex<HashMap<MemoryAccountingClass, u64>>,
}
impl MemoryAccountant {
pub fn new() -> Self {
Self::default()
}
pub fn allocate(&self, attr: MemoryAttribution, amount: u64) {
self.total.fetch_add(amount, Ordering::Relaxed);
let mut by_class = self.by_class.lock().unwrap();
*by_class.entry(attr.class).or_insert(0) += amount;
}
pub fn deallocate(&self, attr: MemoryAttribution, amount: u64) {
self.total
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |prev| {
Some(prev.saturating_sub(amount))
})
.ok();
let mut by_class = self.by_class.lock().unwrap();
if let Some(bytes) = by_class.get_mut(&attr.class) {
*bytes = bytes.saturating_sub(amount);
}
}
pub fn total(&self) -> u64 {
self.total.load(Ordering::Relaxed)
}
pub fn class_usage(&self, class: MemoryAccountingClass) -> u64 {
self.by_class.lock().unwrap().get(&class).copied().unwrap_or(0)
}
pub fn snapshot(&self) -> Vec<(MemoryAccountingClass, u64)> {
let mut out: Vec<(MemoryAccountingClass, u64)> = self
.by_class
.lock()
.unwrap()
.iter()
.map(|(&class, &bytes)| (class, bytes))
.filter(|(_, bytes)| *bytes > 0)
.collect();
out.sort_by_key(|(class, _)| class.discriminant());
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_allocate_deallocate_roundtrip() {
let acc = MemoryAccountant::new();
acc.allocate(BUFFER_POOL, 4096);
acc.allocate(VECTOR_INDEX, 2048);
assert_eq!(acc.total(), 6144);
assert_eq!(acc.class_usage(MemoryAccountingClass::BufferPool), 4096);
acc.deallocate(BUFFER_POOL, 4096);
assert_eq!(acc.total(), 2048);
assert_eq!(acc.class_usage(MemoryAccountingClass::BufferPool), 0);
}
#[test]
fn test_deallocate_never_negative() {
let acc = MemoryAccountant::new();
acc.allocate(BUFFER_POOL, 100);
acc.deallocate(BUFFER_POOL, 10_000);
assert_eq!(acc.class_usage(MemoryAccountingClass::BufferPool), 0);
assert_eq!(acc.total(), 0);
}
#[test]
fn test_snapshot_sorted_non_zero() {
let acc = MemoryAccountant::new();
acc.allocate(FTS_INDEX, 128);
acc.allocate(BUFFER_POOL, 256);
let snap = acc.snapshot();
assert_eq!(
snap,
vec![
(MemoryAccountingClass::BufferPool, 256),
(MemoryAccountingClass::FtsIndex, 128),
]
);
}
#[test]
fn test_class_discriminants_roundtrip() {
for class in [
MemoryAccountingClass::BufferPool,
MemoryAccountingClass::VectorIndex,
MemoryAccountingClass::FtsIndex,
MemoryAccountingClass::Graph,
MemoryAccountingClass::CompiledPlan,
MemoryAccountingClass::Other,
] {
assert_eq!(MemoryAccountingClass::from(class.discriminant()), class);
}
}
}