use linkrs_core::error::QueryError;
use linkrs_core::Value;
use std::sync::Arc;
static BUDGET_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
#[derive(Clone)]
pub struct MemoryBudget {
pub max_bytes: usize,
allocated: Arc<std::sync::atomic::AtomicUsize>,
pub(crate) id: usize,
}
impl std::fmt::Debug for MemoryBudget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemoryBudget")
.field("max_bytes", &self.max_bytes)
.field(
"allocated",
&self.allocated.load(std::sync::atomic::Ordering::Relaxed),
)
.field("id", &self.id)
.finish()
}
}
impl MemoryBudget {
pub const DEFAULT_MAX: usize = 512 * 1024 * 1024;
pub fn new(max_bytes: usize) -> Self {
Self {
max_bytes,
allocated: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
id: BUDGET_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
}
}
pub fn default_budget() -> Self {
Self::new(Self::DEFAULT_MAX)
}
pub fn try_reserve(&self, bytes: usize) -> Result<bool, QueryError> {
let mut prev = self.allocated.load(std::sync::atomic::Ordering::Relaxed);
loop {
let total = prev.checked_add(bytes).ok_or_else(|| {
QueryError::execution(format!(
"Memory budget overflow: request {} bytes overflows usize",
bytes,
))
})?;
if total > self.max_bytes {
return Err(QueryError::execution(format!(
"Memory budget exceeded: request {} bytes, total {} > budget {} bytes",
bytes, total, self.max_bytes,
)));
}
match self.allocated.compare_exchange_weak(
prev,
total,
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
) {
Ok(_) => return Ok(true),
Err(current) => prev = current,
}
}
}
pub fn release(&self, bytes: usize) {
self.allocated
.fetch_sub(bytes, std::sync::atomic::Ordering::Relaxed);
}
pub fn current(&self) -> usize {
self.allocated.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn estimate_row_memory(row: &[Value]) -> usize {
row.iter().map(|v| v.estimated_size()).sum()
}
pub fn estimate_rows_memory(rows: &[Vec<Value>]) -> usize {
rows.iter().map(|row| Self::estimate_row_memory(row)).sum()
}
}
#[derive(Debug, Clone)]
pub struct MemoryTracker {
budget: MemoryBudget,
peak_bytes: usize,
current_bytes: usize,
}
impl MemoryTracker {
pub fn new(budget: MemoryBudget) -> Self {
Self {
budget,
peak_bytes: 0,
current_bytes: 0,
}
}
pub fn try_reserve(&mut self, bytes: usize) -> Result<(), QueryError> {
self.budget.try_reserve(bytes)?;
self.current_bytes += bytes;
self.peak_bytes = self.peak_bytes.max(self.current_bytes);
Ok(())
}
pub fn release(&mut self, bytes: usize) {
self.budget.release(bytes);
self.current_bytes = self.current_bytes.saturating_sub(bytes);
}
pub fn peak(&self) -> usize {
self.peak_bytes
}
pub fn current(&self) -> usize {
self.current_bytes
}
pub fn try_reserve_row(&mut self, row: &[Value]) -> Result<(), QueryError> {
let mem = MemoryBudget::estimate_row_memory(row);
self.try_reserve(mem)
}
pub fn try_reserve_rows(&mut self, rows: &[Vec<Value>]) -> Result<(), QueryError> {
let mem = MemoryBudget::estimate_rows_memory(rows);
self.try_reserve(mem)
}
pub fn reset(&mut self) {
self.budget.release(self.current_bytes);
self.current_bytes = 0;
}
}
#[derive(Debug)]
pub struct MemoryReservation {
budget: MemoryBudget,
bytes: usize,
}
#[derive(Debug)]
pub struct MemoryTrackerReservation<'a> {
tracker: &'a mut MemoryTracker,
bytes: usize,
}
impl MemoryReservation {
fn new(budget: MemoryBudget, bytes: usize) -> Self {
Self { budget, bytes }
}
pub fn forget(mut self) {
self.bytes = 0;
}
pub fn bytes(&self) -> usize {
self.bytes
}
}
impl Drop for MemoryReservation {
fn drop(&mut self) {
if self.bytes > 0 {
self.budget.release(self.bytes);
}
}
}
impl Drop for MemoryTrackerReservation<'_> {
fn drop(&mut self) {
if self.bytes > 0 {
self.tracker.release(self.bytes);
}
}
}
impl MemoryBudget {
pub fn reserve(&self, bytes: usize) -> Result<MemoryReservation, QueryError> {
self.try_reserve(bytes)?;
Ok(MemoryReservation::new(self.clone(), bytes))
}
}
impl MemoryTracker {
pub fn reserve_guarded(
&mut self,
bytes: usize,
) -> Result<MemoryTrackerReservation<'_>, QueryError> {
self.try_reserve(bytes)?;
Ok(MemoryTrackerReservation {
tracker: self,
bytes,
})
}
}
pub trait Spillable {
fn spill_to_disk(&mut self) -> Result<(), QueryError>;
fn spilled_size(&self) -> u64;
fn spill_count(&self) -> u64 {
0
}
fn spilled_rows(&self) -> u64 {
0
}
fn has_spilled(&self) -> bool {
self.spilled_size() > 0
}
fn try_spill(&mut self) -> Result<bool, QueryError> {
if !self.has_spilled() {
self.spill_to_disk()?;
}
Ok(self.has_spilled())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tracked_reservation_releases_budget_and_tracker() {
let budget = MemoryBudget::new(64);
let mut tracker = MemoryTracker::new(budget.clone());
{
let reservation = tracker.reserve_guarded(32).expect("reserve memory");
assert_eq!(reservation.bytes, 32);
}
assert_eq!(budget.current(), 0);
assert_eq!(tracker.current(), 0);
assert_eq!(tracker.peak(), 32);
}
}