molframe_core/execution/
memory.rs1use std::fmt;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicUsize, Ordering};
6
7pub const DEFAULT_MEMORY_BUDGET_BYTES: usize = 100_000_000;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct MemoryBudget {
17 bytes: usize,
18}
19
20impl MemoryBudget {
21 pub const fn new(bytes: usize) -> Result<Self, MemoryBudgetError> {
32 if bytes == 0 {
33 return Err(MemoryBudgetError::Zero);
34 }
35 Ok(Self { bytes })
36 }
37
38 #[must_use]
40 pub const fn bytes(self) -> usize {
41 self.bytes
42 }
43}
44
45impl Default for MemoryBudget {
46 fn default() -> Self {
47 Self {
48 bytes: DEFAULT_MEMORY_BUDGET_BYTES,
49 }
50 }
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum MemoryBudgetError {
56 Zero,
58 Exhausted {
60 requested: usize,
62 available: usize,
64 },
65}
66
67impl fmt::Display for MemoryBudgetError {
68 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 Self::Zero => formatter.write_str("execution memory budget must be non-zero"),
71 Self::Exhausted {
72 requested,
73 available,
74 } => write!(
75 formatter,
76 "execution requested {requested} bytes with {available} bytes available"
77 ),
78 }
79 }
80}
81
82impl std::error::Error for MemoryBudgetError {}
83
84#[derive(Debug)]
85pub(crate) struct MemoryAccount {
86 budget: MemoryBudget,
87 reserved: AtomicUsize,
88 peak_reserved: AtomicUsize,
89 live_batches: AtomicUsize,
90 peak_live_batches: AtomicUsize,
91}
92
93impl MemoryAccount {
94 pub(crate) fn new(budget: MemoryBudget) -> Self {
95 Self {
96 budget,
97 reserved: AtomicUsize::new(0),
98 peak_reserved: AtomicUsize::new(0),
99 live_batches: AtomicUsize::new(0),
100 peak_live_batches: AtomicUsize::new(0),
101 }
102 }
103
104 pub(crate) const fn budget(&self) -> MemoryBudget {
105 self.budget
106 }
107
108 pub(crate) fn reserve(
109 account: &Arc<Self>,
110 bytes: usize,
111 ) -> Result<MemoryReservation, MemoryBudgetError> {
112 let mut current = account.reserved.load(Ordering::Acquire);
113 loop {
114 let Some(next) = current.checked_add(bytes) else {
115 return Err(MemoryBudgetError::Exhausted {
116 requested: bytes,
117 available: account.budget.bytes().saturating_sub(current),
118 });
119 };
120 if next > account.budget.bytes() {
121 return Err(MemoryBudgetError::Exhausted {
122 requested: bytes,
123 available: account.budget.bytes().saturating_sub(current),
124 });
125 }
126 match account.reserved.compare_exchange_weak(
127 current,
128 next,
129 Ordering::AcqRel,
130 Ordering::Acquire,
131 ) {
132 Ok(_) => {
133 account.peak_reserved.fetch_max(next, Ordering::AcqRel);
134 return Ok(MemoryReservation {
135 account: Arc::clone(account),
136 bytes,
137 });
138 }
139 Err(observed) => current = observed,
140 }
141 }
142 }
143
144 pub(crate) fn reserved_bytes(&self) -> usize {
145 self.reserved.load(Ordering::Acquire)
146 }
147
148 pub(crate) fn peak_reserved_bytes(&self) -> usize {
149 self.peak_reserved.load(Ordering::Acquire)
150 }
151
152 pub(crate) fn live_batches(&self) -> usize {
153 self.live_batches.load(Ordering::Acquire)
154 }
155
156 pub(crate) fn peak_live_batches(&self) -> usize {
157 self.peak_live_batches.load(Ordering::Acquire)
158 }
159
160 fn begin_batch(account: &Arc<Self>) -> BatchGuard {
161 let live = account.live_batches.fetch_add(1, Ordering::AcqRel) + 1;
162 account.peak_live_batches.fetch_max(live, Ordering::AcqRel);
163 BatchGuard {
164 account: Arc::clone(account),
165 }
166 }
167}
168
169#[derive(Debug)]
174pub struct MemoryReservation {
175 account: Arc<MemoryAccount>,
176 bytes: usize,
177}
178
179impl MemoryReservation {
180 #[must_use]
182 pub fn belongs_to(&self, context: &super::ExecutionContext) -> bool {
183 Arc::ptr_eq(&self.account, &context.memory)
184 }
185
186 #[must_use]
188 pub const fn bytes(&self) -> usize {
189 self.bytes
190 }
191
192 pub fn shrink_to(&mut self, bytes: usize) {
194 if bytes >= self.bytes {
195 return;
196 }
197 let released = self.bytes - bytes;
198 self.bytes = bytes;
199 self.account.reserved.fetch_sub(released, Ordering::AcqRel);
200 }
201
202 pub fn try_grow(&mut self, additional: usize) -> Result<(), MemoryBudgetError> {
209 if additional == 0 {
210 return Ok(());
211 }
212 let mut extra = MemoryAccount::reserve(&self.account, additional)?;
213 let Some(bytes) = self.bytes.checked_add(additional) else {
214 return Err(MemoryBudgetError::Exhausted {
215 requested: additional,
216 available: 0,
217 });
218 };
219 self.bytes = bytes;
220 extra.bytes = 0;
221 Ok(())
222 }
223
224 pub(crate) fn begin_batch(&self) -> BatchGuard {
225 MemoryAccount::begin_batch(&self.account)
226 }
227}
228
229impl Drop for MemoryReservation {
230 fn drop(&mut self) {
231 self.account
232 .reserved
233 .fetch_sub(self.bytes, Ordering::AcqRel);
234 }
235}
236
237#[derive(Debug)]
239pub(crate) struct BatchGuard {
240 account: Arc<MemoryAccount>,
241}
242
243impl Drop for BatchGuard {
244 fn drop(&mut self) {
245 self.account.live_batches.fetch_sub(1, Ordering::AcqRel);
246 }
247}
248
249#[cfg(test)]
250#[path = "memory_tests.rs"]
251mod tests;
252
253crate::diagnostic_from!(
254 MemoryBudgetError,
255 |_error| crate::Code::E7001,
256 |diagnostic, error| match error {
257 MemoryBudgetError::Zero => diagnostic.with_context("budget", "zero"),
258 MemoryBudgetError::Exhausted {
259 requested,
260 available,
261 } => diagnostic
262 .with_context("requested_bytes", requested.to_string())
263 .with_context("available_bytes", available.to_string()),
264 }
265);