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    pub fn shares_allowance(&self, other: &Self) -> bool {
136        StrongArc::ptr_eq(&self.0, &other.0)
137    }
138
139    /// Largest simultaneous reservation, including old and replacement buffers.
140    pub fn peak(&self) -> usize {
141        self.0.peak.load(Ordering::Relaxed)
142    }
143
144    pub fn reserve(&self, bytes: usize) -> Result<MemoryReservation, MemoryError> {
145        let mut reservation = self.empty_reservation();
146        reservation.grow(bytes)?;
147        Ok(reservation)
148    }
149
150    pub fn empty_reservation(&self) -> MemoryReservation {
151        MemoryReservation {
152            budget: self.clone(),
153            bytes: 0,
154        }
155    }
156}
157
158/// A unique lease: release it only after the associated allocation has been freed.
159#[derive(Debug)]
160pub struct MemoryReservation {
161    budget: MemoryBudget,
162    bytes: usize,
163}
164
165impl MemoryReservation {
166    pub fn bytes(&self) -> usize {
167        self.bytes
168    }
169
170    pub fn budget(&self) -> &MemoryBudget {
171        &self.budget
172    }
173
174    /// Transfer part of an existing allocation lease without releasing or reserving bytes.
175    ///
176    /// Panics if `bytes` exceeds this lease. The original lease is unchanged on failure.
177    pub fn split(&mut self, bytes: usize) -> Self {
178        self.bytes = self
179            .bytes
180            .checked_sub(bytes)
181            .expect("insufficient reserved bytes");
182        Self {
183            budget: self.budget.clone(),
184            bytes,
185        }
186    }
187
188    /// Reserve additional bytes atomically; a failed request leaves this lease unchanged.
189    pub fn grow(&mut self, additional: usize) -> Result<(), MemoryError> {
190        if additional == 0 {
191            return Ok(());
192        }
193        for (claimed, allowance) in self.budget.allowances().enumerate() {
194            if let Err(error) = allowance.claim(additional) {
195                for previous in self.budget.allowances().take(claimed) {
196                    previous.used.fetch_sub(additional, Ordering::Relaxed);
197                }
198                return Err(error);
199            }
200        }
201        self.bytes += additional;
202        Ok(())
203    }
204
205    /// Transfer ownership without releasing and reacquiring the shared allowance.
206    ///
207    /// Panics if the leases belong to different allowances.
208    pub fn absorb(&mut self, mut other: Self) {
209        assert!(
210            self.budget.shares_allowance(&other.budget),
211            "different memory allowances"
212        );
213        self.bytes += other.bytes;
214        other.bytes = 0;
215    }
216}
217
218impl Drop for MemoryReservation {
219    fn drop(&mut self) {
220        for allowance in self.budget.allowances() {
221            allowance.used.fetch_sub(self.bytes, Ordering::Relaxed);
222        }
223    }
224}
225
226/// An immutable result and its owned allocations. Cloning the value is a separate allocation.
227#[derive(Debug)]
228pub struct Budgeted<T> {
229    // Field order ensures that the value is destroyed before its allowance is returned.
230    value: T,
231    memory: MemoryReservation,
232}
233
234impl<T> Budgeted<T> {
235    /// The caller must associate all owned allocations with this reservation.
236    pub fn new(value: T, memory: MemoryReservation) -> Self {
237        Self { value, memory }
238    }
239
240    pub fn reserved_bytes(&self) -> usize {
241        self.memory.bytes()
242    }
243
244    pub fn budget(&self) -> &MemoryBudget {
245        self.memory.budget()
246    }
247
248    /// Share the immutable value and its lease, reserving the shared payload before allocation. Reference-count bookkeeping is outside the payload allowance.
249    pub fn into_shared(self) -> Result<Arc<Self>, MemoryError> {
250        let mut memory = self.memory.budget().reserve(std::mem::size_of::<Self>())?;
251        let (value, allocations) = self.into_parts();
252        memory.absorb(allocations);
253        Ok(Arc::new(Self::new(value, memory)))
254    }
255
256    /// Move the value and lease together to another allocation owner.
257    pub fn into_parts(self) -> (T, MemoryReservation) {
258        (self.value, self.memory)
259    }
260}
261
262impl<T> std::ops::Deref for Budgeted<T> {
263    type Target = T;
264
265    fn deref(&self) -> &T {
266        &self.value
267    }
268}
269
270impl<T: PartialEq> PartialEq for Budgeted<T> {
271    fn eq(&self, other: &Self) -> bool {
272        self.value == other.value
273    }
274}
275
276impl<T: Eq> Eq for Budgeted<T> {}
277
278impl<T: AsRef<U>, U: ?Sized> AsRef<U> for Budgeted<T> {
279    fn as_ref(&self) -> &U {
280        self.value.as_ref()
281    }
282}
283
284fn buffer_bytes<T>(capacity: usize) -> Result<usize, MemoryError> {
285    capacity
286        .checked_mul(std::mem::size_of::<T>())
287        .filter(|bytes| isize::try_from(*bytes).is_ok())
288        .ok_or(MemoryError::SizeOverflow)
289}
290
291fn reconcile_buffer_capacity<T>(
292    memory: &mut MemoryReservation,
293    capacity: usize,
294) -> Result<(), MemoryError> {
295    memory.grow(buffer_bytes::<T>(capacity)?.saturating_sub(memory.bytes()))
296}
297
298fn replacement<T>(
299    budget: &MemoryBudget,
300    capacity: usize,
301    required: usize,
302) -> Result<(usize, MemoryReservation), MemoryError> {
303    let preferred = capacity.saturating_mul(2).max(required);
304    if let Ok(bytes) = buffer_bytes::<T>(preferred) {
305        if let Ok(memory) = budget.reserve(bytes) {
306            return Ok((preferred, memory));
307        }
308    }
309    Ok((required, budget.reserve(buffer_bytes::<T>(required)?)?))
310}
311
312#[cfg(test)]
313mod tests;