1use lru::LruCache;
2use std::borrow::Borrow;
3use std::hash::Hash;
4use std::num::NonZeroUsize;
5use std::sync::{Arc, Mutex};
6
7use crate::core::types::{CpuTile, DatasetId};
8
9pub(crate) const DEFAULT_TILE_CACHE_SIZE: u64 = 64 * 1024 * 1024;
18const TILE_CACHE_BYTES_ENV: &str = "WSI_RS_TILE_CACHE_BYTES";
19pub(crate) const DEFAULT_DISPLAY_TILE_CACHE_SIZE: u64 = 32 * 1024 * 1024;
26const DISPLAY_TILE_CACHE_BYTES_ENV: &str = "WSI_RS_DISPLAY_TILE_CACHE_BYTES";
27const PRIVATE_CACHE_ENTRY_ACCOUNTING_FLOOR_BYTES: u64 = 256;
31const PRIVATE_CACHE_BUDGET_DIVISOR: u64 = 4;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37#[non_exhaustive]
38pub struct CacheConfig {
39 pub shared_tile_bytes: Option<u64>,
40 pub display_tile_bytes: Option<u64>,
41}
42
43impl CacheConfig {
44 pub const fn deterministic() -> Self {
45 Self {
46 shared_tile_bytes: None,
47 display_tile_bytes: None,
48 }
49 }
50
51 pub const fn with_shared_tile_bytes(mut self, bytes: u64) -> Self {
52 self.shared_tile_bytes = Some(bytes);
53 self
54 }
55
56 pub const fn with_display_tile_bytes(mut self, bytes: u64) -> Self {
57 self.display_tile_bytes = Some(bytes);
58 self
59 }
60
61 pub(crate) fn shared_tile_budget(self, source_hint: Option<u64>) -> u64 {
62 self.shared_tile_bytes
63 .or(source_hint)
64 .unwrap_or(DEFAULT_TILE_CACHE_SIZE)
65 }
66
67 pub(crate) fn display_tile_budget(self) -> u64 {
68 self.display_tile_bytes
69 .unwrap_or(DEFAULT_DISPLAY_TILE_CACHE_SIZE)
70 }
71
72 pub(crate) fn private_cache_budget(self, cache_count: usize) -> PrivateCacheBudget {
73 PrivateCacheBudget {
74 remaining_bytes: self.private_cache_budget_bytes(),
75 remaining_caches: cache_count,
76 }
77 }
78
79 pub(crate) fn private_cache_budget_bytes(self) -> u64 {
80 self.shared_tile_budget(None) / PRIVATE_CACHE_BUDGET_DIVISOR
81 }
82}
83
84impl Default for CacheConfig {
85 fn default() -> Self {
86 Self::deterministic()
87 }
88}
89
90pub(crate) struct PrivateCacheBudget {
96 remaining_bytes: u64,
97 remaining_caches: usize,
98}
99
100impl PrivateCacheBudget {
101 pub(crate) fn allocate(&mut self, estimated_entry_bytes: u64) -> PrivateCacheCapacity {
102 if self.remaining_caches == 0 {
103 return PrivateCacheCapacity::default();
104 }
105
106 let cache_count = self.remaining_caches as u64;
107 self.remaining_caches -= 1;
108 let accounted_entry_bytes =
109 estimated_entry_bytes.max(PRIVATE_CACHE_ENTRY_ACCOUNTING_FLOOR_BYTES);
110 let fair_share = self.remaining_bytes / cache_count;
111 let mut entries = fair_share / accounted_entry_bytes;
112 if entries == 0 && self.remaining_bytes >= accounted_entry_bytes {
113 entries = 1;
114 }
115 entries = entries.min(usize::MAX as u64);
116 let accounted_bytes = entries
117 .checked_mul(accounted_entry_bytes)
118 .unwrap_or(self.remaining_bytes)
119 .min(self.remaining_bytes);
120 self.remaining_bytes -= accounted_bytes;
121
122 PrivateCacheCapacity {
123 entries: entries as usize,
124 accounted_bytes,
125 }
126 }
127}
128
129#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
130pub(crate) struct PrivateCacheCapacity {
131 entries: usize,
132 accounted_bytes: u64,
133}
134
135#[derive(Debug)]
141pub(crate) struct PrivateCache<K: Hash + Eq, V> {
142 lru: Option<LruCache<K, V>>,
143 capacity: PrivateCacheCapacity,
144}
145
146impl<K: Hash + Eq, V> PrivateCache<K, V> {
147 pub(crate) fn new(capacity: PrivateCacheCapacity) -> Self {
148 let lru = NonZeroUsize::new(capacity.entries).map(LruCache::new);
149 Self { lru, capacity }
150 }
151
152 pub(crate) fn get<'a, Q>(&'a mut self, key: &Q) -> Option<&'a V>
153 where
154 K: Borrow<Q>,
155 Q: Hash + Eq + ?Sized,
156 {
157 self.lru.as_mut()?.get(key)
158 }
159
160 pub(crate) fn put(&mut self, key: K, value: V) {
161 if let Some(lru) = &mut self.lru {
162 lru.put(key, value);
163 }
164 }
165
166 pub(crate) fn capacity_entries(&self) -> usize {
167 self.capacity.entries
168 }
169
170 #[cfg(test)]
171 pub(crate) fn accounted_capacity_bytes(&self) -> u64 {
172 self.capacity.accounted_bytes
173 }
174
175 #[cfg(test)]
176 pub(crate) fn len(&self) -> usize {
177 self.lru.as_ref().map_or(0, LruCache::len)
178 }
179}
180
181#[derive(Hash, Eq, PartialEq, Clone, Debug)]
182pub struct CacheKey {
187 pub(crate) dataset_id: DatasetId,
188 pub(crate) scene: u32,
189 pub(crate) series: u32,
190 pub(crate) level: u32,
191 pub(crate) z: u32,
192 pub(crate) c: u32,
193 pub(crate) t: u32,
194 pub(crate) tile_col: i64,
195 pub(crate) tile_row: i64,
196}
197
198pub struct TileCache {
200 inner: Mutex<TileCacheState>,
201}
202
203#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
205pub struct TileCacheStats {
206 pub hits: u64,
208 pub misses: u64,
210 pub puts: u64,
212 pub evictions: u64,
214 pub rejected_oversize: u64,
216 pub capacity_bytes: u64,
218 pub current_bytes: u64,
220 pub entries: usize,
222}
223
224impl std::fmt::Debug for TileCache {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 let state = self.inner.lock().unwrap_or_else(|e| e.into_inner());
227 f.debug_struct("TileCache")
228 .field("capacity_bytes", &state.capacity_bytes)
229 .field("current_bytes", &state.current_bytes)
230 .field("entries", &state.lru.len())
231 .field("hits", &state.hits)
232 .field("misses", &state.misses)
233 .finish()
234 }
235}
236
237struct TileCacheState {
238 lru: LruCache<CacheKey, CachedTile>,
239 capacity_bytes: u64,
240 current_bytes: u64,
241 hits: u64,
242 misses: u64,
243 puts: u64,
244 evictions: u64,
245 rejected_oversize: u64,
246}
247
248struct CachedTile {
249 data: Arc<CpuTile>,
250 byte_size: u64,
251}
252
253impl TileCache {
254 pub fn new(capacity_bytes: u64) -> Self {
256 Self {
257 inner: Mutex::new(TileCacheState {
258 lru: LruCache::unbounded(),
261 capacity_bytes,
262 current_bytes: 0,
263 hits: 0,
264 misses: 0,
265 puts: 0,
266 evictions: 0,
267 rejected_oversize: 0,
268 }),
269 }
270 }
271
272 pub(crate) fn put(&self, key: CacheKey, data: Arc<CpuTile>) {
273 let byte_size = data.data.byte_size() as u64;
274 let mut state = self.inner.lock().unwrap_or_else(|e| e.into_inner());
275
276 if byte_size > state.capacity_bytes {
277 state.rejected_oversize += 1;
278 return;
279 }
280
281 if let Some((_, existing)) = state.lru.pop_entry(&key) {
283 state.current_bytes -= existing.byte_size;
284 }
285
286 while state.current_bytes + byte_size > state.capacity_bytes {
288 if let Some((_, evicted)) = state.lru.pop_lru() {
289 state.current_bytes -= evicted.byte_size;
290 state.evictions += 1;
291 } else {
292 break;
293 }
294 }
295
296 state.lru.put(key, CachedTile { data, byte_size });
297 state.current_bytes += byte_size;
298 state.puts += 1;
299 }
300
301 pub(crate) fn get(&self, key: &CacheKey) -> Option<Arc<CpuTile>> {
302 let mut state = self.inner.lock().unwrap_or_else(|e| e.into_inner());
303 let cached = state.lru.get(key).map(|entry| entry.data.clone());
304 if cached.is_some() {
305 state.hits += 1;
306 } else {
307 state.misses += 1;
308 }
309 cached
310 }
311
312 pub fn stats(&self) -> TileCacheStats {
314 let state = self.inner.lock().unwrap_or_else(|e| e.into_inner());
315 TileCacheStats {
316 hits: state.hits,
317 misses: state.misses,
318 puts: state.puts,
319 evictions: state.evictions,
320 rejected_oversize: state.rejected_oversize,
321 capacity_bytes: state.capacity_bytes,
322 current_bytes: state.current_bytes,
323 entries: state.lru.len(),
324 }
325 }
326
327 pub(crate) fn display_default() -> Self {
328 Self::new(capacity_from_env(
329 DISPLAY_TILE_CACHE_BYTES_ENV,
330 DEFAULT_DISPLAY_TILE_CACHE_SIZE,
331 ))
332 }
333
334 pub(crate) fn display_with_config(config: CacheConfig) -> Self {
335 Self::new(config.display_tile_budget())
336 }
337
338 pub(crate) fn shared_default_with_hint(default_bytes: u64) -> Self {
339 Self::new(capacity_from_env(TILE_CACHE_BYTES_ENV, default_bytes))
340 }
341
342 pub(crate) fn shared_with_config(config: CacheConfig, source_hint: Option<u64>) -> Self {
343 Self::new(config.shared_tile_budget(source_hint))
344 }
345}
346
347impl Default for TileCache {
348 fn default() -> Self {
349 Self::shared_default_with_hint(DEFAULT_TILE_CACHE_SIZE)
350 }
351}
352
353fn capacity_from_env(env_name: &str, default_bytes: u64) -> u64 {
354 std::env::var(env_name)
355 .ok()
356 .and_then(|value| value.parse::<u64>().ok())
357 .filter(|bytes| *bytes > 0)
358 .unwrap_or(default_bytes)
359}
360
361#[cfg(test)]
362mod tile_cache_tests {
363 use super::*;
364 use crate::core::types::*;
365
366 const SVS_RGB_240_TILE_BYTES: usize = 240 * 240 * 3;
367 const COMMON_ZOOM_VIEWPORT_TILE_COUNT: i64 = 96;
368
369 fn make_sample_buffer(size: usize) -> CpuTile {
370 CpuTile {
371 width: 256,
372 height: 256,
373 channels: 3,
374 color_space: ColorSpace::Rgb,
375 layout: CpuTileLayout::Interleaved,
376 data: CpuTileData::u8(vec![0u8; size]),
377 }
378 }
379
380 fn make_key(dataset_id: u128, level: u32, col: i64, row: i64) -> CacheKey {
381 CacheKey {
382 dataset_id: DatasetId::new(dataset_id),
383 scene: 0,
384 series: 0,
385 level,
386 z: 0,
387 c: 0,
388 t: 0,
389 tile_col: col,
390 tile_row: row,
391 }
392 }
393
394 #[test]
395 fn private_cache_budget_bounds_aggregate_capacity_and_disables_excess_caches() {
396 let config = CacheConfig::deterministic().with_shared_tile_bytes(8 * 1024);
397 let mut budget = config.private_cache_budget(8);
398 let mut caches = (0..8)
399 .map(|_| PrivateCache::<u32, u32>::new(budget.allocate(1024)))
400 .collect::<Vec<_>>();
401
402 assert!(
403 caches
404 .iter()
405 .map(PrivateCache::accounted_capacity_bytes)
406 .sum::<u64>()
407 <= config.private_cache_budget_bytes()
408 );
409 assert_eq!(
410 caches
411 .iter()
412 .map(PrivateCache::capacity_entries)
413 .sum::<usize>(),
414 2
415 );
416 assert_eq!(
417 caches
418 .iter()
419 .filter(|cache| cache.capacity_entries() == 0)
420 .count(),
421 6
422 );
423
424 let disabled = caches
425 .iter_mut()
426 .find(|cache| cache.capacity_entries() == 0)
427 .expect("small aggregate budget disables at least one cache");
428 disabled.put(1, 2);
429 assert_eq!(disabled.len(), 0, "disabled caches retain no entries");
430 }
431
432 #[test]
433 fn put_and_get() {
434 let cache = TileCache::new(1024 * 1024);
435 let buf = Arc::new(make_sample_buffer(100));
436 let key = make_key(1, 0, 0, 0);
437 cache.put(key.clone(), buf.clone());
438 let result = cache.get(&key).unwrap();
439 assert_eq!(result.width, 256);
440 }
441
442 #[test]
443 fn miss_returns_none() {
444 let cache = TileCache::new(1024);
445 let key = make_key(1, 0, 0, 0);
446 assert!(cache.get(&key).is_none());
447 }
448
449 #[test]
450 fn eviction_by_byte_size() {
451 let cache = TileCache::new(250);
452 cache.put(make_key(1, 0, 0, 0), Arc::new(make_sample_buffer(100)));
453 cache.put(make_key(1, 0, 1, 0), Arc::new(make_sample_buffer(100)));
454 assert!(cache.get(&make_key(1, 0, 0, 0)).is_some());
456 assert!(cache.get(&make_key(1, 0, 1, 0)).is_some());
457
458 cache.put(make_key(1, 0, 2, 0), Arc::new(make_sample_buffer(100)));
460 assert!(cache.get(&make_key(1, 0, 0, 0)).is_none()); assert!(cache.get(&make_key(1, 0, 1, 0)).is_some());
462 assert!(cache.get(&make_key(1, 0, 2, 0)).is_some());
463 }
464
465 #[test]
466 fn different_datasets_are_independent() {
467 let cache = TileCache::new(1024);
468 cache.put(make_key(1, 0, 0, 0), Arc::new(make_sample_buffer(10)));
469 cache.put(make_key(2, 0, 0, 0), Arc::new(make_sample_buffer(10)));
470 assert!(cache.get(&make_key(1, 0, 0, 0)).is_some());
471 assert!(cache.get(&make_key(2, 0, 0, 0)).is_some());
472 }
473
474 #[test]
475 fn axis_aware_keys() {
476 let cache = TileCache::new(1024);
477 let mut key_z0 = make_key(1, 0, 0, 0);
478 key_z0.z = 0;
479 let mut key_z1 = make_key(1, 0, 0, 0);
480 key_z1.z = 1;
481 cache.put(key_z0.clone(), Arc::new(make_sample_buffer(10)));
482 cache.put(key_z1.clone(), Arc::new(make_sample_buffer(10)));
483 assert!(cache.get(&key_z0).is_some());
484 assert!(cache.get(&key_z1).is_some());
485 }
486
487 #[test]
488 fn oversize_entry_rejected() {
489 let cache = TileCache::new(50);
490 cache.put(make_key(1, 0, 0, 0), Arc::new(make_sample_buffer(100)));
491 assert!(cache.get(&make_key(1, 0, 0, 0)).is_none());
492 }
493
494 #[test]
495 fn shared_across_threads() {
496 let cache = Arc::new(TileCache::new(4096));
497 let cache_clone = cache.clone();
498 let handle = std::thread::spawn(move || {
499 cache_clone.put(make_key(1, 0, 5, 5), Arc::new(make_sample_buffer(10)));
500 });
501 handle.join().unwrap();
502 assert!(cache.get(&make_key(1, 0, 5, 5)).is_some());
503 }
504
505 #[test]
506 fn display_default_holds_common_svs_zoom_viewport_working_set() {
507 let cache = TileCache::new(DEFAULT_DISPLAY_TILE_CACHE_SIZE);
508 for col in 0..COMMON_ZOOM_VIEWPORT_TILE_COUNT {
509 cache.put(
510 make_key(1, 0, col, 0),
511 Arc::new(make_sample_buffer(SVS_RGB_240_TILE_BYTES)),
512 );
513 }
514
515 let stats = cache.stats();
516 assert_eq!(stats.entries, COMMON_ZOOM_VIEWPORT_TILE_COUNT as usize);
517 assert_eq!(stats.evictions, 0);
518 assert_eq!(stats.rejected_oversize, 0);
519 }
520
521 #[test]
522 fn stats_count_hits_misses_puts_evictions_and_oversize_rejections() {
523 let cache = TileCache::new(150);
524 let missing = make_key(1, 0, 9, 9);
525 assert!(cache.get(&missing).is_none());
526
527 cache.put(make_key(1, 0, 0, 0), Arc::new(make_sample_buffer(100)));
528 assert!(cache.get(&make_key(1, 0, 0, 0)).is_some());
529
530 cache.put(make_key(1, 0, 1, 0), Arc::new(make_sample_buffer(100)));
531 cache.put(make_key(1, 0, 2, 0), Arc::new(make_sample_buffer(200)));
532
533 let stats = cache.stats();
534 assert_eq!(stats.hits, 1);
535 assert_eq!(stats.misses, 1);
536 assert_eq!(stats.puts, 2);
537 assert_eq!(stats.evictions, 1);
538 assert_eq!(stats.rejected_oversize, 1);
539 assert_eq!(stats.capacity_bytes, 150);
540 assert_eq!(stats.current_bytes, 100);
541 assert_eq!(stats.entries, 1);
542 }
543}