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