1#[cfg(not(feature = "std"))]
9extern crate alloc;
10
11#[cfg(not(feature = "std"))]
12use alloc::{vec, vec::Vec};
13
14use std::alloc;
15use std::sync::Mutex;
16use core::ops::{Deref, DerefMut};
17
18#[cfg(feature = "std")]
19use std::collections::HashMap;
20#[cfg(not(feature = "std"))]
21use alloc::collections::BTreeMap;
22
23use crate::chunked_read::ChunkInfo;
24
25#[cfg(target_arch = "aarch64")]
34pub const CACHE_LINE_SIZE: usize = 128;
35
36#[cfg(target_arch = "x86_64")]
37pub const CACHE_LINE_SIZE: usize = 64;
38
39#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
40pub const CACHE_LINE_SIZE: usize = 64;
41
42#[inline]
44pub fn align_to_cache_line(size: usize) -> usize {
45 (size + CACHE_LINE_SIZE - 1) & !(CACHE_LINE_SIZE - 1)
46}
47
48pub struct CacheAlignedBuffer {
60 ptr: *mut u8,
61 len: usize,
62 capacity: usize,
63}
64
65unsafe impl Send for CacheAlignedBuffer {}
67unsafe impl Sync for CacheAlignedBuffer {}
68
69impl CacheAlignedBuffer {
70 pub fn zeroed(len: usize) -> Self {
73 if len == 0 {
74 return Self {
75 ptr: core::ptr::NonNull::dangling().as_ptr(),
76 len: 0,
77 capacity: 0,
78 };
79 }
80 let capacity = align_to_cache_line(len);
81 let layout = core::alloc::Layout::from_size_align(capacity, CACHE_LINE_SIZE)
82 .expect("invalid layout");
83 let ptr = unsafe { alloc::alloc_zeroed(layout) };
85 if ptr.is_null() {
86 alloc::handle_alloc_error(layout);
87 }
88 Self { ptr, len, capacity }
89 }
90
91 pub fn from_slice(data: &[u8]) -> Self {
93 let mut buf = Self::zeroed(data.len());
94 buf.as_mut_slice()[..data.len()].copy_from_slice(data);
95 buf
96 }
97
98 pub fn from_vec(v: Vec<u8>) -> Self {
100 Self::from_slice(&v)
101 }
102
103 #[inline]
105 pub fn len(&self) -> usize {
106 self.len
107 }
108
109 #[inline]
111 pub fn is_empty(&self) -> bool {
112 self.len == 0
113 }
114
115 #[inline]
117 pub fn as_ptr(&self) -> *const u8 {
118 self.ptr
119 }
120
121 #[inline]
123 pub fn as_mut_ptr(&mut self) -> *mut u8 {
124 self.ptr
125 }
126
127 #[inline]
129 pub fn as_slice(&self) -> &[u8] {
130 if self.len == 0 {
131 return &[];
132 }
133 unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
135 }
136
137 #[inline]
139 pub fn as_mut_slice(&mut self) -> &mut [u8] {
140 if self.len == 0 {
141 return &mut [];
142 }
143 unsafe { core::slice::from_raw_parts_mut(self.ptr, self.len) }
145 }
146
147 pub fn to_vec(&self) -> Vec<u8> {
149 self.as_slice().to_vec()
150 }
151
152 #[inline]
154 pub fn is_aligned(&self) -> bool {
155 self.len == 0 || (self.ptr as usize) % CACHE_LINE_SIZE == 0
156 }
157}
158
159impl Drop for CacheAlignedBuffer {
160 fn drop(&mut self) {
161 if self.capacity > 0 {
162 let layout = core::alloc::Layout::from_size_align(self.capacity, CACHE_LINE_SIZE)
163 .expect("invalid layout");
164 unsafe { alloc::dealloc(self.ptr, layout) };
166 }
167 }
168}
169
170impl Clone for CacheAlignedBuffer {
171 fn clone(&self) -> Self {
172 Self::from_slice(self.as_slice())
173 }
174}
175
176impl Deref for CacheAlignedBuffer {
177 type Target = [u8];
178 #[inline]
179 fn deref(&self) -> &[u8] {
180 self.as_slice()
181 }
182}
183
184impl DerefMut for CacheAlignedBuffer {
185 #[inline]
186 fn deref_mut(&mut self) -> &mut [u8] {
187 self.as_mut_slice()
188 }
189}
190
191impl core::fmt::Debug for CacheAlignedBuffer {
192 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
193 f.debug_struct("CacheAlignedBuffer")
194 .field("len", &self.len)
195 .field("capacity", &self.capacity)
196 .field("aligned", &self.is_aligned())
197 .finish()
198 }
199}
200
201pub type ChunkCoord = Vec<u64>;
203
204pub const DEFAULT_CACHE_BYTES: usize = 1024 * 1024; pub const DEFAULT_MAX_SLOTS: usize = 16;
209
210struct CachedChunk {
215 coord: ChunkCoord,
216 data: CacheAlignedBuffer,
217 last_access: u64,
219}
220
221pub struct ChunkCache {
237 inner: Mutex<CacheInner>,
238}
239
240struct CacheInner {
241 #[cfg(feature = "std")]
244 index: Option<HashMap<ChunkCoord, ChunkInfo>>,
245 #[cfg(not(feature = "std"))]
246 index: Option<BTreeMap<ChunkCoord, ChunkInfo>>,
247
248 slots: Vec<CachedChunk>,
250
251 current_bytes: usize,
253
254 max_bytes: usize,
256
257 max_slots: usize,
259
260 tick: u64,
262
263 last_coord: Option<ChunkCoord>,
265
266 stats: AccessStats,
268}
269
270#[derive(Debug, Clone, Default)]
275pub struct AccessStats {
276 pub sequential_count: u64,
278 pub random_count: u64,
280 pub sweep_direction: Option<&'static str>,
282}
283
284impl ChunkCache {
285 pub fn new() -> Self {
287 Self::with_capacity(DEFAULT_CACHE_BYTES, DEFAULT_MAX_SLOTS)
288 }
289
290 pub fn with_capacity(max_bytes: usize, max_slots: usize) -> Self {
292 Self {
293 inner: Mutex::new(CacheInner {
294 index: None,
295 slots: Vec::with_capacity(max_slots.min(64)),
296 current_bytes: 0,
297 max_bytes,
298 max_slots,
299 tick: 0,
300 last_coord: None,
301 stats: AccessStats::default(),
302 }),
303 }
304 }
305
306 pub fn has_index(&self) -> bool {
310 self.inner.lock().unwrap().index.is_some()
311 }
312
313 pub fn populate_index(&self, chunks: &[ChunkInfo], rank: usize) {
318 let mut inner = self.inner.lock().unwrap();
319 if inner.index.is_some() {
320 return; }
322 #[cfg(feature = "std")]
323 let mut map = HashMap::with_capacity(chunks.len());
324 #[cfg(not(feature = "std"))]
325 let mut map = BTreeMap::new();
326
327 for ci in chunks {
328 let coord: ChunkCoord = ci.offsets.iter().take(rank).copied().collect();
329 map.insert(coord, ci.clone());
330 }
331 inner.index = Some(map);
332 }
333
334 pub fn lookup_index(&self, coord: &[u64]) -> Option<ChunkInfo> {
336 let inner = self.inner.lock().unwrap();
337 inner.index.as_ref()?.get(coord).cloned()
338 }
339
340 pub fn all_indexed_chunks(&self) -> Option<Vec<ChunkInfo>> {
342 let inner = self.inner.lock().unwrap();
343 inner.index.as_ref().map(|m| m.values().cloned().collect())
344 }
345
346 pub fn get_decompressed(&self, coord: &[u64]) -> Option<Vec<u8>> {
352 let mut inner = self.inner.lock().unwrap();
353 inner.tick += 1;
354 let tick = inner.tick;
355
356 let is_sequential = inner.last_coord.as_ref().map_or(false, |prev| {
358 let changes: usize = prev.iter().zip(coord.iter())
360 .filter(|(a, b)| a != b)
361 .count();
362 changes <= 1
363 });
364 if is_sequential {
365 inner.stats.sequential_count += 1;
366 } else if inner.last_coord.is_some() {
367 inner.stats.random_count += 1;
368 }
369 inner.last_coord = Some(coord.to_vec());
370
371 for slot in inner.slots.iter_mut() {
372 if slot.coord.as_slice() == coord {
373 slot.last_access = tick;
374 return Some(slot.data.to_vec());
375 }
376 }
377 None
378 }
379
380 pub fn get_decompressed_aligned(&self, coord: &[u64]) -> Option<CacheAlignedBuffer> {
382 let mut inner = self.inner.lock().unwrap();
383 inner.tick += 1;
384 let tick = inner.tick;
385 for slot in inner.slots.iter_mut() {
386 if slot.coord.as_slice() == coord {
387 slot.last_access = tick;
388 return Some(slot.data.clone());
389 }
390 }
391 None
392 }
393
394 pub fn put_decompressed(&self, coord: ChunkCoord, data: Vec<u8>) {
399 let aligned = CacheAlignedBuffer::from_slice(&data);
400 self.put_decompressed_aligned(coord, aligned);
401 }
402
403 pub fn put_decompressed_aligned(&self, coord: ChunkCoord, data: CacheAlignedBuffer) {
405 let mut inner = self.inner.lock().unwrap();
406 let data_len = data.len();
407
408 if data_len > inner.max_bytes {
410 return;
411 }
412
413 inner.tick += 1;
415 let tick = inner.tick;
416 for slot in inner.slots.iter_mut() {
417 if slot.coord == coord {
418 slot.last_access = tick;
419 return; }
421 }
422
423 while inner.slots.len() >= inner.max_slots
425 || (inner.current_bytes + data_len > inner.max_bytes && !inner.slots.is_empty())
426 {
427 let lru_idx = inner
429 .slots
430 .iter()
431 .enumerate()
432 .min_by_key(|(_, s)| s.last_access)
433 .map(|(i, _)| i)
434 .unwrap();
435 let removed = inner.slots.swap_remove(lru_idx);
436 inner.current_bytes -= removed.data.len();
437 }
438
439 inner.current_bytes += data_len;
440 inner.slots.push(CachedChunk {
441 coord,
442 data,
443 last_access: tick,
444 });
445 }
446
447 pub fn clear(&self) {
449 let mut inner = self.inner.lock().unwrap();
450 inner.index = None;
451 inner.slots.clear();
452 inner.current_bytes = 0;
453 inner.tick = 0;
454 inner.last_coord = None;
455 inner.stats = AccessStats::default();
456 }
457
458 pub fn prefetch_hint(&self, next_coords: &[ChunkCoord]) {
464 let inner = self.inner.lock().unwrap();
465 if inner.index.is_none() {
466 return;
467 }
468 drop(inner);
469 let mut inner = self.inner.lock().unwrap();
474 for coord in next_coords {
475 let exists = inner.index.as_ref()
476 .map(|idx| idx.contains_key(coord))
477 .unwrap_or(false);
478 if exists {
479 inner.stats.sequential_count += 1;
480 }
481 }
482 }
483
484 pub fn access_stats(&self) -> AccessStats {
486 self.inner.lock().unwrap().stats.clone()
487 }
488
489 pub fn set_sweep_direction(&self, direction: &'static str) {
491 self.inner.lock().unwrap().stats.sweep_direction = Some(direction);
492 }
493
494 pub fn cached_chunk_count(&self) -> usize {
496 self.inner.lock().unwrap().slots.len()
497 }
498
499 pub fn cached_bytes(&self) -> usize {
501 self.inner.lock().unwrap().current_bytes
502 }
503}
504
505impl Default for ChunkCache {
506 fn default() -> Self {
507 Self::new()
508 }
509}
510
511#[cfg(test)]
516mod tests {
517 use super::*;
518
519 fn make_chunk(offsets: Vec<u64>, address: u64, size: u32) -> ChunkInfo {
520 ChunkInfo {
521 chunk_size: size,
522 filter_mask: 0,
523 offsets,
524 address,
525 }
526 }
527
528 #[test]
529 fn index_populate_and_lookup() {
530 let cache = ChunkCache::new();
531 let chunks = vec![
532 make_chunk(vec![0, 0, 0], 0x1000, 80),
533 make_chunk(vec![10, 0, 0], 0x2000, 80),
534 ];
535 cache.populate_index(&chunks, 2); assert!(cache.has_index());
537
538 let c0 = cache.lookup_index(&[0, 0]).unwrap();
539 assert_eq!(c0.address, 0x1000);
540
541 let c1 = cache.lookup_index(&[10, 0]).unwrap();
542 assert_eq!(c1.address, 0x2000);
543
544 assert!(cache.lookup_index(&[5, 0]).is_none());
545 }
546
547 #[test]
548 fn decompressed_cache_hit() {
549 let cache = ChunkCache::new();
550 cache.put_decompressed(vec![0, 0], vec![1, 2, 3, 4]);
551 let got = cache.get_decompressed(&[0, 0]).unwrap();
552 assert_eq!(got, vec![1, 2, 3, 4]);
553 }
554
555 #[test]
556 fn lru_eviction_by_slots() {
557 let cache = ChunkCache::with_capacity(1024 * 1024, 2); cache.put_decompressed(vec![0], vec![1; 10]);
560 cache.put_decompressed(vec![1], vec![2; 10]);
561 assert_eq!(cache.cached_chunk_count(), 2);
562
563 cache.get_decompressed(&[0]);
565
566 cache.put_decompressed(vec![2], vec![3; 10]);
568 assert_eq!(cache.cached_chunk_count(), 2);
569
570 assert!(cache.get_decompressed(&[0]).is_some());
571 assert!(cache.get_decompressed(&[1]).is_none()); assert!(cache.get_decompressed(&[2]).is_some());
573 }
574
575 #[test]
576 fn lru_eviction_by_bytes() {
577 let cache = ChunkCache::with_capacity(50, 100); cache.put_decompressed(vec![0], vec![0; 20]);
580 cache.put_decompressed(vec![1], vec![0; 20]);
581 assert_eq!(cache.cached_bytes(), 40);
582
583 cache.put_decompressed(vec![2], vec![0; 20]);
585 assert!(cache.cached_bytes() <= 50);
586 assert!(cache.get_decompressed(&[0]).is_none()); }
588
589 #[test]
590 fn oversized_chunk_not_cached() {
591 let cache = ChunkCache::with_capacity(10, 16);
592 cache.put_decompressed(vec![0], vec![0; 100]); assert_eq!(cache.cached_chunk_count(), 0);
594 }
595
596 #[test]
597 fn clear_resets_everything() {
598 let cache = ChunkCache::new();
599 let chunks = vec![make_chunk(vec![0, 0], 0x1000, 80)];
600 cache.populate_index(&chunks, 1);
601 cache.put_decompressed(vec![0], vec![1, 2, 3]);
602
603 cache.clear();
604 assert!(!cache.has_index());
605 assert_eq!(cache.cached_chunk_count(), 0);
606 assert_eq!(cache.cached_bytes(), 0);
607 }
608
609 #[test]
610 fn duplicate_insert_is_noop() {
611 let cache = ChunkCache::new();
612 cache.put_decompressed(vec![0], vec![1, 2, 3]);
613 cache.put_decompressed(vec![0], vec![1, 2, 3]); assert_eq!(cache.cached_chunk_count(), 1);
615 assert_eq!(cache.cached_bytes(), 3);
616 }
617
618 #[test]
621 fn aligned_buffer_basic() {
622 let buf = CacheAlignedBuffer::zeroed(256);
623 assert_eq!(buf.len(), 256);
624 assert!(buf.is_aligned());
625 assert_eq!(&buf[..4], &[0, 0, 0, 0]);
626 }
627
628 #[test]
629 fn aligned_buffer_from_slice() {
630 let data = vec![1u8, 2, 3, 4, 5];
631 let buf = CacheAlignedBuffer::from_slice(&data);
632 assert_eq!(buf.len(), 5);
633 assert!(buf.is_aligned());
634 assert_eq!(buf.to_vec(), data);
635 }
636
637 #[test]
638 fn aligned_buffer_from_vec() {
639 let data = vec![42u8; 1024];
640 let buf = CacheAlignedBuffer::from_vec(data.clone());
641 assert!(buf.is_aligned());
642 assert_eq!(buf.to_vec(), data);
643 }
644
645 #[test]
646 fn aligned_buffer_empty() {
647 let buf = CacheAlignedBuffer::zeroed(0);
648 assert!(buf.is_empty());
649 assert!(buf.is_aligned());
650 assert_eq!(buf.to_vec(), Vec::<u8>::new());
651 }
652
653 #[test]
654 fn aligned_buffer_clone_is_aligned() {
655 let buf = CacheAlignedBuffer::from_slice(&[1, 2, 3, 4]);
656 let cloned = buf.clone();
657 assert!(cloned.is_aligned());
658 assert_eq!(buf.to_vec(), cloned.to_vec());
659 }
660
661 #[test]
662 fn aligned_buffer_deref_works() {
663 let buf = CacheAlignedBuffer::from_slice(&[10, 20, 30]);
664 assert_eq!(buf[0], 10);
665 assert_eq!(buf[1], 20);
666 assert_eq!(buf[2], 30);
667 }
668
669 #[test]
670 fn aligned_buffer_various_sizes() {
671 for size in [1, 7, 63, 64, 65, 127, 128, 129, 255, 256, 1000, 4096] {
673 let buf = CacheAlignedBuffer::zeroed(size);
674 assert!(buf.is_aligned(), "not aligned for size {size}");
675 assert_eq!(buf.len(), size);
676 }
677 }
678
679 #[test]
680 fn cached_data_is_aligned() {
681 let cache = ChunkCache::new();
682 cache.put_decompressed(vec![0, 0], vec![1, 2, 3, 4, 5, 6, 7, 8]);
683 let aligned = cache.get_decompressed_aligned(&[0, 0]).unwrap();
684 assert!(aligned.is_aligned());
685 assert_eq!(aligned.to_vec(), vec![1, 2, 3, 4, 5, 6, 7, 8]);
686 }
687
688 #[test]
689 fn align_to_cache_line_values() {
690 assert_eq!(align_to_cache_line(0), 0);
691 assert_eq!(align_to_cache_line(1), CACHE_LINE_SIZE);
692 assert_eq!(align_to_cache_line(CACHE_LINE_SIZE), CACHE_LINE_SIZE);
693 assert_eq!(align_to_cache_line(CACHE_LINE_SIZE + 1), CACHE_LINE_SIZE * 2);
694 }
695}