use crate::context::ExecutionContext;
use crate::hash_table::JoinMemoryReservation;
use radixdb_core::{Error, Result, Row, Value};
#[doc(hidden)]
pub struct RetainedRowsBudget {
owner: &'static str,
max_rows: usize,
max_bytes: usize,
rows: usize,
bytes: usize,
peak_rows: usize,
peak_bytes: usize,
request_reservation: Option<JoinMemoryReservation>,
request_max_bytes: usize,
}
impl RetainedRowsBudget {
pub const DEFAULT_MAX_ROWS: usize = 1_000_000;
pub const DEFAULT_MAX_BYTES: usize = 256 * 1024 * 1024;
pub fn new(owner: &'static str) -> Self {
Self::with_limits(owner, Self::DEFAULT_MAX_ROWS, Self::DEFAULT_MAX_BYTES)
}
pub fn with_limits(owner: &'static str, max_rows: usize, max_bytes: usize) -> Self {
Self {
owner,
max_rows,
max_bytes,
rows: 0,
bytes: 0,
peak_rows: 0,
peak_bytes: 0,
request_reservation: None,
request_max_bytes: 0,
}
}
pub fn with_request_memory(owner: &'static str, ctx: &ExecutionContext) -> Result<Self> {
let mut budget = Self::new(owner);
budget.request_reservation = ctx.reserve_join_memory(0);
budget.request_max_bytes = ctx.join_hash_state_max_bytes();
if budget.request_reservation.is_none() {
return Err(Error::InvalidArgument(format!(
"{owner} request retained-memory budget is unavailable"
)));
}
Ok(budget)
}
pub fn admit(&mut self, row: &Row) -> Result<()> {
let row_bytes = Self::estimate_row_bytes(row);
self.admit_bytes(row_bytes)
}
pub fn admit_estimated_bytes(&mut self, retained_bytes: usize) -> Result<()> {
self.admit_bytes(retained_bytes)
}
pub fn admit_row_and_values(&mut self, row: &Row, values: &[Value]) -> Result<()> {
self.admit_bytes(
Self::estimate_row_bytes(row).saturating_add(Self::estimate_values_bytes(values)),
)
}
pub fn admit_values(&mut self, values: &[Value]) -> Result<()> {
self.admit_auxiliary_bytes(Self::estimate_values_bytes(values))
}
pub fn ensure_capacity(&self, rows: usize) -> Result<()> {
let minimum_bytes = rows.saturating_mul(std::mem::size_of::<Row>());
if rows > self.max_rows || minimum_bytes > self.max_bytes {
return Err(Error::InvalidArgument(format!(
"{} retained-row budget exceeded before allocation (rows {}/{}, minimum bytes {}/{})",
self.owner, rows, self.max_rows, minimum_bytes, self.max_bytes
)));
}
Ok(())
}
fn admit_bytes(&mut self, retained_bytes: usize) -> Result<()> {
let next_rows = self.rows.saturating_add(1);
let next_bytes = self.bytes.saturating_add(retained_bytes);
if next_rows > self.max_rows || next_bytes > self.max_bytes {
return Err(Error::InvalidArgument(format!(
"{} retained-row budget exceeded (rows {}/{}, bytes {}/{})",
self.owner, next_rows, self.max_rows, next_bytes, self.max_bytes
)));
}
if self
.request_reservation
.as_mut()
.is_some_and(|reservation| !reservation.try_resize(next_bytes, self.request_max_bytes))
{
return Err(Error::InvalidArgument(format!(
"{} request retained-memory budget exceeded (bytes {}/{})",
self.owner, next_bytes, self.request_max_bytes
)));
}
self.rows = next_rows;
self.bytes = next_bytes;
self.peak_rows = self.peak_rows.max(next_rows);
self.peak_bytes = self.peak_bytes.max(next_bytes);
Ok(())
}
fn admit_auxiliary_bytes(&mut self, retained_bytes: usize) -> Result<()> {
let next_bytes = self.bytes.saturating_add(retained_bytes);
if next_bytes > self.max_bytes {
return Err(Error::InvalidArgument(format!(
"{} retained-row budget exceeded (rows {}/{}, bytes {}/{})",
self.owner, self.rows, self.max_rows, next_bytes, self.max_bytes
)));
}
if self
.request_reservation
.as_mut()
.is_some_and(|reservation| !reservation.try_resize(next_bytes, self.request_max_bytes))
{
return Err(Error::InvalidArgument(format!(
"{} request retained-memory budget exceeded (bytes {}/{})",
self.owner, next_bytes, self.request_max_bytes
)));
}
self.bytes = next_bytes;
self.peak_bytes = self.peak_bytes.max(next_bytes);
Ok(())
}
pub fn release(&mut self, row: &Row) {
self.rows = self.rows.saturating_sub(1);
self.bytes = self.bytes.saturating_sub(Self::estimate_row_bytes(row));
self.resize_request_reservation_after_release();
}
pub fn release_row_and_values(&mut self, row: &Row, values: &[Value]) {
self.rows = self.rows.saturating_sub(1);
self.bytes = self.bytes.saturating_sub(
Self::estimate_row_bytes(row).saturating_add(Self::estimate_values_bytes(values)),
);
self.resize_request_reservation_after_release();
}
pub fn release_values(&mut self, values: &[Value]) {
self.bytes = self
.bytes
.saturating_sub(Self::estimate_values_bytes(values));
self.resize_request_reservation_after_release();
}
fn resize_request_reservation_after_release(&mut self) {
if let Some(reservation) = self.request_reservation.as_mut() {
debug_assert!(reservation.try_resize(self.bytes, self.request_max_bytes));
}
}
pub fn estimate_row_bytes(row: &Row) -> usize {
std::mem::size_of::<Row>().saturating_add(Self::estimate_values_bytes(row.as_slice()))
}
pub fn estimate_values_bytes(values: &[Value]) -> usize {
values.iter().fold(0, |total, value| {
let payload = match value {
Value::Text(text) => text.len(),
Value::Extension(bytes) => bytes.len(),
_ => 0,
};
total
.saturating_add(std::mem::size_of::<Value>())
.saturating_add(payload)
})
}
#[doc(hidden)]
pub fn retained_rows(&self) -> usize {
self.rows
}
#[doc(hidden)]
pub fn retained_bytes(&self) -> usize {
self.bytes
}
pub fn peak_rows(&self) -> usize {
self.peak_rows
}
pub fn peak_bytes(&self) -> usize {
self.peak_bytes
}
}