1use std::{
2 sync::{
3 atomic::{AtomicU64, AtomicUsize, Ordering},
4 Arc, Mutex,
5 },
6 time::{Duration, Instant},
7};
8
9use super::registry::RegistryState;
10use super::{
11 CacheDomain, CacheRegistration, CacheScope, CacheUsage, DomainInstanceId, DomainRegistration,
12 DomainSnapshot, MemoryAction, MemoryEvent, MemoryOptions, MemorySnapshot, TrimReason,
13 TrimRequest, TrimSnapshot,
14};
15
16#[cfg(feature = "persistent-cache")]
17use super::PersistentCacheStore;
18
19const FRAME_BUDGET_CHECK_INTERVAL: Duration = Duration::from_millis(250);
20
21#[derive(Clone)]
22pub struct MemoryGovernor {
23 inner: Arc<MemoryGovernorInner>,
24}
25
26struct MemoryGovernorInner {
27 options: Mutex<MemoryOptions>,
28 registry: Arc<Mutex<RegistryState>>,
29 epoch: AtomicU64,
30 transient_reserved: Arc<AtomicUsize>,
31 large_tasks_in_flight: Arc<AtomicUsize>,
32 last_trim: Mutex<Option<TrimSnapshot>>,
33 last_frame_budget_check: Mutex<Option<Instant>>,
34 #[cfg(feature = "persistent-cache")]
35 persistent: Option<Arc<dyn PersistentCacheStore>>,
36}
37
38impl MemoryGovernor {
39 pub fn new(options: MemoryOptions) -> Self {
40 Self::with_store(
41 options,
42 #[cfg(feature = "persistent-cache")]
43 None,
44 )
45 }
46
47 pub(crate) fn with_store(
48 options: MemoryOptions,
49 #[cfg(feature = "persistent-cache")] persistent: Option<Arc<dyn PersistentCacheStore>>,
50 ) -> Self {
51 options
52 .validate()
53 .expect("invalid application memory policy");
54 Self {
55 inner: Arc::new(MemoryGovernorInner {
56 options: Mutex::new(options),
57 registry: Arc::new(Mutex::new(RegistryState::default())),
58 epoch: AtomicU64::new(1),
59 transient_reserved: Arc::new(AtomicUsize::new(0)),
60 large_tasks_in_flight: Arc::new(AtomicUsize::new(0)),
61 last_trim: Mutex::new(None),
62 last_frame_budget_check: Mutex::new(None),
63 #[cfg(feature = "persistent-cache")]
64 persistent,
65 }),
66 }
67 }
68
69 pub fn options(&self) -> MemoryOptions {
70 *self.inner.options.lock().expect("memory options poisoned")
71 }
72
73 pub fn set_options(&self, options: MemoryOptions) {
74 options
75 .validate()
76 .expect("invalid application memory policy");
77 *self.inner.options.lock().expect("memory options poisoned") = options;
78 self.bump_epoch();
79 self.rebalance_budgets();
80 self.enforce_budget(true);
81 #[cfg(feature = "persistent-cache")]
82 if let Some(store) = self.persistent_cache() {
83 let _ = store.trim_to(options.budget.persistent_bytes);
84 }
85 }
86
87 pub fn next_instance_id(&self) -> DomainInstanceId {
88 let mut registry = self
89 .inner
90 .registry
91 .lock()
92 .expect("memory registry poisoned");
93 let id = registry.next_instance;
94 registry.next_instance = registry.next_instance.wrapping_add(1).max(1);
95 DomainInstanceId(id)
96 }
97
98 pub fn register(&self, registration: DomainRegistration) -> CacheRegistration {
99 let mut registry = self
100 .inner
101 .registry
102 .lock()
103 .expect("memory registry poisoned");
104 let id = registry.next_registration;
105 registry.next_registration = registry.next_registration.wrapping_add(1).max(1);
106 registry.entries.insert(id, registration);
107 drop(registry);
108 self.rebalance_budgets();
109 self.bump_epoch();
110 let governor = self.clone();
111 CacheRegistration::new(id, Arc::downgrade(&self.inner.registry), move || {
112 governor.rebalance_budgets();
113 governor.bump_epoch();
114 })
115 }
116
117 pub fn snapshot(&self) -> MemorySnapshot {
118 let epoch = self.inner.epoch.load(Ordering::Acquire);
119 let entries = self.registered_entries();
120 let mut usage = CacheUsage::default();
121 let mut domains = Vec::with_capacity(entries.len());
122 for (id, registration) in entries {
123 let domain_usage = registration.adapter.usage();
124 usage.add_assign(domain_usage);
125 domains.push(DomainSnapshot {
126 registration_id: id,
127 domain: registration.domain,
128 instance: registration.instance,
129 owner: registration.owner,
130 usage: domain_usage,
131 });
132 }
133 let options = self.options();
134 let cache_soft = options.budget.cache_soft_bytes;
135 MemorySnapshot {
136 epoch,
137 options,
138 transient_reserved_bytes: self.inner.transient_reserved.load(Ordering::Acquire),
139 large_tasks_in_flight: self.inner.large_tasks_in_flight.load(Ordering::Acquire),
140 pinned_overflow_bytes: usage.pinned_bytes.saturating_sub(cache_soft),
141 usage,
142 domains,
143 last_trim: *self
144 .inner
145 .last_trim
146 .lock()
147 .expect("memory trim state poisoned"),
148 }
149 }
150
151 pub fn trim(&self, reason: TrimReason, scope: CacheScope, target_bytes: usize) -> usize {
152 let epoch = self.bump_epoch();
153 let started = Instant::now();
154 let entries = if scope == CacheScope::Persistent {
155 Vec::new()
156 } else {
157 self.registered_entries()
158 };
159 let assignments = entries
160 .iter()
161 .filter(|(_, registration)| {
162 registration.domain != CacheDomain::Persistent
163 && (scope == CacheScope::AllRebuildable
164 || registration.domain != CacheDomain::HostScene)
165 })
166 .map(|(_, registration)| {
167 (
168 registration,
169 self.assigned_budget(registration.domain, &entries),
170 )
171 })
172 .collect::<Vec<_>>();
173 let assigned_total = assignments
174 .iter()
175 .map(|(_, assigned)| *assigned as u128)
176 .sum::<u128>();
177 let mut released = 0usize;
178 for (registration, assigned) in assignments {
179 let domain_target = if target_bytes == 0 {
180 0
181 } else if target_bytes == usize::MAX {
182 assigned
183 } else if assigned_total == 0 {
184 0
185 } else {
186 let proportional =
187 (assigned as u128).saturating_mul(target_bytes as u128) / assigned_total;
188 usize::try_from(proportional).unwrap_or(usize::MAX)
189 };
190 let result = registration.adapter.trim(TrimRequest {
191 reason,
192 scope,
193 target_bytes: domain_target,
194 });
195 released = released.saturating_add(result.released_bytes());
196 }
197 #[cfg(feature = "persistent-cache")]
198 if scope == CacheScope::Persistent {
199 if let Some(store) = self.inner.persistent.as_ref() {
200 let target = u64::try_from(target_bytes).unwrap_or(u64::MAX);
201 if let (Ok(before), Ok(after)) = (store.stats(), store.trim_to(target)) {
202 released = released.saturating_add(
203 usize::try_from(before.bytes.saturating_sub(after.bytes))
204 .unwrap_or(usize::MAX),
205 );
206 }
207 }
208 }
209 let duration_micros = started.elapsed().as_micros().min(u64::MAX as u128) as u64;
210 *self
211 .inner
212 .last_trim
213 .lock()
214 .expect("memory trim state poisoned") = Some(TrimSnapshot {
215 reason,
216 requested_at_epoch: epoch,
217 released_bytes: released,
218 duration_micros,
219 });
220 released
221 }
222
223 pub fn invalidate_domain(&self, domain: CacheDomain) -> usize {
224 let entries = self.registered_entries();
225 let mut released = 0usize;
226 for (_, registration) in entries {
227 if registration.domain == domain {
228 released = released.saturating_add(
229 registration
230 .adapter
231 .trim(TrimRequest {
232 reason: TrimReason::Explicit,
233 scope: CacheScope::Memory,
234 target_bytes: 0,
235 })
236 .released_bytes(),
237 );
238 }
239 }
240 self.bump_epoch();
241 released
242 }
243
244 pub fn notify(&self, event: MemoryEvent) {
245 match self.options().event_action(event) {
246 MemoryAction::None => {}
247 MemoryAction::EnforceBudget => {
248 if event == MemoryEvent::FrameCommitted {
249 if self.begin_frame_budget_check() {
250 self.finish_frame_budget_check();
251 }
252 } else {
253 self.enforce_budget(true);
254 }
255 }
256 MemoryAction::Trim {
257 scope,
258 target_bytes,
259 } => {
260 self.trim(event.trim_reason(), scope, target_bytes);
261 }
262 }
263 }
264
265 pub fn try_reserve(&self, bytes: usize) -> Option<MemoryReservation> {
266 let hard = self.options().budget.transient_hard_bytes;
267 let reserved = &self.inner.transient_reserved;
268 if hard == usize::MAX {
269 return Some(MemoryReservation {
270 bytes,
271 accounted_bytes: 0,
272 reserved: Arc::clone(reserved),
273 });
274 }
275 let mut current = reserved.load(Ordering::Acquire);
276 loop {
277 let next = current.checked_add(bytes)?;
278 if next > hard {
279 self.trim(TrimReason::HardBudget, CacheScope::Memory, 0);
280 return None;
281 }
282 match reserved.compare_exchange_weak(current, next, Ordering::AcqRel, Ordering::Acquire)
283 {
284 Ok(_) => {
285 return Some(MemoryReservation {
286 bytes,
287 accounted_bytes: bytes,
288 reserved: Arc::clone(reserved),
289 })
290 }
291 Err(actual) => current = actual,
292 }
293 }
294 }
295
296 pub fn try_reserve_task(&self, bytes: usize) -> Option<MemoryTaskReservation> {
297 let limit = self.options().budget.max_parallel_large_tasks;
298 let in_flight = &self.inner.large_tasks_in_flight;
299 let mut current = in_flight.load(Ordering::Acquire);
300 loop {
301 if current >= limit {
302 return None;
303 }
304 match in_flight.compare_exchange_weak(
305 current,
306 current + 1,
307 Ordering::AcqRel,
308 Ordering::Acquire,
309 ) {
310 Ok(_) => break,
311 Err(actual) => current = actual,
312 }
313 }
314 let Some(reservation) = self.try_reserve(bytes) else {
315 in_flight.fetch_sub(1, Ordering::AcqRel);
316 return None;
317 };
318 Some(MemoryTaskReservation {
319 _reservation: reservation,
320 in_flight: Arc::clone(in_flight),
321 })
322 }
323
324 #[cfg(feature = "persistent-cache")]
325 pub fn persistent_cache(&self) -> Option<Arc<dyn PersistentCacheStore>> {
326 self.options()
327 .persistent_cache_enabled
328 .then(|| self.inner.persistent.as_ref().map(Arc::clone))
329 .flatten()
330 }
331
332 #[cfg(feature = "persistent-cache")]
333 pub fn persistent_cache_stats(
334 &self,
335 ) -> Result<super::PersistentCacheStats, super::CacheStoreError> {
336 self.inner.persistent.as_ref().map_or_else(
337 || Ok(super::PersistentCacheStats::default()),
338 |store| store.stats(),
339 )
340 }
341
342 #[cfg(feature = "persistent-cache")]
343 pub fn clear_persistent_cache(
344 &self,
345 namespace: Option<&str>,
346 ) -> Result<super::PersistentCacheStats, super::CacheStoreError> {
347 if let Some(store) = self.inner.persistent.as_ref() {
348 store.clear(namespace)?;
349 self.bump_epoch();
350 return store.stats();
351 }
352 Ok(super::PersistentCacheStats::default())
353 }
354
355 fn enforce_budget(&self, include_soft_limit: bool) {
356 let options = self.options();
357 let usage = self.budget_usage();
358 let evictable = usage.managed_bytes.saturating_sub(usage.protected_bytes);
359 let reason = if evictable > options.budget.cache_hard_bytes {
360 Some(TrimReason::HardBudget)
361 } else if include_soft_limit && evictable > options.budget.cache_soft_bytes {
362 Some(TrimReason::SoftBudget)
363 } else {
364 None
365 };
366 if let Some(reason) = reason {
367 self.trim(reason, CacheScope::Memory, options.budget.cache_soft_bytes);
368 }
369 }
370
371 pub(crate) fn begin_frame_budget_check(&self) -> bool {
372 if self.options().event_action(MemoryEvent::FrameCommitted) != MemoryAction::EnforceBudget {
373 return false;
374 }
375 let now = Instant::now();
376 let mut last = self
377 .inner
378 .last_frame_budget_check
379 .lock()
380 .expect("memory budget check state poisoned");
381 if last.is_some_and(|previous| {
382 now.saturating_duration_since(previous) < FRAME_BUDGET_CHECK_INTERVAL
383 }) {
384 return false;
385 }
386 *last = Some(now);
387 true
388 }
389
390 pub(crate) fn finish_frame_budget_check(&self) {
391 self.enforce_budget(false);
392 }
393
394 fn budget_usage(&self) -> BudgetUsage {
395 let probes = self.registered_usage_probes();
396 let mut result = BudgetUsage::default();
397 for (domain, adapter) in probes {
398 let usage = adapter.usage();
399 let managed = usage.managed_bytes();
400 result.managed_bytes = result.managed_bytes.saturating_add(managed);
401 let protected = if domain == CacheDomain::HostScene {
402 managed
403 } else {
404 usage.pinned_bytes.min(managed)
405 };
406 result.protected_bytes = result.protected_bytes.saturating_add(protected);
407 }
408 result
409 }
410
411 fn registered_usage_probes(&self) -> Vec<(CacheDomain, super::CacheAdapter)> {
412 self.inner
413 .registry
414 .lock()
415 .expect("memory registry poisoned")
416 .entries
417 .values()
418 .map(|registration| (registration.domain, registration.adapter.clone()))
419 .collect()
420 }
421
422 fn registered_entries(&self) -> Vec<(u64, DomainRegistration)> {
423 self.inner
424 .registry
425 .lock()
426 .expect("memory registry poisoned")
427 .entries
428 .iter()
429 .map(|(id, registration)| (*id, registration.clone()))
430 .collect()
431 }
432
433 fn rebalance_budgets(&self) {
434 let entries = self.registered_entries();
435 for (_, registration) in &entries {
436 registration
437 .adapter
438 .set_budget(self.assigned_budget(registration.domain, &entries));
439 }
440 }
441
442 fn assigned_budget(&self, domain: CacheDomain, entries: &[(u64, DomainRegistration)]) -> usize {
443 let options = self.options();
444 if domain == CacheDomain::Persistent {
445 return options.budget.persistent_bytes.min(usize::MAX as u64) as usize;
446 }
447 let instances = entries
448 .iter()
449 .filter(|(_, registration)| registration.domain == domain)
450 .count()
451 .max(1);
452 options.domain_budget(domain).saturating_div(instances)
453 }
454
455 fn bump_epoch(&self) -> u64 {
456 self.inner.epoch.fetch_add(1, Ordering::AcqRel) + 1
457 }
458}
459
460#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
461struct BudgetUsage {
462 managed_bytes: usize,
463 protected_bytes: usize,
464}
465
466pub struct MemoryReservation {
467 bytes: usize,
468 accounted_bytes: usize,
469 reserved: Arc<AtomicUsize>,
470}
471
472pub struct MemoryTaskReservation {
473 _reservation: MemoryReservation,
474 in_flight: Arc<AtomicUsize>,
475}
476
477impl MemoryTaskReservation {
478 pub const fn bytes(&self) -> usize {
479 self._reservation.bytes()
480 }
481}
482
483impl Drop for MemoryTaskReservation {
484 fn drop(&mut self) {
485 self.in_flight.fetch_sub(1, Ordering::AcqRel);
486 }
487}
488
489impl MemoryReservation {
490 pub const fn bytes(&self) -> usize {
491 self.bytes
492 }
493}
494
495impl Drop for MemoryReservation {
496 fn drop(&mut self) {
497 self.reserved
498 .fetch_sub(self.accounted_bytes, Ordering::AcqRel);
499 }
500}