1use lru::LruCache;
2use std::sync::{Arc, Mutex};
3
4use crate::core::types::{CpuTile, DatasetId};
5
6pub(crate) const DEFAULT_TILE_CACHE_SIZE: u64 = 8 * 1024 * 1024; const TILE_CACHE_BYTES_ENV: &str = "WSI_RS_TILE_CACHE_BYTES";
10pub(crate) const DEFAULT_DISPLAY_TILE_CACHE_SIZE: u64 = 1024 * 1024; const DISPLAY_TILE_CACHE_BYTES_ENV: &str = "WSI_RS_DISPLAY_TILE_CACHE_BYTES";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[non_exhaustive]
15pub struct CacheConfig {
16 pub shared_tile_bytes: Option<u64>,
17 pub display_tile_bytes: Option<u64>,
18}
19
20impl CacheConfig {
21 pub const fn deterministic() -> Self {
22 Self {
23 shared_tile_bytes: None,
24 display_tile_bytes: None,
25 }
26 }
27
28 pub const fn with_shared_tile_bytes(mut self, bytes: u64) -> Self {
29 self.shared_tile_bytes = Some(bytes);
30 self
31 }
32
33 pub const fn with_display_tile_bytes(mut self, bytes: u64) -> Self {
34 self.display_tile_bytes = Some(bytes);
35 self
36 }
37
38 pub(crate) fn shared_tile_budget(self, source_hint: Option<u64>) -> u64 {
39 self.shared_tile_bytes
40 .or(source_hint)
41 .unwrap_or(DEFAULT_TILE_CACHE_SIZE)
42 }
43
44 pub(crate) fn display_tile_budget(self) -> u64 {
45 self.display_tile_bytes
46 .unwrap_or(DEFAULT_DISPLAY_TILE_CACHE_SIZE)
47 }
48}
49
50impl Default for CacheConfig {
51 fn default() -> Self {
52 Self::deterministic()
53 }
54}
55
56#[derive(Hash, Eq, PartialEq, Clone, Debug)]
57pub struct CacheKey {
62 pub(crate) dataset_id: DatasetId,
63 pub(crate) scene: u32,
64 pub(crate) series: u32,
65 pub(crate) level: u32,
66 pub(crate) z: u32,
67 pub(crate) c: u32,
68 pub(crate) t: u32,
69 pub(crate) tile_col: i64,
70 pub(crate) tile_row: i64,
71}
72
73pub struct TileCache {
74 inner: Mutex<TileCacheState>,
75}
76
77#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
78pub(crate) struct CacheStats {
79 pub(crate) hits: u64,
80 pub(crate) misses: u64,
81 pub(crate) puts: u64,
82 pub(crate) evictions: u64,
83 pub(crate) rejected_oversize: u64,
84 pub(crate) capacity_bytes: u64,
85 pub(crate) current_bytes: u64,
86 pub(crate) entries: usize,
87}
88
89impl std::fmt::Debug for TileCache {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 let state = self.inner.lock().unwrap_or_else(|e| e.into_inner());
92 f.debug_struct("TileCache")
93 .field("capacity_bytes", &state.capacity_bytes)
94 .field("current_bytes", &state.current_bytes)
95 .field("entries", &state.lru.len())
96 .field("hits", &state.hits)
97 .field("misses", &state.misses)
98 .finish()
99 }
100}
101
102struct TileCacheState {
103 lru: LruCache<CacheKey, CachedTile>,
104 capacity_bytes: u64,
105 current_bytes: u64,
106 hits: u64,
107 misses: u64,
108 puts: u64,
109 evictions: u64,
110 rejected_oversize: u64,
111}
112
113struct CachedTile {
114 data: Arc<CpuTile>,
115 byte_size: u64,
116}
117
118impl TileCache {
119 pub(crate) fn new(capacity_bytes: u64) -> Self {
120 Self {
121 inner: Mutex::new(TileCacheState {
122 lru: LruCache::unbounded(),
125 capacity_bytes,
126 current_bytes: 0,
127 hits: 0,
128 misses: 0,
129 puts: 0,
130 evictions: 0,
131 rejected_oversize: 0,
132 }),
133 }
134 }
135
136 pub(crate) fn put(&self, key: CacheKey, data: Arc<CpuTile>) {
137 let byte_size = data.data.byte_size() as u64;
138 let mut state = self.inner.lock().unwrap_or_else(|e| e.into_inner());
139
140 if byte_size > state.capacity_bytes {
141 state.rejected_oversize += 1;
142 return;
143 }
144
145 if let Some((_, existing)) = state.lru.pop_entry(&key) {
147 state.current_bytes -= existing.byte_size;
148 }
149
150 while state.current_bytes + byte_size > state.capacity_bytes {
152 if let Some((_, evicted)) = state.lru.pop_lru() {
153 state.current_bytes -= evicted.byte_size;
154 state.evictions += 1;
155 } else {
156 break;
157 }
158 }
159
160 state.lru.put(key, CachedTile { data, byte_size });
161 state.current_bytes += byte_size;
162 state.puts += 1;
163 }
164
165 pub(crate) fn get(&self, key: &CacheKey) -> Option<Arc<CpuTile>> {
166 let mut state = self.inner.lock().unwrap_or_else(|e| e.into_inner());
167 let cached = state.lru.get(key).map(|entry| entry.data.clone());
168 if cached.is_some() {
169 state.hits += 1;
170 } else {
171 state.misses += 1;
172 }
173 cached
174 }
175
176 pub(crate) fn stats(&self) -> CacheStats {
177 let state = self.inner.lock().unwrap_or_else(|e| e.into_inner());
178 CacheStats {
179 hits: state.hits,
180 misses: state.misses,
181 puts: state.puts,
182 evictions: state.evictions,
183 rejected_oversize: state.rejected_oversize,
184 capacity_bytes: state.capacity_bytes,
185 current_bytes: state.current_bytes,
186 entries: state.lru.len(),
187 }
188 }
189
190 pub(crate) fn display_default() -> Self {
191 Self::new(capacity_from_env(
192 DISPLAY_TILE_CACHE_BYTES_ENV,
193 DEFAULT_DISPLAY_TILE_CACHE_SIZE,
194 ))
195 }
196
197 pub(crate) fn display_with_config(config: CacheConfig) -> Self {
198 Self::new(config.display_tile_budget())
199 }
200
201 pub(crate) fn shared_default_with_hint(default_bytes: u64) -> Self {
202 Self::new(capacity_from_env(TILE_CACHE_BYTES_ENV, default_bytes))
203 }
204
205 pub(crate) fn shared_with_config(config: CacheConfig, source_hint: Option<u64>) -> Self {
206 Self::new(config.shared_tile_budget(source_hint))
207 }
208}
209
210impl Default for TileCache {
211 fn default() -> Self {
212 Self::shared_default_with_hint(DEFAULT_TILE_CACHE_SIZE)
213 }
214}
215
216fn capacity_from_env(env_name: &str, default_bytes: u64) -> u64 {
217 std::env::var(env_name)
218 .ok()
219 .and_then(|value| value.parse::<u64>().ok())
220 .filter(|bytes| *bytes > 0)
221 .unwrap_or(default_bytes)
222}
223
224#[cfg(test)]
225mod tile_cache_tests {
226 use super::*;
227 use crate::core::types::*;
228
229 fn make_sample_buffer(size: usize) -> CpuTile {
230 CpuTile {
231 width: 256,
232 height: 256,
233 channels: 3,
234 color_space: ColorSpace::Rgb,
235 layout: CpuTileLayout::Interleaved,
236 data: CpuTileData::u8(vec![0u8; size]),
237 }
238 }
239
240 fn make_key(dataset_id: u128, level: u32, col: i64, row: i64) -> CacheKey {
241 CacheKey {
242 dataset_id: DatasetId::new(dataset_id),
243 scene: 0,
244 series: 0,
245 level,
246 z: 0,
247 c: 0,
248 t: 0,
249 tile_col: col,
250 tile_row: row,
251 }
252 }
253
254 #[test]
255 fn put_and_get() {
256 let cache = TileCache::new(1024 * 1024);
257 let buf = Arc::new(make_sample_buffer(100));
258 let key = make_key(1, 0, 0, 0);
259 cache.put(key.clone(), buf.clone());
260 let result = cache.get(&key).unwrap();
261 assert_eq!(result.width, 256);
262 }
263
264 #[test]
265 fn miss_returns_none() {
266 let cache = TileCache::new(1024);
267 let key = make_key(1, 0, 0, 0);
268 assert!(cache.get(&key).is_none());
269 }
270
271 #[test]
272 fn eviction_by_byte_size() {
273 let cache = TileCache::new(250);
274 cache.put(make_key(1, 0, 0, 0), Arc::new(make_sample_buffer(100)));
275 cache.put(make_key(1, 0, 1, 0), Arc::new(make_sample_buffer(100)));
276 assert!(cache.get(&make_key(1, 0, 0, 0)).is_some());
278 assert!(cache.get(&make_key(1, 0, 1, 0)).is_some());
279
280 cache.put(make_key(1, 0, 2, 0), Arc::new(make_sample_buffer(100)));
282 assert!(cache.get(&make_key(1, 0, 0, 0)).is_none()); assert!(cache.get(&make_key(1, 0, 1, 0)).is_some());
284 assert!(cache.get(&make_key(1, 0, 2, 0)).is_some());
285 }
286
287 #[test]
288 fn different_datasets_are_independent() {
289 let cache = TileCache::new(1024);
290 cache.put(make_key(1, 0, 0, 0), Arc::new(make_sample_buffer(10)));
291 cache.put(make_key(2, 0, 0, 0), Arc::new(make_sample_buffer(10)));
292 assert!(cache.get(&make_key(1, 0, 0, 0)).is_some());
293 assert!(cache.get(&make_key(2, 0, 0, 0)).is_some());
294 }
295
296 #[test]
297 fn axis_aware_keys() {
298 let cache = TileCache::new(1024);
299 let mut key_z0 = make_key(1, 0, 0, 0);
300 key_z0.z = 0;
301 let mut key_z1 = make_key(1, 0, 0, 0);
302 key_z1.z = 1;
303 cache.put(key_z0.clone(), Arc::new(make_sample_buffer(10)));
304 cache.put(key_z1.clone(), Arc::new(make_sample_buffer(10)));
305 assert!(cache.get(&key_z0).is_some());
306 assert!(cache.get(&key_z1).is_some());
307 }
308
309 #[test]
310 fn oversize_entry_rejected() {
311 let cache = TileCache::new(50);
312 cache.put(make_key(1, 0, 0, 0), Arc::new(make_sample_buffer(100)));
313 assert!(cache.get(&make_key(1, 0, 0, 0)).is_none());
314 }
315
316 #[test]
317 fn shared_across_threads() {
318 let cache = Arc::new(TileCache::new(4096));
319 let cache_clone = cache.clone();
320 let handle = std::thread::spawn(move || {
321 cache_clone.put(make_key(1, 0, 5, 5), Arc::new(make_sample_buffer(10)));
322 });
323 handle.join().unwrap();
324 assert!(cache.get(&make_key(1, 0, 5, 5)).is_some());
325 }
326
327 #[test]
328 fn stats_count_hits_misses_puts_evictions_and_oversize_rejections() {
329 let cache = TileCache::new(150);
330 let missing = make_key(1, 0, 9, 9);
331 assert!(cache.get(&missing).is_none());
332
333 cache.put(make_key(1, 0, 0, 0), Arc::new(make_sample_buffer(100)));
334 assert!(cache.get(&make_key(1, 0, 0, 0)).is_some());
335
336 cache.put(make_key(1, 0, 1, 0), Arc::new(make_sample_buffer(100)));
337 cache.put(make_key(1, 0, 2, 0), Arc::new(make_sample_buffer(200)));
338
339 let stats = cache.stats();
340 assert_eq!(stats.hits, 1);
341 assert_eq!(stats.misses, 1);
342 assert_eq!(stats.puts, 2);
343 assert_eq!(stats.evictions, 1);
344 assert_eq!(stats.rejected_oversize, 1);
345 assert_eq!(stats.capacity_bytes, 150);
346 assert_eq!(stats.current_bytes, 100);
347 assert_eq!(stats.entries, 1);
348 }
349}