1use crate::{Error, Result};
11use std::{
12 any::Any,
13 collections::HashMap,
14 ops::Deref,
15 sync::{
16 Arc, Condvar, Mutex, Weak,
17 atomic::{AtomicUsize, Ordering},
18 },
19};
20
21type Value = dyn Any + Send + Sync;
22struct Resource {
23 value: Arc<Value>,
24 bytes: usize,
25 idle: Box<dyn Fn(&Value) -> bool + Send + Sync>,
26 pins: AtomicUsize,
27 _reservation: MemoryReservation,
29}
30type Outcome = std::result::Result<Arc<Resource>, Arc<Error>>;
31struct Flight {
32 result: Mutex<Option<Outcome>>,
33 changed: Condvar,
34}
35enum Entry {
36 Loading(Arc<Flight>),
37 Ready { resource: Arc<Resource>, used: u64 },
38}
39struct State {
40 entries: HashMap<String, Entry>,
41 clock: u64,
42 evictions: u64,
43}
44struct Inner {
45 usage: Arc<Usage>,
46 state: Mutex<State>,
47}
48
49struct Usage {
50 limit: usize,
51 bytes: AtomicUsize,
52}
53
54#[derive(Clone)]
59pub struct MemoryBudget {
60 usage: Arc<Usage>,
61 registry: Weak<Inner>,
62}
63impl std::fmt::Debug for MemoryBudget {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 f.debug_struct("MemoryBudget")
66 .field("limit", &self.usage.limit)
67 .field("reserved_bytes", &self.reserved_bytes())
68 .finish()
69 }
70}
71
72pub struct MemoryReservation {
76 usage: Arc<Usage>,
77 bytes: usize,
78}
79impl MemoryReservation {
80 pub fn bytes(&self) -> usize {
82 self.bytes
83 }
84
85 fn shrink(&mut self, bytes: usize) {
86 assert!(bytes <= self.bytes);
87 self.usage
88 .bytes
89 .fetch_sub(self.bytes - bytes, Ordering::AcqRel);
90 self.bytes = bytes;
91 }
92}
93impl Drop for MemoryReservation {
94 fn drop(&mut self) {
95 self.usage.bytes.fetch_sub(self.bytes, Ordering::AcqRel);
96 }
97}
98impl Usage {
99 #[allow(deprecated, reason = "fetch_update supports the Rust 1.91 MSRV")]
100 fn reserve(self: &Arc<Self>, bytes: usize) -> Option<MemoryReservation> {
101 self.bytes
102 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |used| {
103 used.checked_add(bytes).filter(|&total| total <= self.limit)
104 })
105 .ok()
106 .map(|_| MemoryReservation {
107 usage: self.clone(),
108 bytes,
109 })
110 }
111}
112impl State {
113 fn evict_idle(&mut self) -> Option<Arc<Resource>> {
114 let victim = self
115 .entries
116 .iter()
117 .filter_map(|(key, entry)| match entry {
118 Entry::Ready { resource, used }
119 if Arc::strong_count(resource) == 1
120 && resource.pins.load(Ordering::Acquire) == 0
121 && (resource.idle)(resource.value.as_ref()) =>
122 {
123 Some((key.clone(), *used))
124 }
125 _ => None,
126 })
127 .min_by_key(|(_, used)| *used)
128 .map(|(key, _)| key)?;
129 let Some(Entry::Ready { resource, .. }) = self.entries.remove(&victim) else {
130 unreachable!("selected ready resource");
131 };
132 self.evictions += 1;
133 Some(resource)
134 }
135}
136impl MemoryBudget {
137 pub fn manager(&self) -> Option<ResidencyManager> {
140 self.registry
141 .upgrade()
142 .map(|inner| ResidencyManager { inner })
143 }
144 pub(crate) fn contains(&self, reservation: &MemoryReservation) -> bool {
145 Arc::ptr_eq(&self.usage, &reservation.usage)
146 }
147 pub fn reserve(&self, bytes: usize) -> Result<MemoryReservation> {
151 if bytes > self.usage.limit {
152 return Err(Error::Message("allocation exceeds residency budget".into()));
153 }
154 loop {
155 if let Some(reservation) = self.usage.reserve(bytes) {
156 return Ok(reservation);
157 }
158 let victim = self.registry.upgrade().and_then(|registry| {
159 registry
160 .state
161 .lock()
162 .unwrap_or_else(|e| e.into_inner())
163 .evict_idle()
164 });
165 let Some(victim) = victim else {
166 return Err(Error::Busy("residency budget is pinned or loading".into()));
167 };
168 drop(victim);
171 }
172 }
173
174 pub fn reserved_bytes(&self) -> usize {
176 self.usage.bytes.load(Ordering::Acquire)
177 }
178}
179
180#[derive(Clone)]
182pub struct ResidencyManager {
183 inner: Arc<Inner>,
184}
185
186#[derive(Clone, Copy, Debug, serde::Serialize)]
188pub struct ResidencyStatistics {
189 pub budget_bytes: usize,
191 pub reserved_bytes: usize,
193 pub resources: usize,
195 pub evictions: u64,
197}
198
199pub struct ModelLease<T> {
201 value: Arc<T>,
202 resource: Arc<Resource>,
203}
204impl<T> Clone for ModelLease<T> {
205 fn clone(&self) -> Self {
206 Self {
207 resource: self.resource.clone(),
208 value: self.value.clone(),
209 }
210 }
211}
212impl<T> Deref for ModelLease<T> {
213 type Target = T;
214 fn deref(&self) -> &T {
215 &self.value
216 }
217}
218impl<T> ModelLease<T> {
219 pub fn bytes(&self) -> usize {
222 self.resource.bytes
223 }
224 pub fn pin(&self) -> ResidencyPin {
226 self.resource.pins.fetch_add(1, Ordering::AcqRel);
227 ResidencyPin {
228 resource: self.resource.clone(),
229 }
230 }
231 pub fn keepalive(&self) -> Arc<dyn Send + Sync> {
233 self.resource.clone()
234 }
235}
236pub struct ResidencyPin {
238 resource: Arc<Resource>,
239}
240impl Drop for ResidencyPin {
241 fn drop(&mut self) {
242 self.resource.pins.fetch_sub(1, Ordering::AcqRel);
243 }
244}
245
246impl ResidencyManager {
247 pub fn new(budget_bytes: usize) -> Result<Self> {
249 if budget_bytes == 0 {
250 return Err(Error::Message("residency budget must be nonzero".into()));
251 }
252 Ok(Self {
253 inner: Arc::new(Inner {
254 usage: Arc::new(Usage {
255 limit: budget_bytes,
256 bytes: AtomicUsize::new(0),
257 }),
258 state: Mutex::new(State {
259 entries: HashMap::new(),
260 clock: 0,
261 evictions: 0,
262 }),
263 }),
264 })
265 }
266
267 pub fn budget(&self) -> MemoryBudget {
270 MemoryBudget {
271 usage: self.inner.usage.clone(),
272 registry: Arc::downgrade(&self.inner),
273 }
274 }
275
276 pub fn load<T: Any + Send + Sync>(
286 &self,
287 key: impl Into<String>,
288 peak_bytes: usize,
289 idle: fn(&T) -> bool,
290 loader: impl FnOnce() -> Result<(T, usize)>,
291 ) -> Result<ModelLease<T>> {
292 if peak_bytes == 0 || peak_bytes > self.inner.usage.limit {
293 return Err(Error::Message(
294 "loader reservation exceeds residency budget or is zero".into(),
295 ));
296 }
297 self.load_inner(key.into(), peak_bytes, false, idle, loader)
298 }
299
300 pub fn load_budgeted<T: Any + Send + Sync>(
308 &self,
309 key: impl Into<String>,
310 idle: fn(&T) -> bool,
311 loader: impl FnOnce(MemoryBudget) -> Result<T>,
312 ) -> Result<ModelLease<T>> {
313 self.load_inner(key.into(), 0, true, idle, || {
314 loader(self.budget()).map(|v| (v, 0))
315 })
316 }
317
318 pub fn evict(&self, key: &str) -> Result<bool> {
321 let victim = {
322 let mut state = self.inner.state.lock().unwrap_or_else(|e| e.into_inner());
323 match state.entries.get(key) {
324 None => return Ok(false),
325 Some(Entry::Ready { resource, .. })
326 if Arc::strong_count(resource) == 1
327 && resource.pins.load(Ordering::Acquire) == 0
328 && (resource.idle)(resource.value.as_ref()) => {}
329 _ => return Err(Error::Busy("resident resource is in use".into())),
330 }
331 state.evictions += 1;
332 state.entries.remove(key)
333 };
334 drop(victim);
335 Ok(true)
336 }
337
338 fn load_inner<T: Any + Send + Sync>(
339 &self,
340 key: String,
341 peak_bytes: usize,
342 budgeted: bool,
343 idle: fn(&T) -> bool,
344 loader: impl FnOnce() -> Result<(T, usize)>,
345 ) -> Result<ModelLease<T>> {
346 let (flight, reservation) = loop {
347 let mut state = self.inner.state.lock().unwrap_or_else(|e| e.into_inner());
348 state.clock = state.clock.saturating_add(1);
349 let used = state.clock;
350 if let Some(entry) = state.entries.get_mut(&key) {
351 match entry {
352 Entry::Ready {
353 resource,
354 used: last,
355 } => {
356 *last = used;
357 return lease(resource.clone());
358 }
359 Entry::Loading(flight) => break (flight.clone(), None),
360 }
361 } else {
362 let Some(reservation) = self.inner.usage.reserve(peak_bytes) else {
363 let Some(victim) = state.evict_idle() else {
364 return Err(Error::Busy("residency budget is pinned or loading".into()));
365 };
366 drop(state);
367 drop(victim);
368 continue;
369 };
370 let flight = Arc::new(Flight {
371 result: Mutex::new(None),
372 changed: Condvar::new(),
373 });
374 state
375 .entries
376 .insert(key.clone(), Entry::Loading(flight.clone()));
377 break (flight, Some(reservation));
378 }
379 };
380 let resource = if let Some(mut reservation) = reservation {
381 let result = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(loader)) {
382 Ok(Ok((value, bytes))) if (budgeted || bytes > 0) && bytes <= peak_bytes => {
383 reservation.shrink(bytes);
384 Ok(Arc::new(Resource {
385 value: Arc::new(value),
386 bytes,
387 pins: AtomicUsize::new(0),
388 _reservation: reservation,
389 idle: Box::new(move |value| {
390 idle(value.downcast_ref::<T>().expect("registered resource type"))
391 }),
392 }))
393 }
394 other => {
395 let error = match other {
396 Ok(Ok(_)) => Error::Message(
397 "resident bytes exceed loader reservation or are zero".into(),
398 ),
399 Ok(Err(error)) => error,
400 Err(_) => Error::Message("resident model loader panicked".into()),
401 };
402 drop(reservation);
403 Err(Arc::new(error))
404 }
405 };
406 {
407 let mut state = self.inner.state.lock().unwrap_or_else(|e| e.into_inner());
408 match &result {
409 Ok(resource) => {
410 let used = state.clock;
411 state.entries.insert(
412 key,
413 Entry::Ready {
414 resource: resource.clone(),
415 used,
416 },
417 );
418 }
419 Err(_) => {
420 state.entries.remove(&key);
421 }
422 }
423 *flight.result.lock().unwrap_or_else(|e| e.into_inner()) = Some(result.clone());
424 }
425 flight.changed.notify_all();
426 result.map_err(|source| Error::Execution { source })?
427 } else {
428 let mut result = flight.result.lock().unwrap_or_else(|e| e.into_inner());
429 while result.is_none() {
430 result = flight
431 .changed
432 .wait(result)
433 .unwrap_or_else(|e| e.into_inner());
434 }
435 result
436 .as_ref()
437 .unwrap()
438 .clone()
439 .map_err(|source| Error::Execution { source })?
440 };
441 lease(resource)
442 }
443
444 pub fn statistics(&self) -> ResidencyStatistics {
446 let state = self.inner.state.lock().unwrap_or_else(|e| e.into_inner());
447 ResidencyStatistics {
448 budget_bytes: self.inner.usage.limit,
449 reserved_bytes: self.inner.usage.bytes.load(Ordering::Acquire),
450 resources: state.entries.len(),
451 evictions: state.evictions,
452 }
453 }
454}
455fn lease<T: Any + Send + Sync>(resource: Arc<Resource>) -> Result<ModelLease<T>> {
456 let value = resource
457 .value
458 .clone()
459 .downcast::<T>()
460 .map_err(|_| Error::Message("residency key has another resource type".into()))?;
461 Ok(ModelLease { resource, value })
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 #[test]
468 fn budgeted_units_grow_without_double_charging_and_evict_only_when_idle() {
469 let manager = ResidencyManager::new(100).unwrap();
470 let budget = manager.budget();
471 let model = manager
472 .load_budgeted(
473 "model",
474 |_: &Mutex<Vec<MemoryReservation>>| true,
475 |budget| Ok(Mutex::new(vec![budget.reserve(40)?])),
476 )
477 .unwrap();
478 assert_eq!(model.bytes(), 0);
479 model.lock().unwrap().push(budget.reserve(50).unwrap());
480 assert_eq!(budget.reserved_bytes(), 90);
481 assert!(manager.evict("model").is_err());
482 let pin = model.pin();
483 drop(model);
484 assert!(budget.reserve(11).is_err());
485 drop(pin);
486 let other = budget.reserve(100).unwrap();
487 assert_eq!(manager.statistics().resources, 0);
488 drop(other);
489 assert_eq!(budget.reserved_bytes(), 0);
490 assert!(!manager.evict("model").unwrap());
491 }
492
493 #[test]
494 fn budgeted_loader_failure_and_panic_release_allocations_and_allow_retry() {
495 let manager = ResidencyManager::new(64).unwrap();
496 for panic in [false, true] {
497 let result = manager.load_budgeted(
498 "model",
499 |_: &MemoryReservation| true,
500 |budget| {
501 let _held = budget.reserve(64)?;
502 assert!(!panic, "injected loader panic");
503 Err(Error::Message("injected loader failure".into()))
504 },
505 );
506 assert!(result.is_err());
507 assert_eq!(manager.statistics().reserved_bytes, 0);
508 assert_eq!(manager.statistics().resources, 0);
509 }
510 drop(
511 manager
512 .load_budgeted(
513 "model",
514 |_: &MemoryReservation| true,
515 |budget| budget.reserve(64),
516 )
517 .unwrap(),
518 );
519 assert!(manager.evict("model").unwrap());
520 assert_eq!(manager.statistics().reserved_bytes, 0);
521 assert!(manager.budget().manager().is_some());
522 let budget = manager.budget();
523 drop(manager);
524 assert!(budget.manager().is_none());
525 }
526 #[test]
527 fn allocations_and_loaders_share_a_ceiling_and_idle_eviction() {
528 let manager = ResidencyManager::new(100).unwrap();
529 let budget = manager.budget();
530 let scratch = budget.reserve(40).unwrap();
531 let model = manager
532 .load("model", 60, |_: &u8| true, || Ok((1, 50)))
533 .unwrap();
534 assert_eq!(budget.reserved_bytes(), 90);
535 assert!(matches!(budget.reserve(11), Err(Error::Busy(_))));
536 drop(model);
537 let staging = budget.reserve(60).unwrap();
538 assert_eq!(manager.statistics().evictions, 1);
539 assert_eq!(manager.statistics().reserved_bytes, 100);
540 assert!(
541 manager
542 .load("other", 1, |_: &u8| true, || Ok((2, 1)))
543 .is_err()
544 );
545 drop(staging);
546 drop(scratch);
547 assert_eq!(budget.reserved_bytes(), 0);
548 assert!(budget.reserve(101).is_err());
549 assert_eq!(budget.reserve(0).unwrap().bytes(), 0);
550 }
551
552 #[test]
553 fn budget_owners_do_not_cycle_and_survive_registry_teardown() {
554 let manager = ResidencyManager::new(64).unwrap();
555 let registry = Arc::downgrade(&manager.inner);
556 let budget = manager.budget();
557 let scratch = budget.reserve(16).unwrap();
558 drop(
559 manager
560 .load(
561 "owner",
562 32,
563 |_: &MemoryBudget| true,
564 || Ok((budget.clone(), 32)),
565 )
566 .unwrap(),
567 );
568 assert_eq!(budget.reserved_bytes(), 48);
569 drop(manager);
570 assert!(registry.upgrade().is_none());
571 assert_eq!(budget.reserved_bytes(), 16);
572 let tail = budget.reserve(48).unwrap();
573 assert!(budget.reserve(1).is_err());
574 drop((scratch, tail));
575 assert_eq!(budget.reserved_bytes(), 0);
576 }
577
578 #[test]
579 fn concurrent_reservations_cannot_oversubscribe() {
580 let manager = ResidencyManager::new(64).unwrap();
581 let budget = manager.budget();
582 let barrier = std::sync::Barrier::new(8);
583 std::thread::scope(|scope| {
584 for _ in 0..8 {
585 scope.spawn(|| {
586 let held = budget.reserve(8).unwrap();
587 barrier.wait();
588 assert_eq!(budget.reserved_bytes(), 64);
589 assert!(budget.reserve(1).is_err());
590 barrier.wait();
591 drop(held);
592 });
593 }
594 });
595 assert_eq!(budget.reserved_bytes(), 0);
596 }
597 #[test]
598 fn retiring_native_storage_remains_charged_until_its_destructor_finishes() {
599 struct Retiring {
600 started: std::sync::mpsc::Sender<()>,
601 release: Mutex<std::sync::mpsc::Receiver<()>>,
602 }
603 impl Drop for Retiring {
604 fn drop(&mut self) {
605 self.started.send(()).unwrap();
606 self.release.get_mut().unwrap().recv().unwrap();
607 }
608 }
609 let manager = ResidencyManager::new(64).unwrap();
610 let (started, seen) = std::sync::mpsc::channel();
611 let (release, gate) = std::sync::mpsc::channel();
612 drop(
613 manager
614 .load(
615 "old",
616 64,
617 |_: &Retiring| true,
618 || {
619 Ok((
620 Retiring {
621 started,
622 release: Mutex::new(gate),
623 },
624 64,
625 ))
626 },
627 )
628 .unwrap(),
629 );
630 std::thread::scope(|scope| {
631 let loading = scope.spawn(|| {
632 manager
633 .load("new", 64, |_: &u8| true, || Ok((1, 64)))
634 .unwrap()
635 });
636 seen.recv_timeout(std::time::Duration::from_secs(5))
637 .unwrap();
638 assert_eq!(manager.statistics().reserved_bytes, 64);
639 assert!(matches!(
640 manager.load("other", 1, |_: &u8| true, || Ok((2, 1))),
641 Err(Error::Busy(_))
642 ));
643 release.send(()).unwrap();
644 assert_eq!(*loading.join().unwrap(), 1);
645 });
646 }
647 #[test]
648 fn leases_pins_and_exported_work_prevent_eviction() {
649 let manager = ResidencyManager::new(100).unwrap();
650 let model = manager
651 .load(
652 "one",
653 70,
654 |v: &AtomicUsize| v.load(Ordering::Relaxed) == 0,
655 || Ok((AtomicUsize::new(0), 60)),
656 )
657 .unwrap();
658 let pin = model.pin();
659 drop(model);
660 assert!(matches!(
661 manager.load("two", 50, |_: &u8| true, || Ok((2, 50))),
662 Err(Error::Busy(_))
663 ));
664 drop(pin);
665 let model = manager
666 .load("one", 70, |_: &AtomicUsize| true, || unreachable!())
667 .unwrap();
668 model.store(1, Ordering::Relaxed);
669 drop(model);
670 assert!(matches!(
671 manager.load("two", 50, |_: &u8| true, || Ok((2, 50))),
672 Err(Error::Busy(_))
673 ));
674 manager
675 .load("one", 70, |_: &AtomicUsize| true, || unreachable!())
676 .unwrap()
677 .store(0, Ordering::Relaxed);
678 let second = manager
679 .load("two", 50, |_: &u8| true, || Ok((2, 50)))
680 .unwrap();
681 assert_eq!(*second, 2);
682 assert_eq!(manager.statistics().evictions, 1);
683 assert_eq!(manager.statistics().reserved_bytes, 50);
684 }
685 #[test]
686 fn failed_loads_rollback_and_concurrent_loads_share_one_reservation() {
687 let manager = ResidencyManager::new(64).unwrap();
688 assert!(
689 manager
690 .load(
691 "a",
692 64,
693 |_: &usize| true,
694 || Err(Error::Message("injected".into()))
695 )
696 .is_err()
697 );
698 assert_eq!(manager.statistics().reserved_bytes, 0);
699 assert!(
700 manager
701 .load("a", 64, |_: &usize| true, || Ok((1, 65)))
702 .is_err()
703 );
704 let barrier = std::sync::Barrier::new(8);
705 let calls = AtomicUsize::new(0);
706 std::thread::scope(|scope| {
707 for _ in 0..8 {
708 scope.spawn(|| {
709 barrier.wait();
710 let value = manager
711 .load(
712 "a",
713 64,
714 |_: &usize| true,
715 || {
716 calls.fetch_add(1, Ordering::Relaxed);
717 Ok((9, 32))
718 },
719 )
720 .unwrap();
721 assert_eq!(*value, 9);
722 });
723 }
724 });
725 assert_eq!(calls.load(Ordering::Relaxed), 1);
726 assert_eq!(manager.statistics().reserved_bytes, 32);
727 assert!(
728 manager
729 .load("a", 64, |_: &u8| true, || unreachable!())
730 .is_err()
731 );
732 }
733}