Skip to main content

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}