1#[cfg(not(feature = "std"))]
9extern crate alloc;
10
11#[cfg(not(feature = "std"))]
12use alloc::vec::Vec;
13
14#[cfg(not(feature = "std"))]
15use crate::nosync::Mutex;
16#[cfg(feature = "std")]
17use std::sync::Mutex;
18
19#[cfg(not(feature = "std"))]
20use alloc::collections::BTreeMap;
21#[cfg(feature = "std")]
22use std::collections::HashMap;
23
24use crate::chunked_read::ChunkInfo;
25
26pub type ChunkCoord = Vec<u64>;
28
29pub const DEFAULT_CACHE_BYTES: usize = 1024 * 1024; pub const DEFAULT_MAX_SLOTS: usize = 16;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct ChunkCacheConfig {
46 max_bytes: usize,
47 max_slots: usize,
48 cache_index: bool,
49}
50
51impl ChunkCacheConfig {
52 pub const fn new() -> Self {
55 Self {
56 max_bytes: DEFAULT_CACHE_BYTES,
57 max_slots: DEFAULT_MAX_SLOTS,
58 cache_index: true,
59 }
60 }
61
62 pub const fn from_h5p_cache(rdcc_nslots: usize, rdcc_nbytes: usize) -> Self {
71 Self {
72 max_bytes: rdcc_nbytes,
73 max_slots: rdcc_nslots,
74 cache_index: true,
75 }
76 }
77
78 pub const fn disabled() -> Self {
80 Self {
81 max_bytes: 0,
82 max_slots: 0,
83 cache_index: false,
84 }
85 }
86
87 pub const fn with_max_bytes(mut self, max_bytes: usize) -> Self {
89 self.max_bytes = max_bytes;
90 self
91 }
92
93 pub const fn with_max_slots(mut self, max_slots: usize) -> Self {
95 self.max_slots = max_slots;
96 self
97 }
98
99 pub const fn with_index_cache(mut self, enabled: bool) -> Self {
101 self.cache_index = enabled;
102 self
103 }
104
105 pub const fn max_bytes(&self) -> usize {
107 self.max_bytes
108 }
109
110 pub const fn max_slots(&self) -> usize {
112 self.max_slots
113 }
114
115 pub const fn index_cache_enabled(&self) -> bool {
117 self.cache_index
118 }
119}
120
121impl Default for ChunkCacheConfig {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
135pub struct ChunkCacheStats {
136 index_loaded: bool,
137 cached_chunks: usize,
138 cached_bytes: usize,
139}
140
141impl ChunkCacheStats {
142 pub const fn index_loaded(&self) -> bool {
144 self.index_loaded
145 }
146
147 pub const fn cached_chunks(&self) -> usize {
149 self.cached_chunks
150 }
151
152 pub const fn cached_bytes(&self) -> usize {
154 self.cached_bytes
155 }
156}
157
158struct CachedChunk {
163 coord: ChunkCoord,
164 data: Vec<u8>,
165 last_access: u64,
167}
168
169pub struct ChunkCache {
185 inner: Mutex<CacheInner>,
186}
187
188struct CacheInner {
189 #[cfg(feature = "std")]
192 index: Option<HashMap<ChunkCoord, ChunkInfo>>,
193 #[cfg(not(feature = "std"))]
194 index: Option<BTreeMap<ChunkCoord, ChunkInfo>>,
195
196 slots: Vec<CachedChunk>,
198
199 current_bytes: usize,
201
202 max_bytes: usize,
204
205 max_slots: usize,
207
208 tick: u64,
210
211 cache_index: bool,
213}
214
215impl ChunkCache {
216 pub fn new() -> Self {
218 Self::with_capacity(DEFAULT_CACHE_BYTES, DEFAULT_MAX_SLOTS)
219 }
220
221 pub fn with_capacity(max_bytes: usize, max_slots: usize) -> Self {
223 Self::with_config(
224 ChunkCacheConfig::new()
225 .with_max_bytes(max_bytes)
226 .with_max_slots(max_slots),
227 )
228 }
229
230 pub fn with_config(config: ChunkCacheConfig) -> Self {
232 Self {
233 inner: Mutex::new(CacheInner {
234 index: None,
235 slots: Vec::with_capacity(config.max_slots.min(64)),
236 current_bytes: 0,
237 max_bytes: config.max_bytes,
238 max_slots: config.max_slots,
239 tick: 0,
240 cache_index: config.cache_index,
241 }),
242 }
243 }
244
245 pub fn stats(&self) -> ChunkCacheStats {
252 let inner = self.inner.lock().unwrap();
253 ChunkCacheStats {
254 index_loaded: inner.index.is_some(),
255 cached_chunks: inner.slots.len(),
256 cached_bytes: inner.current_bytes,
257 }
258 }
259
260 pub fn populate_index(&self, chunks: &[ChunkInfo], rank: usize) {
267 let mut inner = self.inner.lock().unwrap();
268 if !inner.cache_index {
269 return;
270 }
271 if inner.index.is_some() {
272 return; }
274 #[cfg(feature = "std")]
275 let mut map = HashMap::with_capacity(chunks.len());
276 #[cfg(not(feature = "std"))]
277 let mut map = BTreeMap::new();
278
279 for ci in chunks {
280 let coord: ChunkCoord = ci.offsets.iter().take(rank).copied().collect();
281 map.insert(coord, ci.clone());
282 }
283 inner.index = Some(map);
284 }
285
286 pub fn all_indexed_chunks(&self) -> Option<Vec<ChunkInfo>> {
288 let inner = self.inner.lock().unwrap();
289 inner.index.as_ref().map(|m| m.values().cloned().collect())
290 }
291
292 pub fn with_decompressed<R>(&self, coord: &[u64], f: impl FnOnce(&[u8]) -> R) -> Option<R> {
302 let mut inner = self.inner.lock().unwrap();
303 inner.tick += 1;
304 let tick = inner.tick;
305 for slot in inner.slots.iter_mut() {
306 if slot.coord.as_slice() == coord {
307 slot.last_access = tick;
308 return Some(f(&slot.data));
309 }
310 }
311 None
312 }
313
314 fn accepts_decompressed_len(&self, data_len: usize) -> bool {
318 let inner = self.inner.lock().unwrap();
319 inner.max_bytes != 0 && inner.max_slots != 0 && data_len <= inner.max_bytes
320 }
321
322 pub fn put_decompressed(&self, coord: ChunkCoord, data: Vec<u8>) {
326 let mut inner = self.inner.lock().unwrap();
327 let data_len = data.len();
328
329 if inner.max_bytes == 0 || inner.max_slots == 0 || data_len > inner.max_bytes {
331 return;
332 }
333
334 inner.tick += 1;
336 let tick = inner.tick;
337 for slot in inner.slots.iter_mut() {
338 if slot.coord == coord {
339 slot.last_access = tick;
340 return; }
342 }
343
344 while inner.slots.len() >= inner.max_slots
346 || (inner.current_bytes + data_len > inner.max_bytes && !inner.slots.is_empty())
347 {
348 let lru_idx = inner
350 .slots
351 .iter()
352 .enumerate()
353 .min_by_key(|(_, s)| s.last_access)
354 .map(|(i, _)| i)
355 .unwrap();
356 let removed = inner.slots.swap_remove(lru_idx);
357 inner.current_bytes -= removed.data.len();
358 }
359
360 inner.current_bytes += data_len;
361 inner.slots.push(CachedChunk {
362 coord,
363 data,
364 last_access: tick,
365 });
366 }
367
368 pub fn put_decompressed_slice(&self, coord: ChunkCoord, data: &[u8]) {
373 if !self.accepts_decompressed_len(data.len()) {
374 return;
375 }
376 self.put_decompressed(coord, data.to_vec());
377 }
378
379 pub fn clear(&self) {
386 let mut inner = self.inner.lock().unwrap();
387 inner.index = None;
388 inner.slots.clear();
389 inner.current_bytes = 0;
390 inner.tick = 0;
391 }
392}
393
394impl Default for ChunkCache {
395 fn default() -> Self {
396 Self::new()
397 }
398}
399
400#[cfg(test)]
405mod tests {
406 use super::*;
407
408 fn make_chunk(offsets: Vec<u64>, address: u64, size: u32) -> ChunkInfo {
409 ChunkInfo {
410 chunk_size: size,
411 filter_mask: 0,
412 offsets,
413 address,
414 }
415 }
416
417 #[test]
418 fn index_populate_and_lookup() {
419 let cache = ChunkCache::new();
420 let chunks = vec![
421 make_chunk(vec![0, 0, 0], 0x1000, 80),
422 make_chunk(vec![10, 0, 0], 0x2000, 80),
423 ];
424 cache.populate_index(&chunks, 2); assert!(cache.stats().index_loaded());
426
427 let mut addrs: Vec<u64> = cache
428 .all_indexed_chunks()
429 .unwrap()
430 .iter()
431 .map(|c| c.address)
432 .collect();
433 addrs.sort_unstable();
434 assert_eq!(addrs, vec![0x1000, 0x2000]);
435 }
436
437 fn get_decompressed(cache: &ChunkCache, coord: &[u64]) -> Option<Vec<u8>> {
440 cache.with_decompressed(coord, <[u8]>::to_vec)
441 }
442
443 #[test]
444 fn decompressed_cache_hit() {
445 let cache = ChunkCache::new();
446 cache.put_decompressed(vec![0, 0], vec![1, 2, 3, 4]);
447 let got = get_decompressed(&cache, &[0, 0]).unwrap();
448 assert_eq!(got, vec![1, 2, 3, 4]);
449 }
450
451 #[test]
452 fn lru_eviction_by_slots() {
453 let cache = ChunkCache::with_capacity(1024 * 1024, 2); cache.put_decompressed(vec![0], vec![1; 10]);
456 cache.put_decompressed(vec![1], vec![2; 10]);
457 assert_eq!(cache.stats().cached_chunks(), 2);
458
459 get_decompressed(&cache, &[0]);
461
462 cache.put_decompressed(vec![2], vec![3; 10]);
464 assert_eq!(cache.stats().cached_chunks(), 2);
465
466 assert!(get_decompressed(&cache, &[0]).is_some());
467 assert!(get_decompressed(&cache, &[1]).is_none()); assert!(get_decompressed(&cache, &[2]).is_some());
469 }
470
471 #[test]
472 fn lru_eviction_by_bytes() {
473 let cache = ChunkCache::with_capacity(50, 100); cache.put_decompressed(vec![0], vec![0; 20]);
476 cache.put_decompressed(vec![1], vec![0; 20]);
477 assert_eq!(cache.stats().cached_bytes(), 40);
478
479 cache.put_decompressed(vec![2], vec![0; 20]);
481 assert!(cache.stats().cached_bytes() <= 50);
482 assert!(get_decompressed(&cache, &[0]).is_none()); }
484
485 #[test]
486 fn put_decompressed_slice_only_copies_when_admitted() {
487 let cache = ChunkCache::with_config(ChunkCacheConfig::disabled());
489 cache.put_decompressed_slice(vec![0], &[1, 2, 3]);
490 assert_eq!(cache.stats().cached_chunks(), 0);
491
492 let cache = ChunkCache::with_capacity(1024, 16);
494 cache.put_decompressed_slice(vec![0], &[1, 2, 3, 4]);
495 assert_eq!(get_decompressed(&cache, &[0]).unwrap(), vec![1, 2, 3, 4]);
496
497 let cache = ChunkCache::with_capacity(2, 16);
499 cache.put_decompressed_slice(vec![0], &[1, 2, 3, 4]);
500 assert_eq!(cache.stats().cached_chunks(), 0);
501 }
502
503 #[test]
504 fn oversized_chunk_not_cached() {
505 let cache = ChunkCache::with_capacity(10, 16);
506 cache.put_decompressed(vec![0], vec![0; 100]); assert_eq!(cache.stats().cached_chunks(), 0);
508 }
509
510 #[test]
511 fn disabled_cache_retains_no_index_or_chunks() {
512 let cache = ChunkCache::with_config(ChunkCacheConfig::disabled());
513 let chunks = vec![make_chunk(vec![0, 0], 0x1000, 80)];
514 cache.populate_index(&chunks, 1);
515 assert!(!cache.stats().index_loaded());
516
517 cache.put_decompressed(vec![0], vec![1, 2, 3]);
518 assert_eq!(cache.stats().cached_chunks(), 0);
519 assert_eq!(cache.stats().cached_bytes(), 0);
520 }
521
522 #[test]
523 fn h5p_cache_constructor_maps_raw_data_chunk_settings() {
524 let config = ChunkCacheConfig::from_h5p_cache(521, 2 * 1024 * 1024);
525 assert_eq!(config.max_slots(), 521);
526 assert_eq!(config.max_bytes(), 2 * 1024 * 1024);
527 assert!(config.index_cache_enabled());
528 }
529
530 #[test]
531 fn clear_resets_everything() {
532 let cache = ChunkCache::new();
533 let chunks = vec![make_chunk(vec![0, 0], 0x1000, 80)];
534 cache.populate_index(&chunks, 1);
535 cache.put_decompressed(vec![0], vec![1, 2, 3]);
536
537 cache.clear();
538 assert!(!cache.stats().index_loaded());
539 assert_eq!(cache.stats().cached_chunks(), 0);
540 assert_eq!(cache.stats().cached_bytes(), 0);
541 }
542
543 #[test]
544 fn duplicate_insert_is_noop() {
545 let cache = ChunkCache::new();
546 cache.put_decompressed(vec![0], vec![1, 2, 3]);
547 cache.put_decompressed(vec![0], vec![1, 2, 3]); assert_eq!(cache.stats().cached_chunks(), 1);
549 assert_eq!(cache.stats().cached_bytes(), 3);
550 }
551}