asc_engine/resident.rs
1//! Hosts that keep engines open (the CLI daemon, the MCP server) count what
2//! those engines hold outside the cache — class graphs and resource tables
3//! ([`Engine::resident_bytes`]) — against the memory budget of their shared
4//! cache, closing the least recently used ones beyond a share of it.
5
6use crate::{Engine, SharedCache};
7
8/// The open engines' class graphs and resource tables take at most this
9/// fraction (1/n) of the memory budget; the cache keeps the rest for DEX.
10const RESIDENT_SHARE: usize = 2;
11
12impl SharedCache {
13 /// Of `engines`, most recently used first, the indexes of those to close
14 /// so that what the rest hold fits in half the memory budget; what stays is reserved in the budget. The most recent engine
15 /// stays whatever its size; without a memory cache every engine stays.
16 ///
17 /// Sizing waits for an engine building its class graph, so callers do
18 /// not hold their own engine registry's lock across this call.
19 pub fn fit_resident(&self, engines: &[&Engine]) -> Vec<usize> {
20 // Without a memory cache there is no budget to fit in.
21 if self.memory_capacity() == 0 {
22 return Vec::new();
23 }
24 let sizes: Vec<usize> = engines.iter().map(|e| e.resident_bytes()).collect();
25 self.fit_resident_bytes(&sizes)
26 }
27
28 /// [`SharedCache::fit_resident`] by what each open program holds
29 /// outside the cache, for a host whose programs are not all
30 /// [`Engine`]s (another format's engines share the budget).
31 pub fn fit_resident_bytes(&self, sizes: &[usize]) -> Vec<usize> {
32 if self.memory_capacity() == 0 {
33 return Vec::new();
34 }
35 let (held, closing) = within_share(sizes, self.resident_share());
36 self.set_reserved(held);
37 closing
38 }
39
40 /// The bytes open programs may hold outside the cache: half the memory budget.
41 pub fn resident_share(&self) -> usize {
42 self.memory_capacity() / RESIDENT_SHARE
43 }
44}
45
46/// Of the resident sizes, most recently used first: the bytes of those that
47/// fit in `share` together, and the indexes of the rest (to close). The
48/// first stays whatever its size.
49fn within_share(sizes: &[usize], share: usize) -> (usize, Vec<usize>) {
50 let mut held = 0;
51 let mut closing = Vec::new();
52 for (i, &bytes) in sizes.iter().enumerate() {
53 if i > 0 && bytes > 0 && held + bytes > share {
54 closing.push(i);
55 } else {
56 held += bytes;
57 }
58 }
59 (held, closing)
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn beyond_the_share_least_recent_close_first() {
68 assert_eq!(within_share(&[], 10), (0, vec![]));
69 assert_eq!(within_share(&[50], 10), (50, vec![]), "the engine in use stays");
70 assert_eq!(within_share(&[4, 5, 3, 1], 10), (10, vec![2]), "smaller older ones still fit");
71 assert_eq!(within_share(&[4, 0, 0], 0), (4, vec![]), "nothing resident, nothing closed");
72 }
73}