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
14use triomphe::Arc as StrongArc;
15
16mod deque;
17mod hash_set;
18mod heap;
19mod map;
20mod production;
21mod small_vec;
22mod string;
23mod vec;
24
25pub use deque::BudgetedDeque;
26pub use hash_set::BudgetedHashSet;
27pub use heap::BudgetedBinaryHeap;
28pub use map::{
29    BudgetedMap, BudgetedMapIter, BudgetedSharedMap, BudgetedSharedMapIter,
30    BudgetedSharedMapSnapshot, OwnedMap, OwnedMapIntoIter, OwnedSet, OwnedSetIntoIter,
31    OwnedSetIter, PreparedMapEntry,
32};
33pub use production::{Produced, ProductionControl, ProductionString, ProductionVec};
34pub use small_vec::BudgetedSmallVec;
35pub use string::BudgetedString;
36pub use vec::BudgetedVec;
37
38#[derive(Debug, thiserror::Error)]
39pub enum MemoryError {
40    #[error("memory requires {required} bytes, exceeding limit {limit}")]
41    Limit { required: usize, limit: usize },
42    #[error("memory allocation size overflow")]
43    SizeOverflow,
44    #[error("memory allocation failed: {0}")]
45    Allocation(#[from] std::collections::TryReserveError),
46}
47
48#[derive(Debug)]
49struct Allowance {
50    limit: usize,
51    used: AtomicUsize,
52    peak: AtomicUsize,
53    parent: Option<MemoryBudget>,
54}
55
56impl Allowance {
57    fn claim(&self, additional: usize) -> Result<(), MemoryError> {
58        let mut used = self.used.load(Ordering::Relaxed);
59        loop {
60            let required = used
61                .checked_add(additional)
62                .ok_or(MemoryError::SizeOverflow)?;
63            if required > self.limit {
64                return Err(MemoryError::Limit {
65                    required,
66                    limit: self.limit,
67                });
68            }
69            match self.used.compare_exchange_weak(
70                used,
71                required,
72                Ordering::Relaxed,
73                Ordering::Relaxed,
74            ) {
75                Ok(_) => {
76                    self.peak.fetch_max(required, Ordering::Relaxed);
77                    return Ok(());
78                }
79                Err(current) => used = current,
80            }
81        }
82    }
83}
84
85impl Drop for Allowance {
86    fn drop(&mut self) {
87        // Destroy an unshared parent chain iteratively, including deeply nested application budgets.
88        let mut parent = self.parent.take();
89        while let Some(budget) = parent {
90            match StrongArc::try_unwrap(budget.0) {
91                Ok(mut allowance) => parent = allowance.parent.take(),
92                Err(_) => break,
93            }
94        }
95    }
96}
97
98/// Clones share one allowance, including reservations retained by completed producers.
99#[derive(Debug, Clone)]
100pub struct MemoryBudget(StrongArc<Allowance>);
101
102impl MemoryBudget {
103    pub fn new(limit: usize) -> Self {
104        Self(StrongArc::new(Allowance {
105            limit,
106            used: AtomicUsize::new(0),
107            peak: AtomicUsize::new(0),
108            parent: None,
109        }))
110    }
111
112    /// Add a component limit without enlarging this allowance. Every descendant reservation also charges each ancestor until its final owner releases it.
113    pub fn child(&self, limit: usize) -> Self {
114        Self(StrongArc::new(Allowance {
115            limit,
116            used: AtomicUsize::new(0),
117            peak: AtomicUsize::new(0),
118            parent: Some(self.clone()),
119        }))
120    }
121
122    fn allowances(&self) -> impl Iterator<Item = &Allowance> {
123        std::iter::successors(Some(self), |budget| budget.0.parent.as_ref())
124            .map(|budget| &*budget.0)
125    }
126
127    pub fn limit(&self) -> usize {
128        self.0.limit
129    }
130
131    pub fn used(&self) -> usize {
132        self.0.used.load(Ordering::Relaxed)
133    }
134
135    /// Currently unreserved bytes under this limit and every ancestor limit. This is a sizing hint; concurrent owners can consume it before a reservation succeeds.
136    pub fn available(&self) -> usize {
137        self.allowances()
138            .map(|allowance| {
139                allowance
140                    .limit
141                    .saturating_sub(allowance.used.load(Ordering::Relaxed))
142            })
143            .min()
144            .expect("a budget has its own allowance")
145    }
146
147    pub fn shares_allowance(&self, other: &Self) -> bool {
148        StrongArc::ptr_eq(&self.0, &other.0)
149    }
150
151    /// Largest simultaneous reservation, including old and replacement buffers.
152    pub fn peak(&self) -> usize {
153        self.0.peak.load(Ordering::Relaxed)
154    }
155
156    pub fn reserve(&self, bytes: usize) -> Result<MemoryReservation, MemoryError> {
157        let mut reservation = self.empty_reservation();
158        reservation.grow(bytes)?;
159        Ok(reservation)
160    }
161
162    pub fn empty_reservation(&self) -> MemoryReservation {
163        MemoryReservation {
164            budget: self.clone(),
165            bytes: 0,
166        }
167    }
168}
169
170/// A unique lease: release it only after the associated allocation has been freed.
171#[derive(Debug)]
172pub struct MemoryReservation {
173    budget: MemoryBudget,
174    bytes: usize,
175}
176
177impl MemoryReservation {
178    pub fn bytes(&self) -> usize {
179        self.bytes
180    }
181
182    pub fn budget(&self) -> &MemoryBudget {
183        &self.budget
184    }
185
186    /// Transfer part of an existing allocation lease without releasing or reserving bytes.
187    ///
188    /// Panics if `bytes` exceeds this lease. The original lease is unchanged on failure.
189    pub fn split(&mut self, bytes: usize) -> Self {
190        self.bytes = self
191            .bytes
192            .checked_sub(bytes)
193            .expect("insufficient reserved bytes");
194        Self {
195            budget: self.budget.clone(),
196            bytes,
197        }
198    }
199
200    /// Reserve additional bytes atomically; a failed request leaves this lease unchanged.
201    pub fn grow(&mut self, additional: usize) -> Result<(), MemoryError> {
202        if additional == 0 {
203            return Ok(());
204        }
205        for (claimed, allowance) in self.budget.allowances().enumerate() {
206            if let Err(error) = allowance.claim(additional) {
207                for previous in self.budget.allowances().take(claimed) {
208                    previous.used.fetch_sub(additional, Ordering::Relaxed);
209                }
210                return Err(error);
211            }
212        }
213        self.bytes += additional;
214        Ok(())
215    }
216
217    /// Transfer ownership without releasing and reacquiring the shared allowance.
218    ///
219    /// Panics if the leases belong to different allowances.
220    pub fn absorb(&mut self, mut other: Self) {
221        assert!(
222            self.budget.shares_allowance(&other.budget),
223            "different memory allowances"
224        );
225        self.bytes += other.bytes;
226        other.bytes = 0;
227    }
228}
229
230impl Drop for MemoryReservation {
231    fn drop(&mut self) {
232        for allowance in self.budget.allowances() {
233            allowance.used.fetch_sub(self.bytes, Ordering::Relaxed);
234        }
235    }
236}
237
238/// An immutable result and its owned allocations. Cloning the value is a separate allocation.
239#[derive(Debug)]
240pub struct Budgeted<T> {
241    // Field order ensures that the value is destroyed before its allowance is returned.
242    value: T,
243    memory: MemoryReservation,
244}
245
246impl<T> Budgeted<T> {
247    /// The caller must associate all owned allocations with this reservation.
248    pub fn new(value: T, memory: MemoryReservation) -> Self {
249        Self { value, memory }
250    }
251
252    pub fn reserved_bytes(&self) -> usize {
253        self.memory.bytes()
254    }
255
256    pub fn budget(&self) -> &MemoryBudget {
257        self.memory.budget()
258    }
259
260    /// Share the immutable value and its lease, reserving the shared payload before allocation. Reference-count bookkeeping is outside the payload allowance.
261    pub fn into_shared(self) -> Result<Arc<Self>, MemoryError> {
262        let mut memory = self.memory.budget().reserve(std::mem::size_of::<Self>())?;
263        let (value, allocations) = self.into_parts();
264        memory.absorb(allocations);
265        Ok(Arc::new(Self::new(value, memory)))
266    }
267
268    /// Move the value and lease together to another allocation owner.
269    pub fn into_parts(self) -> (T, MemoryReservation) {
270        (self.value, self.memory)
271    }
272}
273
274impl<T> std::ops::Deref for Budgeted<T> {
275    type Target = T;
276
277    fn deref(&self) -> &T {
278        &self.value
279    }
280}
281
282impl<T: PartialEq> PartialEq for Budgeted<T> {
283    fn eq(&self, other: &Self) -> bool {
284        self.value == other.value
285    }
286}
287
288impl<T: Eq> Eq for Budgeted<T> {}
289
290impl<T: AsRef<U>, U: ?Sized> AsRef<U> for Budgeted<T> {
291    fn as_ref(&self) -> &U {
292        self.value.as_ref()
293    }
294}
295
296fn buffer_bytes<T>(capacity: usize) -> Result<usize, MemoryError> {
297    capacity
298        .checked_mul(std::mem::size_of::<T>())
299        .filter(|bytes| isize::try_from(*bytes).is_ok())
300        .ok_or(MemoryError::SizeOverflow)
301}
302
303fn reconcile_buffer_capacity<T>(
304    memory: &mut MemoryReservation,
305    capacity: usize,
306) -> Result<(), MemoryError> {
307    memory.grow(buffer_bytes::<T>(capacity)?.saturating_sub(memory.bytes()))
308}
309
310fn replacement<T>(
311    budget: &MemoryBudget,
312    capacity: usize,
313    required: usize,
314) -> Result<(usize, MemoryReservation), MemoryError> {
315    let preferred = capacity.saturating_mul(2).max(required);
316    if let Ok(bytes) = buffer_bytes::<T>(preferred) {
317        if let Ok(memory) = budget.reserve(bytes) {
318            return Ok((preferred, memory));
319        }
320    }
321    Ok((required, budget.reserve(buffer_bytes::<T>(required)?)?))
322}
323
324#[cfg(test)]
325mod tests;