Skip to main content

motion_core/authoring/context/
cache.rs

1//! Shared LRU storage for pure authoring computations with optional byte accounting.
2
3use crate::sync::MutexExt;
4use std::any::{Any, TypeId};
5use std::collections::{VecDeque, hash_map::DefaultHasher};
6use std::hash::{Hash, Hasher};
7use std::panic::Location;
8use std::sync::{Arc, Mutex};
9
10use super::CacheLimits;
11
12/// Shared LRU compute cache for pure authoring computations, with optional byte accounting.
13///
14/// Entries can be dropped and recomputed at any time; decoded resources that must keep their
15/// identity live in a [`crate::ResourceStore`] instead.
16///
17/// Values must depend only on their keys and immutable resources. Resource
18/// revisions belong in the key. The default retains 256 entries without a byte cap.
19/// with_limits adds a caller-declared logical byte allowance for cache_with_cost.
20/// Ordinary cache() is entry-bounded only and reports no retained-byte charge.
21/// Outstanding Arc pins and allocator overhead are not bounded.
22/// Concurrent misses may compute more than once; builders and cost callbacks run
23/// outside the lock. Zero entries disable retention; zero bytes disable costed values.
24/// Original keys are retained and compared by type and equality after hashing.
25pub struct SceneCache {
26    capacity: usize,
27    max_bytes: usize,
28    pub(super) entries: Mutex<VecDeque<CacheEntry>>,
29}
30
31impl std::fmt::Debug for SceneCache {
32    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        formatter
34            .debug_struct("SceneCache")
35            .field("capacity", &self.capacity)
36            .field("max_bytes", &self.max_bytes)
37            .finish_non_exhaustive()
38    }
39}
40
41pub(super) struct CacheEntry {
42    key: CacheKey,
43    input: Box<dyn Any + Send + Sync>,
44    value: Arc<dyn Any + Send + Sync>,
45    bytes: usize,
46}
47
48#[derive(PartialEq, Eq)]
49struct CacheKey {
50    location: &'static Location<'static>,
51    value_type: TypeId,
52    hash: u64,
53}
54
55impl CacheEntry {
56    fn matches<Key: Eq + 'static>(&self, key: &CacheKey, input: &Key) -> bool {
57        self.key == *key && self.input.downcast_ref::<Key>() == Some(input)
58    }
59}
60
61impl Default for SceneCache {
62    fn default() -> Self {
63        Self::new(256)
64    }
65}
66
67impl SceneCache {
68    /// Creates a cache with at most this many retained entries; zero disables it.
69    pub fn new(capacity: usize) -> Self {
70        Self::with_limits(CacheLimits {
71            max_entries: capacity,
72            max_bytes: usize::MAX,
73        })
74    }
75
76    /// Bounds entries and caller-declared retained payload bytes.
77    /// Heap-owning keys and values need SceneContext::cache_with_cost for payload charges.
78    /// Neither allocator overhead nor externally pinned Arc values are bounded.
79    pub fn with_limits(limits: CacheLimits) -> Self {
80        Self {
81            capacity: limits.max_entries,
82            max_bytes: limits.max_bytes,
83            entries: Mutex::new(VecDeque::new()),
84        }
85    }
86
87    /// Logical payload allowance; usize::MAX means no configured byte bound.
88    pub fn max_bytes(&self) -> usize {
89        self.max_bytes
90    }
91
92    /// Sum of the declared charges for currently retained entries.
93    pub fn retained_bytes(&self) -> usize {
94        self.entries
95            .lock_unpoisoned()
96            .iter()
97            .map(|entry| entry.bytes)
98            .sum()
99    }
100
101    /// Maximum number of retained entries, not a byte budget.
102    pub fn capacity(&self) -> usize {
103        self.capacity
104    }
105
106    /// Number of currently retained entries.
107    pub fn len(&self) -> usize {
108        self.entries.lock_unpoisoned().len()
109    }
110
111    /// Whether no computed values are currently retained.
112    pub fn is_empty(&self) -> bool {
113        self.len() == 0
114    }
115
116    /// Removes retained entries without invalidating existing returned handles.
117    pub fn clear(&self) {
118        let entries = std::mem::take(&mut *self.entries.lock_unpoisoned());
119        drop(entries);
120    }
121
122    #[track_caller]
123    pub(super) fn get<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static>(
124        &self,
125        key: Key,
126        build: impl FnOnce() -> Value,
127    ) -> Arc<Value> {
128        match self.get_cached(
129            key,
130            || Ok::<_, std::convert::Infallible>(build()),
131            |_| 0,
132            false,
133            Location::caller(),
134        ) {
135            Ok(value) => value,
136            Err(never) => match never {},
137        }
138    }
139
140    /// Like [`get`](Self::get) for a fallible builder; failures are returned and never cached.
141    #[track_caller]
142    pub(super) fn try_get<
143        Key: Eq + Hash + Send + Sync + 'static,
144        Value: Send + Sync + 'static,
145        Error,
146    >(
147        &self,
148        key: Key,
149        build: impl FnOnce() -> Result<Value, Error>,
150    ) -> Result<Arc<Value>, Error> {
151        self.get_cached(key, build, |_| 0, false, Location::caller())
152    }
153
154    /// Gets a pure computed value with a caller-declared retained payload charge.
155    #[track_caller]
156    pub fn get_with_cost<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static>(
157        &self,
158        key: Key,
159        build: impl FnOnce() -> Value,
160        cost: impl FnOnce(&Value) -> usize,
161    ) -> Arc<Value> {
162        match self.get_cached(
163            key,
164            || Ok::<_, std::convert::Infallible>(build()),
165            cost,
166            true,
167            Location::caller(),
168        ) {
169            Ok(value) => value,
170            Err(never) => match never {},
171        }
172    }
173
174    fn content_location() -> &'static Location<'static> {
175        Location::caller()
176    }
177
178    /// Looks up a renderer content key independently of the caller's source location.
179    pub fn lookup<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static>(
180        &self,
181        key: &Key,
182    ) -> Option<Arc<Value>> {
183        let mut hasher = DefaultHasher::new();
184        key.hash(&mut hasher);
185        self.find(
186            &CacheKey {
187                location: Self::content_location(),
188                value_type: TypeId::of::<Value>(),
189                hash: hasher.finish(),
190            },
191            key,
192        )
193    }
194
195    /// Inserts checked pure content, converging concurrent inserts to the retained value.
196    pub fn insert_with_cost<
197        Key: Eq + Hash + Send + Sync + 'static,
198        Value: Send + Sync + 'static,
199    >(
200        &self,
201        key: Key,
202        value: Value,
203        cost: impl FnOnce(&Value) -> usize,
204    ) -> Arc<Value> {
205        match self.get_cached(
206            key,
207            || Ok::<_, std::convert::Infallible>(value),
208            cost,
209            true,
210            Self::content_location(),
211        ) {
212            Ok(value) => value,
213            Err(never) => match never {},
214        }
215    }
216
217    #[track_caller]
218    fn get_cached<Key: Eq + Hash + Send + Sync + 'static, Value: Send + Sync + 'static, Error>(
219        &self,
220        key: Key,
221        build: impl FnOnce() -> Result<Value, Error>,
222        cost: impl FnOnce(&Value) -> usize,
223        accounted: bool,
224        location: &'static Location<'static>,
225    ) -> Result<Arc<Value>, Error> {
226        if self.capacity == 0 || (accounted && self.max_bytes == 0) {
227            return build().map(Arc::new);
228        }
229        let mut hasher = DefaultHasher::new();
230        key.hash(&mut hasher);
231        let cache_key = CacheKey {
232            location,
233            value_type: TypeId::of::<Value>(),
234            hash: hasher.finish(),
235        };
236        if let Some(value) = self.find(&cache_key, &key) {
237            return Ok(value);
238        }
239        let value = Arc::new(build()?);
240        let bytes = if accounted {
241            cost(value.as_ref())
242                .max(std::mem::size_of::<Value>().saturating_add(std::mem::size_of::<Key>()))
243        } else {
244            0
245        };
246        if bytes > self.max_bytes {
247            return Ok(value);
248        }
249        let mut entries = self.entries.lock_unpoisoned();
250        if let Some(index) = entries
251            .iter()
252            .position(|entry| entry.matches(&cache_key, &key))
253        {
254            let entry = entries.remove(index).expect("located cache entry");
255            let stored = entry.value.clone();
256            entries.push_back(entry);
257            drop(entries);
258            return Ok(stored
259                .downcast()
260                .unwrap_or_else(|_| unreachable!("the cache key encodes the value type")));
261        }
262        let mut retained: usize = entries.iter().map(|entry| entry.bytes).sum();
263        let mut evicted = Vec::new();
264        while entries.len() >= self.capacity || bytes > self.max_bytes.saturating_sub(retained) {
265            let Some(entry) = entries.pop_front() else {
266                break;
267            };
268            retained -= entry.bytes;
269            evicted.push(entry);
270        }
271        entries.push_back(CacheEntry {
272            key: cache_key,
273            input: Box::new(key),
274            value: value.clone(),
275            bytes,
276        });
277        drop(entries);
278        drop(evicted);
279        Ok(value)
280    }
281
282    fn find<Key: Eq + 'static, Value: Send + Sync + 'static>(
283        &self,
284        key: &CacheKey,
285        input: &Key,
286    ) -> Option<Arc<Value>> {
287        let mut entries = self.entries.lock_unpoisoned();
288        let index = entries.iter().position(|entry| entry.matches(key, input))?;
289        let entry = entries.remove(index).expect("located cache entry");
290        let value = entry.value.clone();
291        entries.push_back(entry);
292        drop(entries);
293        Some(
294            value
295                .downcast()
296                .unwrap_or_else(|_| unreachable!("the cache key encodes the value type")),
297        )
298    }
299}