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