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
26#[cfg(target_arch = "aarch64")]
36pub const CACHE_LINE_SIZE: usize = 128;
37
38#[cfg(target_arch = "x86_64")]
39pub const CACHE_LINE_SIZE: usize = 64;
40
41#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
42pub const CACHE_LINE_SIZE: usize = 64;
43
44#[inline]
46pub fn align_to_cache_line(size: usize) -> usize {
47 (size + CACHE_LINE_SIZE - 1) & !(CACHE_LINE_SIZE - 1)
48}
49
50pub type ChunkCoord = Vec<u64>;
52
53pub const DEFAULT_CACHE_BYTES: usize = 1024 * 1024; pub const DEFAULT_MAX_SLOTS: usize = 16;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct ChunkCacheConfig {
70 max_bytes: usize,
71 max_slots: usize,
72 cache_index: bool,
73}
74
75impl ChunkCacheConfig {
76 pub const fn new() -> Self {
79 Self {
80 max_bytes: DEFAULT_CACHE_BYTES,
81 max_slots: DEFAULT_MAX_SLOTS,
82 cache_index: true,
83 }
84 }
85
86 pub const fn from_h5p_cache(rdcc_nslots: usize, rdcc_nbytes: usize) -> Self {
95 Self {
96 max_bytes: rdcc_nbytes,
97 max_slots: rdcc_nslots,
98 cache_index: true,
99 }
100 }
101
102 pub const fn disabled() -> Self {
104 Self {
105 max_bytes: 0,
106 max_slots: 0,
107 cache_index: false,
108 }
109 }
110
111 pub const fn with_max_bytes(mut self, max_bytes: usize) -> Self {
113 self.max_bytes = max_bytes;
114 self
115 }
116
117 pub const fn with_max_slots(mut self, max_slots: usize) -> Self {
119 self.max_slots = max_slots;
120 self
121 }
122
123 pub const fn with_index_cache(mut self, enabled: bool) -> Self {
125 self.cache_index = enabled;
126 self
127 }
128
129 pub const fn max_bytes(&self) -> usize {
131 self.max_bytes
132 }
133
134 pub const fn max_slots(&self) -> usize {
136 self.max_slots
137 }
138
139 pub const fn index_cache_enabled(&self) -> bool {
141 self.cache_index
142 }
143}
144
145impl Default for ChunkCacheConfig {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
159pub struct ChunkCacheStats {
160 index_loaded: bool,
161 cached_chunks: usize,
162 cached_bytes: usize,
163}
164
165impl ChunkCacheStats {
166 pub const fn index_loaded(&self) -> bool {
168 self.index_loaded
169 }
170
171 pub const fn cached_chunks(&self) -> usize {
173 self.cached_chunks
174 }
175
176 pub const fn cached_bytes(&self) -> usize {
178 self.cached_bytes
179 }
180}
181
182struct CachedChunk {
187 coord: ChunkCoord,
188 data: Vec<u8>,
189 last_access: u64,
191}
192
193pub struct ChunkCache {
209 inner: Mutex<CacheInner>,
210}
211
212struct CacheInner {
213 #[cfg(feature = "std")]
216 index: Option<HashMap<ChunkCoord, ChunkInfo>>,
217 #[cfg(not(feature = "std"))]
218 index: Option<BTreeMap<ChunkCoord, ChunkInfo>>,
219
220 slots: Vec<CachedChunk>,
222
223 current_bytes: usize,
225
226 max_bytes: usize,
228
229 max_slots: usize,
231
232 tick: u64,
234
235 cache_index: bool,
237}
238
239impl ChunkCache {
240 pub fn new() -> Self {
242 Self::with_capacity(DEFAULT_CACHE_BYTES, DEFAULT_MAX_SLOTS)
243 }
244
245 pub fn with_capacity(max_bytes: usize, max_slots: usize) -> Self {
247 Self::with_config(
248 ChunkCacheConfig::new()
249 .with_max_bytes(max_bytes)
250 .with_max_slots(max_slots),
251 )
252 }
253
254 pub fn with_config(config: ChunkCacheConfig) -> Self {
256 Self {
257 inner: Mutex::new(CacheInner {
258 index: None,
259 slots: Vec::with_capacity(config.max_slots.min(64)),
260 current_bytes: 0,
261 max_bytes: config.max_bytes,
262 max_slots: config.max_slots,
263 tick: 0,
264 cache_index: config.cache_index,
265 }),
266 }
267 }
268
269 pub fn stats(&self) -> ChunkCacheStats {
276 let inner = self.inner.lock().unwrap();
277 ChunkCacheStats {
278 index_loaded: inner.index.is_some(),
279 cached_chunks: inner.slots.len(),
280 cached_bytes: inner.current_bytes,
281 }
282 }
283
284 pub fn populate_index(&self, chunks: &[ChunkInfo], rank: usize) {
291 let mut inner = self.inner.lock().unwrap();
292 if !inner.cache_index {
293 return;
294 }
295 if inner.index.is_some() {
296 return; }
298 #[cfg(feature = "std")]
299 let mut map = HashMap::with_capacity(chunks.len());
300 #[cfg(not(feature = "std"))]
301 let mut map = BTreeMap::new();
302
303 for ci in chunks {
304 let coord: ChunkCoord = ci.offsets.iter().take(rank).copied().collect();
305 map.insert(coord, ci.clone());
306 }
307 inner.index = Some(map);
308 }
309
310 pub fn all_indexed_chunks(&self) -> Option<Vec<ChunkInfo>> {
312 let inner = self.inner.lock().unwrap();
313 inner.index.as_ref().map(|m| m.values().cloned().collect())
314 }
315
316 pub fn with_decompressed<R>(&self, coord: &[u64], f: impl FnOnce(&[u8]) -> R) -> Option<R> {
326 let mut inner = self.inner.lock().unwrap();
327 inner.tick += 1;
328 let tick = inner.tick;
329 for slot in inner.slots.iter_mut() {
330 if slot.coord.as_slice() == coord {
331 slot.last_access = tick;
332 return Some(f(&slot.data));
333 }
334 }
335 None
336 }
337
338 fn accepts_decompressed_len(&self, data_len: usize) -> bool {
342 let inner = self.inner.lock().unwrap();
343 inner.max_bytes != 0 && inner.max_slots != 0 && data_len <= inner.max_bytes
344 }
345
346 pub fn put_decompressed(&self, coord: ChunkCoord, data: Vec<u8>) {
350 let mut inner = self.inner.lock().unwrap();
351 let data_len = data.len();
352
353 if inner.max_bytes == 0 || inner.max_slots == 0 || data_len > inner.max_bytes {
355 return;
356 }
357
358 inner.tick += 1;
360 let tick = inner.tick;
361 for slot in inner.slots.iter_mut() {
362 if slot.coord == coord {
363 slot.last_access = tick;
364 return; }
366 }
367
368 while inner.slots.len() >= inner.max_slots
370 || (inner.current_bytes + data_len > inner.max_bytes && !inner.slots.is_empty())
371 {
372 let lru_idx = inner
374 .slots
375 .iter()
376 .enumerate()
377 .min_by_key(|(_, s)| s.last_access)
378 .map(|(i, _)| i)
379 .unwrap();
380 let removed = inner.slots.swap_remove(lru_idx);
381 inner.current_bytes -= removed.data.len();
382 }
383
384 inner.current_bytes += data_len;
385 inner.slots.push(CachedChunk {
386 coord,
387 data,
388 last_access: tick,
389 });
390 }
391
392 pub fn put_decompressed_slice(&self, coord: ChunkCoord, data: &[u8]) {
397 if !self.accepts_decompressed_len(data.len()) {
398 return;
399 }
400 self.put_decompressed(coord, data.to_vec());
401 }
402
403 #[cfg(test)]
408 pub fn clear(&self) {
409 let mut inner = self.inner.lock().unwrap();
410 inner.index = None;
411 inner.slots.clear();
412 inner.current_bytes = 0;
413 inner.tick = 0;
414 }
415}
416
417impl Default for ChunkCache {
418 fn default() -> Self {
419 Self::new()
420 }
421}
422
423#[cfg(test)]
428mod tests {
429 use super::*;
430
431 fn make_chunk(offsets: Vec<u64>, address: u64, size: u32) -> ChunkInfo {
432 ChunkInfo {
433 chunk_size: size,
434 filter_mask: 0,
435 offsets,
436 address,
437 }
438 }
439
440 #[test]
441 fn index_populate_and_lookup() {
442 let cache = ChunkCache::new();
443 let chunks = vec![
444 make_chunk(vec![0, 0, 0], 0x1000, 80),
445 make_chunk(vec![10, 0, 0], 0x2000, 80),
446 ];
447 cache.populate_index(&chunks, 2); assert!(cache.stats().index_loaded());
449
450 let mut addrs: Vec<u64> = cache
451 .all_indexed_chunks()
452 .unwrap()
453 .iter()
454 .map(|c| c.address)
455 .collect();
456 addrs.sort_unstable();
457 assert_eq!(addrs, vec![0x1000, 0x2000]);
458 }
459
460 fn get_decompressed(cache: &ChunkCache, coord: &[u64]) -> Option<Vec<u8>> {
463 cache.with_decompressed(coord, <[u8]>::to_vec)
464 }
465
466 #[test]
467 fn decompressed_cache_hit() {
468 let cache = ChunkCache::new();
469 cache.put_decompressed(vec![0, 0], vec![1, 2, 3, 4]);
470 let got = get_decompressed(&cache, &[0, 0]).unwrap();
471 assert_eq!(got, vec![1, 2, 3, 4]);
472 }
473
474 #[test]
475 fn lru_eviction_by_slots() {
476 let cache = ChunkCache::with_capacity(1024 * 1024, 2); cache.put_decompressed(vec![0], vec![1; 10]);
479 cache.put_decompressed(vec![1], vec![2; 10]);
480 assert_eq!(cache.stats().cached_chunks(), 2);
481
482 get_decompressed(&cache, &[0]);
484
485 cache.put_decompressed(vec![2], vec![3; 10]);
487 assert_eq!(cache.stats().cached_chunks(), 2);
488
489 assert!(get_decompressed(&cache, &[0]).is_some());
490 assert!(get_decompressed(&cache, &[1]).is_none()); assert!(get_decompressed(&cache, &[2]).is_some());
492 }
493
494 #[test]
495 fn lru_eviction_by_bytes() {
496 let cache = ChunkCache::with_capacity(50, 100); cache.put_decompressed(vec![0], vec![0; 20]);
499 cache.put_decompressed(vec![1], vec![0; 20]);
500 assert_eq!(cache.stats().cached_bytes(), 40);
501
502 cache.put_decompressed(vec![2], vec![0; 20]);
504 assert!(cache.stats().cached_bytes() <= 50);
505 assert!(get_decompressed(&cache, &[0]).is_none()); }
507
508 #[test]
509 fn put_decompressed_slice_only_copies_when_admitted() {
510 let cache = ChunkCache::with_config(ChunkCacheConfig::disabled());
512 cache.put_decompressed_slice(vec![0], &[1, 2, 3]);
513 assert_eq!(cache.stats().cached_chunks(), 0);
514
515 let cache = ChunkCache::with_capacity(1024, 16);
517 cache.put_decompressed_slice(vec![0], &[1, 2, 3, 4]);
518 assert_eq!(get_decompressed(&cache, &[0]).unwrap(), vec![1, 2, 3, 4]);
519
520 let cache = ChunkCache::with_capacity(2, 16);
522 cache.put_decompressed_slice(vec![0], &[1, 2, 3, 4]);
523 assert_eq!(cache.stats().cached_chunks(), 0);
524 }
525
526 #[test]
527 fn oversized_chunk_not_cached() {
528 let cache = ChunkCache::with_capacity(10, 16);
529 cache.put_decompressed(vec![0], vec![0; 100]); assert_eq!(cache.stats().cached_chunks(), 0);
531 }
532
533 #[test]
534 fn disabled_cache_retains_no_index_or_chunks() {
535 let cache = ChunkCache::with_config(ChunkCacheConfig::disabled());
536 let chunks = vec![make_chunk(vec![0, 0], 0x1000, 80)];
537 cache.populate_index(&chunks, 1);
538 assert!(!cache.stats().index_loaded());
539
540 cache.put_decompressed(vec![0], vec![1, 2, 3]);
541 assert_eq!(cache.stats().cached_chunks(), 0);
542 assert_eq!(cache.stats().cached_bytes(), 0);
543 }
544
545 #[test]
546 fn h5p_cache_constructor_maps_raw_data_chunk_settings() {
547 let config = ChunkCacheConfig::from_h5p_cache(521, 2 * 1024 * 1024);
548 assert_eq!(config.max_slots(), 521);
549 assert_eq!(config.max_bytes(), 2 * 1024 * 1024);
550 assert!(config.index_cache_enabled());
551 }
552
553 #[test]
554 fn clear_resets_everything() {
555 let cache = ChunkCache::new();
556 let chunks = vec![make_chunk(vec![0, 0], 0x1000, 80)];
557 cache.populate_index(&chunks, 1);
558 cache.put_decompressed(vec![0], vec![1, 2, 3]);
559
560 cache.clear();
561 assert!(!cache.stats().index_loaded());
562 assert_eq!(cache.stats().cached_chunks(), 0);
563 assert_eq!(cache.stats().cached_bytes(), 0);
564 }
565
566 #[test]
567 fn duplicate_insert_is_noop() {
568 let cache = ChunkCache::new();
569 cache.put_decompressed(vec![0], vec![1, 2, 3]);
570 cache.put_decompressed(vec![0], vec![1, 2, 3]); assert_eq!(cache.stats().cached_chunks(), 1);
572 assert_eq!(cache.stats().cached_bytes(), 3);
573 }
574
575 #[test]
576 fn align_to_cache_line_values() {
577 assert_eq!(align_to_cache_line(0), 0);
578 assert_eq!(align_to_cache_line(1), CACHE_LINE_SIZE);
579 assert_eq!(align_to_cache_line(CACHE_LINE_SIZE), CACHE_LINE_SIZE);
580 assert_eq!(
581 align_to_cache_line(CACHE_LINE_SIZE + 1),
582 CACHE_LINE_SIZE * 2
583 );
584 }
585}