Skip to main content

tiff_reader/
cache.rs

1//! LRU cache for decompressed strips and tiles.
2
3use std::num::NonZeroUsize;
4use std::sync::Arc;
5
6use lru::LruCache;
7use parking_lot::Mutex;
8
9/// Cache key for a decoded strip or tile.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct BlockKey {
12    pub ifd_index: usize,
13    pub kind: BlockKind,
14    pub block_index: usize,
15}
16
17/// Whether the cached block came from a strip- or tile-backed image.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum BlockKind {
20    Strip,
21    Tile,
22}
23
24/// Thread-safe LRU cache for decoded block payloads.
25pub struct BlockCache {
26    inner: Mutex<BlockCacheState>,
27    max_bytes: usize,
28    enabled: bool,
29}
30
31struct BlockCacheState {
32    cache: LruCache<BlockKey, Arc<Vec<u8>>>,
33    current_bytes: usize,
34}
35
36impl BlockCache {
37    /// Create a new cache with byte and slot limits.
38    pub fn new(max_bytes: usize, max_slots: usize) -> Self {
39        let slots = NonZeroUsize::new(max_slots.max(1)).unwrap();
40        Self {
41            inner: Mutex::new(BlockCacheState {
42                cache: LruCache::new(slots),
43                current_bytes: 0,
44            }),
45            max_bytes,
46            enabled: max_bytes > 0 && max_slots > 0,
47        }
48    }
49
50    /// Return a cached block and promote it in LRU order.
51    pub fn get(&self, key: &BlockKey) -> Option<Arc<Vec<u8>>> {
52        if !self.enabled {
53            return None;
54        }
55        let mut state = self.inner.lock();
56        state.cache.get(key).cloned()
57    }
58
59    /// Insert a decoded block into the cache.
60    pub fn insert(&self, key: BlockKey, data: Vec<u8>) -> Arc<Vec<u8>> {
61        let data_len = data.len();
62        let value = Arc::new(data);
63
64        let mut state = self.inner.lock();
65        if let Some(previous) = state.cache.pop(&key) {
66            state.current_bytes = state.current_bytes.saturating_sub(previous.len());
67        }
68
69        if !self.enabled || data_len > self.max_bytes {
70            return value;
71        }
72
73        while state.current_bytes > self.max_bytes - data_len && !state.cache.is_empty() {
74            if let Some((_, evicted)) = state.cache.pop_lru() {
75                state.current_bytes = state.current_bytes.saturating_sub(evicted.len());
76            }
77        }
78
79        state.current_bytes += data_len;
80        if let Some((_, evicted)) = state.cache.push(key, value.clone()) {
81            state.current_bytes = state.current_bytes.saturating_sub(evicted.len());
82        }
83
84        value
85    }
86}
87
88impl Default for BlockCache {
89    fn default() -> Self {
90        Self::new(64 * 1024 * 1024, 257)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::{BlockCache, BlockKey, BlockKind};
97
98    #[test]
99    fn caches_and_promotes_entries() {
100        let cache = BlockCache::new(12, 8);
101        let a = BlockKey {
102            ifd_index: 0,
103            kind: BlockKind::Strip,
104            block_index: 0,
105        };
106        let b = BlockKey {
107            ifd_index: 0,
108            kind: BlockKind::Strip,
109            block_index: 1,
110        };
111        let c = BlockKey {
112            ifd_index: 0,
113            kind: BlockKind::Strip,
114            block_index: 2,
115        };
116
117        cache.insert(a, vec![0; 4]);
118        cache.insert(b, vec![0; 4]);
119        cache.insert(c, vec![0; 4]);
120
121        let promoted = BlockKey {
122            ifd_index: 0,
123            kind: BlockKind::Strip,
124            block_index: 0,
125        };
126        assert!(cache.get(&promoted).is_some());
127
128        let d = BlockKey {
129            ifd_index: 0,
130            kind: BlockKind::Strip,
131            block_index: 3,
132        };
133        cache.insert(d, vec![0; 4]);
134
135        let evicted = BlockKey {
136            ifd_index: 0,
137            kind: BlockKind::Strip,
138            block_index: 1,
139        };
140        assert!(cache.get(&promoted).is_some());
141        assert!(cache.get(&evicted).is_none());
142    }
143
144    #[test]
145    fn disabled_cache_bypasses_storage() {
146        let cache = BlockCache::new(0, 4);
147        let key = BlockKey {
148            ifd_index: 0,
149            kind: BlockKind::Tile,
150            block_index: 0,
151        };
152        cache.insert(key, vec![1, 2, 3]);
153        assert!(cache.get(&key).is_none());
154    }
155
156    #[test]
157    fn zero_slots_disable_cache_storage() {
158        let cache = BlockCache::new(1024, 0);
159        let key = BlockKey {
160            ifd_index: 0,
161            kind: BlockKind::Tile,
162            block_index: 0,
163        };
164        cache.insert(key, vec![1, 2, 3]);
165        assert!(cache.get(&key).is_none());
166        assert_eq!(cache.inner.lock().current_bytes, 0);
167    }
168
169    #[test]
170    fn slot_eviction_updates_byte_accounting() {
171        let cache = BlockCache::new(100, 2);
172        for block_index in 0..3 {
173            cache.insert(
174                BlockKey {
175                    ifd_index: 0,
176                    kind: BlockKind::Strip,
177                    block_index,
178                },
179                vec![0; 4],
180            );
181        }
182
183        assert_eq!(cache.inner.lock().current_bytes, 8);
184    }
185
186    #[test]
187    fn replacing_mru_entry_preserves_other_cached_blocks() {
188        let cache = BlockCache::new(10, 8);
189        let a = BlockKey {
190            ifd_index: 0,
191            kind: BlockKind::Tile,
192            block_index: 0,
193        };
194        let b = BlockKey {
195            ifd_index: 0,
196            kind: BlockKind::Tile,
197            block_index: 1,
198        };
199
200        cache.insert(a, vec![0; 8]);
201        cache.insert(b, vec![0; 2]);
202        assert!(cache.get(&a).is_some());
203
204        cache.insert(a, vec![0; 7]);
205
206        assert!(cache.get(&a).is_some());
207        assert!(cache.get(&b).is_some());
208        assert_eq!(cache.inner.lock().current_bytes, 9);
209    }
210
211    #[test]
212    fn oversized_replacement_removes_stale_entry() {
213        let cache = BlockCache::new(8, 8);
214        let key = BlockKey {
215            ifd_index: 0,
216            kind: BlockKind::Tile,
217            block_index: 0,
218        };
219
220        cache.insert(key, vec![0; 4]);
221        cache.insert(key, vec![0; 9]);
222
223        assert!(cache.get(&key).is_none());
224        assert_eq!(cache.inner.lock().current_bytes, 0);
225    }
226}