use std::sync::atomic::{AtomicUsize, Ordering};
use crate::error::{QueryError, Result};
#[derive(Debug)]
pub struct MemoryTracker {
budget: usize,
allocated: AtomicUsize,
}
impl MemoryTracker {
#[must_use]
pub fn new(budget_bytes: usize) -> Self {
Self {
budget: budget_bytes,
allocated: AtomicUsize::new(0),
}
}
pub fn try_allocate(&self, bytes: usize) -> Result<()> {
loop {
let current = self.allocated.load(Ordering::Relaxed);
let new =
current
.checked_add(bytes)
.ok_or_else(|| QueryError::QueryMemoryExceeded {
requested: bytes,
allocated: current,
budget: self.budget,
})?;
if new > self.budget {
return Err(QueryError::QueryMemoryExceeded {
requested: bytes,
allocated: current,
budget: self.budget,
});
}
if self
.allocated
.compare_exchange_weak(current, new, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
return Ok(());
}
}
}
pub fn deallocate(&self, bytes: usize) {
let prev = self.allocated.fetch_sub(bytes, Ordering::AcqRel);
debug_assert!(
prev >= bytes,
"deallocate({bytes}) but only {prev} bytes were allocated"
);
}
#[must_use]
pub fn allocated(&self) -> usize {
self.allocated.load(Ordering::Relaxed)
}
#[must_use]
pub fn budget(&self) -> usize {
self.budget
}
#[must_use]
pub fn remaining(&self) -> usize {
self.budget
.saturating_sub(self.allocated.load(Ordering::Relaxed))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_allocate_deallocate() {
let t = MemoryTracker::new(1000);
assert_eq!(t.budget(), 1000);
assert_eq!(t.allocated(), 0);
assert_eq!(t.remaining(), 1000);
t.try_allocate(400).unwrap();
assert_eq!(t.allocated(), 400);
assert_eq!(t.remaining(), 600);
t.try_allocate(600).unwrap();
assert_eq!(t.allocated(), 1000);
assert_eq!(t.remaining(), 0);
t.deallocate(300);
assert_eq!(t.allocated(), 700);
assert_eq!(t.remaining(), 300);
}
#[test]
fn exceeds_budget() {
let t = MemoryTracker::new(100);
t.try_allocate(60).unwrap();
let err = t.try_allocate(50).unwrap_err();
match err {
QueryError::QueryMemoryExceeded {
requested,
allocated,
budget,
} => {
assert_eq!(requested, 50);
assert_eq!(allocated, 60);
assert_eq!(budget, 100);
}
other => panic!("unexpected error: {other}"),
}
assert_eq!(t.allocated(), 60);
}
#[test]
fn exact_budget() {
let t = MemoryTracker::new(100);
t.try_allocate(100).unwrap();
assert_eq!(t.allocated(), 100);
assert_eq!(t.remaining(), 0);
assert!(t.try_allocate(1).is_err());
}
#[test]
fn zero_budget() {
let t = MemoryTracker::new(0);
assert!(t.try_allocate(1).is_err());
t.try_allocate(0).unwrap();
}
#[test]
fn deallocate_full() {
let t = MemoryTracker::new(500);
t.try_allocate(500).unwrap();
t.deallocate(500);
assert_eq!(t.allocated(), 0);
assert_eq!(t.remaining(), 500);
t.try_allocate(500).unwrap();
}
#[test]
fn concurrent_allocations() {
use std::sync::Arc;
use std::thread;
let t = Arc::new(MemoryTracker::new(10_000));
let mut handles = vec![];
for _ in 0..10 {
let tracker = Arc::clone(&t);
handles.push(thread::spawn(move || {
for _ in 0..100 {
let _ = tracker.try_allocate(1);
}
}));
}
for h in handles {
h.join().unwrap();
}
assert!(t.allocated() <= 1000);
}
#[test]
fn error_display() {
let t = MemoryTracker::new(100);
t.try_allocate(80).unwrap();
let err = t.try_allocate(30).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("30"), "should mention requested bytes");
assert!(msg.contains("80"), "should mention allocated bytes");
assert!(msg.contains("100"), "should mention budget");
}
}