Skip to main content

uqa_core/
memory.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Shared byte allowances with reservations that follow allocation ownership.
8//!
9//! Owners reserve requested buffer layouts before allocating and charge any additional reported capacity before moving values. Allocator bookkeeping, borrowed data, and separately owned immutable resources are outside this allowance.
10
11use std::sync::atomic::{AtomicUsize, Ordering};
12use std::sync::Arc;
13
14mod deque;
15mod hash_set;
16mod heap;
17mod map;
18mod production;
19mod string;
20mod vec;
21
22pub use deque::BudgetedDeque;
23pub use hash_set::BudgetedHashSet;
24pub use heap::BudgetedBinaryHeap;
25pub use map::{
26    BudgetedMap, BudgetedMapIter, BudgetedSharedMap, BudgetedSharedMapIter, OwnedMap,
27    OwnedMapIntoIter, OwnedSet, OwnedSetIntoIter, OwnedSetIter, PreparedMapEntry,
28};
29pub use production::{Produced, ProductionControl, ProductionString, ProductionVec};
30pub use string::BudgetedString;
31pub use vec::BudgetedVec;
32
33#[derive(Debug, thiserror::Error)]
34pub enum MemoryError {
35    #[error("memory requires {required} bytes, exceeding limit {limit}")]
36    Limit { required: usize, limit: usize },
37    #[error("memory allocation size overflow")]
38    SizeOverflow,
39    #[error("memory allocation failed: {0}")]
40    Allocation(#[from] std::collections::TryReserveError),
41}
42
43#[derive(Debug)]
44struct Allowance {
45    limit: usize,
46    used: AtomicUsize,
47    peak: AtomicUsize,
48}
49
50/// Clones share one allowance, including reservations retained by completed producers.
51#[derive(Debug, Clone)]
52pub struct MemoryBudget(Arc<Allowance>);
53
54impl MemoryBudget {
55    pub fn new(limit: usize) -> Self {
56        Self(Arc::new(Allowance {
57            limit,
58            used: AtomicUsize::new(0),
59            peak: AtomicUsize::new(0),
60        }))
61    }
62
63    pub fn limit(&self) -> usize {
64        self.0.limit
65    }
66
67    pub fn used(&self) -> usize {
68        self.0.used.load(Ordering::Relaxed)
69    }
70
71    pub fn shares_allowance(&self, other: &Self) -> bool {
72        Arc::ptr_eq(&self.0, &other.0)
73    }
74
75    /// Largest simultaneous reservation, including old and replacement buffers.
76    pub fn peak(&self) -> usize {
77        self.0.peak.load(Ordering::Relaxed)
78    }
79
80    pub fn reserve(&self, bytes: usize) -> Result<MemoryReservation, MemoryError> {
81        let mut reservation = self.empty_reservation();
82        reservation.grow(bytes)?;
83        Ok(reservation)
84    }
85
86    pub fn empty_reservation(&self) -> MemoryReservation {
87        MemoryReservation {
88            budget: self.clone(),
89            bytes: 0,
90        }
91    }
92}
93
94/// A unique lease: release it only after the associated allocation has been freed.
95#[derive(Debug)]
96pub struct MemoryReservation {
97    budget: MemoryBudget,
98    bytes: usize,
99}
100
101impl MemoryReservation {
102    pub fn bytes(&self) -> usize {
103        self.bytes
104    }
105
106    pub fn budget(&self) -> &MemoryBudget {
107        &self.budget
108    }
109
110    /// Transfer part of an existing allocation lease without releasing or reserving bytes.
111    ///
112    /// Panics if `bytes` exceeds this lease. The original lease is unchanged on failure.
113    pub fn split(&mut self, bytes: usize) -> Self {
114        self.bytes = self
115            .bytes
116            .checked_sub(bytes)
117            .expect("insufficient reserved bytes");
118        Self {
119            budget: self.budget.clone(),
120            bytes,
121        }
122    }
123
124    /// Reserve additional bytes atomically; a failed request leaves this lease unchanged.
125    pub fn grow(&mut self, additional: usize) -> Result<(), MemoryError> {
126        if additional == 0 {
127            return Ok(());
128        }
129        let allowance = &self.budget.0;
130        let mut used = allowance.used.load(Ordering::Relaxed);
131        loop {
132            let required = used
133                .checked_add(additional)
134                .ok_or(MemoryError::SizeOverflow)?;
135            if required > allowance.limit {
136                return Err(MemoryError::Limit {
137                    required,
138                    limit: allowance.limit,
139                });
140            }
141            match allowance.used.compare_exchange_weak(
142                used,
143                required,
144                Ordering::Relaxed,
145                Ordering::Relaxed,
146            ) {
147                Ok(_) => {
148                    self.bytes += additional;
149                    allowance.peak.fetch_max(required, Ordering::Relaxed);
150                    return Ok(());
151                }
152                Err(current) => used = current,
153            }
154        }
155    }
156
157    /// Transfer ownership without releasing and reacquiring the shared allowance.
158    ///
159    /// Panics if the leases belong to different allowances.
160    pub fn absorb(&mut self, mut other: Self) {
161        assert!(
162            Arc::ptr_eq(&self.budget.0, &other.budget.0),
163            "different memory allowances"
164        );
165        self.bytes += other.bytes;
166        other.bytes = 0;
167    }
168}
169
170impl Drop for MemoryReservation {
171    fn drop(&mut self) {
172        self.budget.0.used.fetch_sub(self.bytes, Ordering::Relaxed);
173    }
174}
175
176/// An immutable result and its owned allocations. Cloning the value is a separate allocation.
177#[derive(Debug)]
178pub struct Budgeted<T> {
179    // Field order ensures that the value is destroyed before its allowance is returned.
180    value: T,
181    memory: MemoryReservation,
182}
183
184impl<T> Budgeted<T> {
185    /// The caller must associate all owned allocations with this reservation.
186    pub fn new(value: T, memory: MemoryReservation) -> Self {
187        Self { value, memory }
188    }
189
190    pub fn reserved_bytes(&self) -> usize {
191        self.memory.bytes()
192    }
193
194    pub fn budget(&self) -> &MemoryBudget {
195        self.memory.budget()
196    }
197
198    /// Share the immutable value and its lease, reserving the shared payload before allocation. Reference-count bookkeeping is outside the payload allowance.
199    pub fn into_shared(self) -> Result<Arc<Self>, MemoryError> {
200        let mut memory = self.memory.budget().reserve(std::mem::size_of::<Self>())?;
201        let (value, allocations) = self.into_parts();
202        memory.absorb(allocations);
203        Ok(Arc::new(Self::new(value, memory)))
204    }
205
206    /// Move the value and lease together to another allocation owner.
207    pub fn into_parts(self) -> (T, MemoryReservation) {
208        (self.value, self.memory)
209    }
210}
211
212impl<T> std::ops::Deref for Budgeted<T> {
213    type Target = T;
214
215    fn deref(&self) -> &T {
216        &self.value
217    }
218}
219
220impl<T: PartialEq> PartialEq for Budgeted<T> {
221    fn eq(&self, other: &Self) -> bool {
222        self.value == other.value
223    }
224}
225
226impl<T: Eq> Eq for Budgeted<T> {}
227
228impl<T: AsRef<U>, U: ?Sized> AsRef<U> for Budgeted<T> {
229    fn as_ref(&self) -> &U {
230        self.value.as_ref()
231    }
232}
233
234fn buffer_bytes<T>(capacity: usize) -> Result<usize, MemoryError> {
235    capacity
236        .checked_mul(std::mem::size_of::<T>())
237        .filter(|bytes| isize::try_from(*bytes).is_ok())
238        .ok_or(MemoryError::SizeOverflow)
239}
240
241fn reconcile_buffer_capacity<T>(
242    memory: &mut MemoryReservation,
243    capacity: usize,
244) -> Result<(), MemoryError> {
245    memory.grow(buffer_bytes::<T>(capacity)?.saturating_sub(memory.bytes()))
246}
247
248fn replacement<T>(
249    budget: &MemoryBudget,
250    capacity: usize,
251    required: usize,
252) -> Result<(usize, MemoryReservation), MemoryError> {
253    let preferred = capacity.saturating_mul(2).max(required);
254    if let Ok(bytes) = buffer_bytes::<T>(preferred) {
255        if let Ok(memory) = budget.reserve(bytes) {
256            return Ok((preferred, memory));
257        }
258    }
259    Ok((required, budget.reserve(buffer_bytes::<T>(required)?)?))
260}
261
262#[cfg(test)]
263mod tests;