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