1use std::collections::{BTreeMap, VecDeque};
14use std::sync::{Arc, Weak};
15
16use crate::{
17 DocumentFingerprint, ErrorCode, FileMakerError, ResolvedScene, ResourceLimits, Result,
18};
19
20pub struct SceneCache {
22 capacity: usize,
23 max_bytes: usize,
24 used_bytes: usize,
25 entries: BTreeMap<DocumentFingerprint, CacheEntry>,
26 insertion_order: VecDeque<DocumentFingerprint>,
27 retired: Vec<(Weak<ResolvedScene>, usize)>,
28}
29
30struct CacheEntry {
31 scene: Arc<ResolvedScene>,
32 bytes: usize,
33}
34
35impl SceneCache {
36 pub const DEFAULT_MAX_BYTES: usize = 256 * 1024 * 1024;
38
39 pub fn new(capacity: usize) -> Result<Self> {
41 Self::with_byte_capacity(capacity, Self::DEFAULT_MAX_BYTES)
42 }
43
44 pub fn with_byte_capacity(capacity: usize, max_bytes: usize) -> Result<Self> {
48 if capacity == 0 || max_bytes == 0 {
49 return Err(FileMakerError::new(
50 ErrorCode::LimitExceeded,
51 "scene cache entry and byte bounds must be non-zero",
52 ));
53 }
54 Ok(Self {
55 capacity,
56 max_bytes,
57 used_bytes: 0,
58 entries: BTreeMap::new(),
59 insertion_order: VecDeque::new(),
60 retired: Vec::new(),
61 })
62 }
63
64 #[must_use]
66 pub fn get(&self, key: &DocumentFingerprint) -> Option<Arc<ResolvedScene>> {
67 self.entries.get(key).map(|entry| entry.scene.clone())
68 }
69
70 pub fn get_or_try_insert_with<F>(
74 &mut self,
75 key: DocumentFingerprint,
76 limits: &ResourceLimits,
77 resolve: F,
78 ) -> Result<Arc<ResolvedScene>>
79 where
80 F: FnOnce() -> Result<ResolvedScene>,
81 {
82 if let Some(scene) = self.get(&key) {
83 return Ok(scene);
84 }
85 self.check_miss_admission()?;
86 self.insert(key, resolve()?, limits)
87 }
88
89 fn check_miss_admission(&mut self) -> Result<()> {
90 self.retired.retain(|(scene, _)| scene.strong_count() != 0);
91 let (count, bytes) = self
92 .entries
93 .values()
94 .filter(|entry| Arc::strong_count(&entry.scene) > 1)
95 .fold(
96 (self.retired.len(), self.retired_bytes()),
97 |(count, bytes), entry| {
98 (count.saturating_add(1), bytes.saturating_add(entry.bytes))
99 },
100 );
101 if count >= self.capacity || bytes >= self.max_bytes {
102 return Err(FileMakerError::new(
103 ErrorCode::LimitExceeded,
104 "consumer-held scenes leave no capacity for resolving a cache miss",
105 ));
106 }
107 Ok(())
108 }
109
110 pub fn insert(
115 &mut self,
116 key: DocumentFingerprint,
117 scene: ResolvedScene,
118 limits: &ResourceLimits,
119 ) -> Result<Arc<ResolvedScene>> {
120 if scene.engine_version != crate::ENGINE_VERSION {
121 return Err(cache_error(
122 "cached scene engine version does not match the active engine",
123 ));
124 }
125 let elements = scene
126 .pages
127 .iter()
128 .try_fold(0_usize, |total, page| {
129 total.checked_add(page.elements.len())
130 })
131 .ok_or_else(|| cache_error("cached scene element count overflow"))?;
132 if scene.pages.len() > limits.max_pages || elements > limits.max_elements {
133 return Err(FileMakerError::new(
134 ErrorCode::LimitExceeded,
135 "cached scene exceeds configured page or element budget",
136 ));
137 }
138 if let Some(existing) = self.entries.get(&key) {
139 return Ok(existing.scene.clone());
140 }
141 let bytes = crate::memory::serialized_size_bounded(&scene, self.max_bytes)?;
142 self.retired.retain(|(scene, _)| scene.strong_count() != 0);
143 let mut retired_bytes = self.retired_bytes();
144 while self.entries.len().saturating_add(self.retired.len()) >= self.capacity
145 || self
146 .used_bytes
147 .saturating_add(retired_bytes)
148 .saturating_add(bytes)
149 > self.max_bytes
150 {
151 let Some(oldest) = self.insertion_order.pop_front() else {
152 return Err(FileMakerError::new(
153 ErrorCode::LimitExceeded,
154 "consumer-held scenes prevent cache admission",
155 ));
156 };
157 if let Some(entry) = self.entries.remove(&oldest) {
158 self.used_bytes = self.used_bytes.saturating_sub(entry.bytes);
159 let retired = (Arc::downgrade(&entry.scene), entry.bytes);
160 drop(entry);
161 if retired.0.strong_count() != 0 {
164 retired_bytes = retired_bytes.saturating_add(retired.1);
165 self.retired.push(retired);
166 }
167 }
168 }
169 let scene = Arc::new(scene);
170 self.entries.insert(
171 key,
172 CacheEntry {
173 scene: scene.clone(),
174 bytes,
175 },
176 );
177 self.used_bytes += bytes;
178 self.insertion_order.push_back(key);
179 Ok(scene)
180 }
181
182 #[must_use]
184 pub fn len(&self) -> usize {
185 self.entries.len()
186 }
187
188 #[must_use]
190 pub fn is_empty(&self) -> bool {
191 self.entries.is_empty()
192 }
193
194 #[must_use]
196 pub const fn used_bytes(&self) -> usize {
197 self.used_bytes
198 }
199
200 #[must_use]
203 pub fn retired_bytes(&self) -> usize {
204 self.retired
205 .iter()
206 .filter(|(scene, _)| scene.strong_count() != 0)
207 .fold(0_usize, |sum, (_, bytes)| sum.saturating_add(*bytes))
208 }
209
210 #[must_use]
212 pub const fn max_bytes(&self) -> usize {
213 self.max_bytes
214 }
215}
216
217fn cache_error(message: impl Into<String>) -> FileMakerError {
218 FileMakerError::new(ErrorCode::Validation, message)
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 fn fingerprint(byte: u8) -> DocumentFingerprint {
226 let mut builder = crate::FingerprintBuilder::new();
227 builder.field("test", &[byte]).unwrap();
228 builder.finish()
229 }
230
231 #[test]
232 fn evicts_oldest_without_unbounded_growth() {
233 let mut cache = SceneCache::new(1).unwrap();
234 let scene = ResolvedScene {
235 template_id: "cache".to_owned(),
236 pages: Vec::new(),
237 engine_version: crate::ENGINE_VERSION.to_owned(),
238 };
239 cache
240 .insert(fingerprint(1), scene.clone(), &ResourceLimits::default())
241 .unwrap();
242 cache
243 .insert(fingerprint(2), scene, &ResourceLimits::default())
244 .unwrap();
245 assert!(cache.get(&fingerprint(1)).is_none());
246 assert_eq!(cache.len(), 1);
247 }
248
249 #[test]
250 fn resolves_only_once_for_a_repeated_fingerprint() {
251 let mut cache = SceneCache::new(1).unwrap();
252 let key = fingerprint(1);
253 let mut calls = 0;
254 for _ in 0..2 {
255 cache
256 .get_or_try_insert_with(key, &ResourceLimits::default(), || {
257 calls += 1;
258 Ok(ResolvedScene {
259 template_id: "cache".to_owned(),
260 pages: Vec::new(),
261 engine_version: crate::ENGINE_VERSION.to_owned(),
262 })
263 })
264 .unwrap();
265 }
266 assert_eq!(calls, 1);
267 }
268
269 #[test]
270 fn rejects_a_scene_from_another_engine_version() {
271 let mut cache = SceneCache::new(1).unwrap();
272 let error = cache
273 .insert(
274 fingerprint(1),
275 ResolvedScene {
276 template_id: "stale".to_owned(),
277 pages: Vec::new(),
278 engine_version: "other".to_owned(),
279 },
280 &ResourceLimits::default(),
281 )
282 .unwrap_err();
283 assert_eq!(error.code(), ErrorCode::Validation);
284 assert!(cache.is_empty());
285 }
286
287 #[test]
288 fn rejects_one_scene_larger_than_the_byte_budget() {
289 let mut cache = SceneCache::with_byte_capacity(2, 1).unwrap();
290 let error = cache
291 .insert(
292 fingerprint(1),
293 ResolvedScene {
294 template_id: "oversized".to_owned(),
295 pages: Vec::new(),
296 engine_version: crate::ENGINE_VERSION.to_owned(),
297 },
298 &ResourceLimits::default(),
299 )
300 .unwrap_err();
301 assert_eq!(error.code(), ErrorCode::LimitExceeded);
302 assert_eq!(cache.used_bytes(), 0);
303 }
304
305 #[test]
306 fn byte_budget_evicts_even_below_the_entry_capacity() {
307 let scene = ResolvedScene {
308 template_id: "bounded".to_owned(),
309 pages: Vec::new(),
310 engine_version: crate::ENGINE_VERSION.to_owned(),
311 };
312 let bytes = crate::memory::serialized_size(&scene).unwrap();
313 let mut cache = SceneCache::with_byte_capacity(4, bytes).unwrap();
314 cache
315 .insert(fingerprint(1), scene.clone(), &ResourceLimits::default())
316 .unwrap();
317 cache
318 .insert(fingerprint(2), scene, &ResourceLimits::default())
319 .unwrap();
320 assert!(cache.get(&fingerprint(1)).is_none());
321 assert_eq!(cache.len(), 1);
322 assert!(cache.used_bytes() <= cache.max_bytes());
323 }
324
325 #[test]
326 fn consumer_lease_blocks_admission_until_released() {
327 let scene = ResolvedScene {
328 template_id: "leased".to_owned(),
329 pages: Vec::new(),
330 engine_version: crate::ENGINE_VERSION.to_owned(),
331 };
332 let bytes = crate::memory::serialized_size(&scene).unwrap();
333 for (capacity, max_bytes) in [(1, 2 * bytes), (4, bytes)] {
334 let mut cache = SceneCache::with_byte_capacity(capacity, max_bytes).unwrap();
335 let held = cache
336 .insert(fingerprint(1), scene.clone(), &ResourceLimits::default())
337 .unwrap();
338 let weak = Arc::downgrade(&held);
339 let error = cache
340 .insert(fingerprint(2), scene.clone(), &ResourceLimits::default())
341 .unwrap_err();
342 assert_eq!(error.code(), ErrorCode::LimitExceeded);
343 assert_eq!(cache.used_bytes(), 0);
344 assert_eq!(cache.retired_bytes(), bytes);
345 assert!(weak.upgrade().is_some());
346 drop(held);
347 assert!(weak.upgrade().is_none());
348 assert_eq!(cache.retired_bytes(), 0);
349 assert!(cache
350 .insert(fingerprint(3), scene.clone(), &ResourceLimits::default())
351 .is_ok());
352 assert_eq!(cache.len(), 1);
353 }
354 }
355
356 #[test]
357 fn saturated_consumer_leases_reject_miss_before_resolver_runs() {
358 let scene = ResolvedScene {
359 template_id: "leased".to_owned(),
360 pages: Vec::new(),
361 engine_version: crate::ENGINE_VERSION.to_owned(),
362 };
363 let bytes = crate::memory::serialized_size(&scene).unwrap();
364 for (capacity, max_bytes) in [(1, 2 * bytes), (4, bytes)] {
365 let mut cache = SceneCache::with_byte_capacity(capacity, max_bytes).unwrap();
366 let held = cache
367 .insert(fingerprint(1), scene.clone(), &ResourceLimits::default())
368 .unwrap();
369 let mut calls = 0;
370 let rejected =
371 cache.get_or_try_insert_with(fingerprint(2), &ResourceLimits::default(), || {
372 calls += 1;
373 Ok(scene.clone())
374 });
375 assert_eq!(rejected.unwrap_err().code(), ErrorCode::LimitExceeded);
376 assert_eq!(calls, 0);
377 assert!(cache.get(&fingerprint(1)).is_some());
378 drop(held);
379 assert!(cache
380 .get_or_try_insert_with(fingerprint(2), &ResourceLimits::default(), || {
381 calls += 1;
382 Ok(scene.clone())
383 })
384 .is_ok());
385 assert_eq!(calls, 1);
386 }
387 }
388}