1use std::num::NonZeroUsize;
4use std::sync::Arc;
5
6use lru::LruCache;
7use parking_lot::Mutex;
8
9#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum BlockKind {
20 Strip,
21 Tile,
22}
23
24pub 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 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 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 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}