1use async_trait::async_trait;
23use parking_lot::RwLock;
24use std::cell::Cell;
25use std::cmp::Reverse;
26use std::collections::{BTreeMap, BinaryHeap, HashMap};
27use std::io::{self, Read, Write};
28use std::ops::Range;
29use std::path::{Path, PathBuf};
30use std::sync::Arc;
31use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
32
33use super::{Directory, FileHandle, OwnedBytes, RangeReadFn};
34
35pub const SLICE_CACHE_EXTENSION: &str = "slicecache";
37
38const SLICE_CACHE_MAGIC: &[u8; 8] = b"HRMSCACH";
40
41const SLICE_CACHE_VERSION: u32 = 2;
44
45const HIT_METRICS_FLUSH_INTERVAL: u64 = 1024;
48
49const STAMP_BLOCK_SIZE: u64 = 64;
53const COUNTER_SHARDS: usize = 64;
54
55static NEXT_STAMP_BLOCK: AtomicU64 = AtomicU64::new(0);
56static NEXT_COUNTER_SHARD: AtomicUsize = AtomicUsize::new(0);
57
58thread_local! {
59 static STAMP_BLOCK: Cell<(u64, u64)> = const { Cell::new((0, 0)) };
60 static COUNTER_SHARD: usize = NEXT_COUNTER_SHARD.fetch_add(1, Ordering::Relaxed)
61 % COUNTER_SHARDS;
62}
63
64#[inline]
65fn next_stamp() -> u64 {
66 STAMP_BLOCK.with(|block| {
67 let (next, end) = block.get();
68 if next < end {
69 block.set((next + 1, end));
70 return next;
71 }
72 let start = NEXT_STAMP_BLOCK.fetch_add(STAMP_BLOCK_SIZE, Ordering::Relaxed) + 1;
73 block.set((start + 1, start + STAMP_BLOCK_SIZE));
74 start
75 })
76}
77
78#[repr(align(64))]
82struct CounterShard(AtomicU64);
83
84struct ShardedCounter {
85 shards: [CounterShard; COUNTER_SHARDS],
86}
87
88impl ShardedCounter {
89 fn new() -> Self {
90 Self {
91 shards: std::array::from_fn(|_| CounterShard(AtomicU64::new(0))),
92 }
93 }
94
95 #[inline]
96 fn increment(&self) -> u64 {
97 COUNTER_SHARD.with(|&shard| self.shards[shard].0.fetch_add(1, Ordering::Relaxed) + 1)
98 }
99
100 fn load(&self) -> u64 {
101 self.shards
102 .iter()
103 .map(|shard| shard.0.load(Ordering::Relaxed))
104 .sum()
105 }
106}
107
108#[derive(Debug)]
110struct CachedSlice {
111 range: Range<u64>,
113 data: OwnedBytes,
116 access_count: AtomicU64,
119}
120
121impl CachedSlice {
122 #[inline]
123 fn stamp(&self) -> u64 {
124 self.access_count.load(Ordering::Relaxed)
125 }
126}
127
128struct FileSliceCache {
130 id: u64,
132 slices: BTreeMap<u64, CachedSlice>,
134 total_bytes: usize,
136}
137
138impl FileSliceCache {
139 fn new(id: u64) -> Self {
140 Self {
141 id,
142 slices: BTreeMap::new(),
143 total_bytes: 0,
144 }
145 }
146
147 fn serialize(&self) -> Vec<u8> {
149 let mut buf = Vec::new();
150 buf.extend_from_slice(&(self.slices.len() as u32).to_le_bytes());
152 for slice in self.slices.values() {
153 buf.extend_from_slice(&slice.range.start.to_le_bytes());
155 buf.extend_from_slice(&slice.range.end.to_le_bytes());
156 buf.extend_from_slice(&(slice.data.len() as u32).to_le_bytes());
158 buf.extend_from_slice(slice.data.as_slice());
159 }
160 buf
161 }
162
163 fn deserialize(
165 data: &[u8],
166 id: u64,
167 access_counter: u64,
168 max_bytes: usize,
169 ) -> io::Result<(Self, usize)> {
170 let mut pos = 0;
171 if data.len() < 4 {
172 return Err(io::Error::new(
173 io::ErrorKind::InvalidData,
174 "truncated slice cache",
175 ));
176 }
177 let num_slices = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
178 pos += 4;
179
180 let mut cache = FileSliceCache::new(id);
181 for _ in 0..num_slices {
182 if pos + 20 > data.len() {
183 return Err(io::Error::new(
184 io::ErrorKind::InvalidData,
185 "truncated slice entry",
186 ));
187 }
188 let range_start = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
189 pos += 8;
190 let range_end = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
191 pos += 8;
192 let data_len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
193 pos += 4;
194
195 let data_end = pos.checked_add(data_len).ok_or_else(|| {
196 io::Error::new(io::ErrorKind::InvalidData, "slice data length overflow")
197 })?;
198 if data_end > data.len() {
199 return Err(io::Error::new(
200 io::ErrorKind::InvalidData,
201 "truncated slice data",
202 ));
203 }
204 if range_end < range_start || range_end - range_start != data_len as u64 {
205 return Err(io::Error::new(
206 io::ErrorKind::InvalidData,
207 "slice range and data length are inconsistent",
208 ));
209 }
210 let slice_range = range_start..range_end;
211 pos = data_end;
212
213 if data_len <= max_bytes {
217 let bytes_to_free = cache
218 .total_bytes
219 .saturating_add(data_len)
220 .saturating_sub(max_bytes);
221 cache.evict_lru(bytes_to_free);
222 cache.insert(
223 slice_range,
224 OwnedBytes::new(data[data_end - data_len..data_end].to_vec()),
225 access_counter,
226 );
227 debug_assert!(cache.total_bytes <= max_bytes);
228 }
229 }
230 Ok((cache, pos))
231 }
232
233 fn try_read(&self, range: Range<u64>) -> Option<OwnedBytes> {
239 let start = range.start;
240 let end = range.end;
241 let (&slice_start, slice) = self.slices.range(..=start).next_back()?;
242 if slice.range.end < end {
243 return None;
244 }
245 let stamp = next_stamp();
246 slice.access_count.store(stamp, Ordering::Relaxed);
247 let offset = (start - slice_start) as usize;
248 let len = (end - start) as usize;
249 Some(slice.data.slice(offset..offset + len))
250 }
251
252 fn insert(&mut self, range: Range<u64>, data: OwnedBytes, access_counter: u64) -> (isize, u64) {
257 let start = range.start;
258 let end = range.end;
259 let data_len = data.len();
260
261 let mut to_remove: Vec<u64> = Vec::new();
265 let mut merged_start = start;
266 let mut merged_end = end;
267 for (&slice_start, slice) in self.slices.range(..end).rev() {
268 if slice.range.end <= start {
269 break;
270 }
271 to_remove.push(slice_start);
272 merged_start = merged_start.min(slice_start);
273 merged_end = merged_end.max(slice.range.end);
274 }
275
276 let mut bytes_removed: usize = 0;
277 let (final_start, final_data) = if to_remove.is_empty() {
278 (start, data)
279 } else {
280 let merged_len = (merged_end - merged_start) as usize;
281 let mut new_data = vec![0u8; merged_len];
282
283 for &slice_start in &to_remove {
285 if let Some(slice) = self.slices.remove(&slice_start) {
286 let offset = (slice_start - merged_start) as usize;
287 new_data[offset..offset + slice.data.len()]
288 .copy_from_slice(slice.data.as_slice());
289 bytes_removed += slice.data.len();
290 self.total_bytes -= slice.data.len();
291 }
292 }
293 let offset = (start - merged_start) as usize;
294 new_data[offset..offset + data_len].copy_from_slice(data.as_slice());
295 (merged_start, OwnedBytes::new(new_data))
296 };
297
298 let bytes_added = final_data.len();
299 self.total_bytes += bytes_added;
300 self.slices.insert(
301 final_start,
302 CachedSlice {
303 range: final_start..final_start + bytes_added as u64,
304 data: final_data,
305 access_count: AtomicU64::new(access_counter),
306 },
307 );
308
309 (bytes_added as isize - bytes_removed as isize, final_start)
310 }
311
312 fn evict_lru(&mut self, bytes_to_free: usize) -> usize {
316 if bytes_to_free == 0 || self.slices.is_empty() {
317 return 0;
318 }
319 let mut order: Vec<(u64, u64)> = self
320 .slices
321 .iter()
322 .map(|(&start, slice)| (slice.stamp(), start))
323 .collect();
324 order.sort_unstable();
325
326 let mut freed = 0;
327 for (_, start) in order {
328 if freed >= bytes_to_free {
329 break;
330 }
331 if let Some(slice) = self.slices.remove(&start) {
332 freed += slice.data.len();
333 self.total_bytes -= slice.data.len();
334 }
335 }
336 freed
337 }
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
343struct LruEntry {
344 stamp: u64,
345 file_id: u64,
346 start: u64,
347}
348
349struct CacheState {
351 files: HashMap<Arc<Path>, FileSliceCache>,
352 paths: HashMap<u64, Arc<Path>>,
354 lru: BinaryHeap<Reverse<LruEntry>>,
355 current_bytes: usize,
356 total_slices: usize,
357 next_file_id: u64,
358}
359
360impl CacheState {
361 fn new() -> Self {
362 Self {
363 files: HashMap::new(),
364 paths: HashMap::new(),
365 lru: BinaryHeap::new(),
366 current_bytes: 0,
367 total_slices: 0,
368 next_file_id: 0,
369 }
370 }
371
372 fn file_mut(&mut self, path: &Path) -> &mut FileSliceCache {
373 if !self.files.contains_key(path) {
374 let id = self.next_file_id;
375 self.next_file_id += 1;
376 let shared: Arc<Path> = Arc::from(path);
377 self.paths.insert(id, Arc::clone(&shared));
378 self.files.insert(shared, FileSliceCache::new(id));
379 }
380 self.files.get_mut(path).expect("file cache just inserted")
381 }
382
383 fn remove_file(&mut self, path: &Path) -> Option<FileSliceCache> {
384 let file = self.files.remove(path)?;
385 self.paths.remove(&file.id);
386 self.current_bytes = self.current_bytes.saturating_sub(file.total_bytes);
387 self.total_slices = self.total_slices.saturating_sub(file.slices.len());
388 Some(file)
390 }
391
392 fn rename_file(&mut self, from: &Path, to: &Path) {
393 self.remove_file(to);
395 if let Some(file) = self.files.remove(from) {
396 let id = file.id;
397 let shared: Arc<Path> = Arc::from(to);
398 self.paths.insert(id, Arc::clone(&shared));
399 self.files.insert(shared, file);
400 }
401 }
402
403 fn replace_file(&mut self, path: &Path, mut cache: FileSliceCache) {
406 self.remove_file(path);
407 let id = self.next_file_id;
408 self.next_file_id += 1;
409 cache.id = id;
410 for (&start, slice) in &cache.slices {
411 self.lru.push(Reverse(LruEntry {
412 stamp: slice.stamp(),
413 file_id: id,
414 start,
415 }));
416 }
417 self.current_bytes = self.current_bytes.saturating_add(cache.total_bytes);
418 self.total_slices += cache.slices.len();
419 let shared: Arc<Path> = Arc::from(path);
420 self.paths.insert(id, Arc::clone(&shared));
421 self.files.insert(shared, cache);
422 self.compact_lru_if_bloated();
423 }
424
425 fn insert_slice(&mut self, path: &Path, range: Range<u64>, data: OwnedBytes, stamp: u64) {
427 let file = self.file_mut(path);
428 let slices_before = file.slices.len();
429 let (net_change, start) = file.insert(range, data, stamp);
430 let file_id = file.id;
431 let slices_after = file.slices.len();
432 self.total_slices = (self.total_slices + slices_after).saturating_sub(slices_before);
433 if net_change >= 0 {
434 self.current_bytes += net_change as usize;
435 } else {
436 self.current_bytes = self.current_bytes.saturating_sub((-net_change) as usize);
437 }
438 self.lru.push(Reverse(LruEntry {
439 stamp,
440 file_id,
441 start,
442 }));
443 self.compact_lru_if_bloated();
444 }
445
446 fn compact_lru_if_bloated(&mut self) {
449 if self.lru.len() > 2 * self.total_slices + 1024 {
450 self.rebuild_lru();
451 }
452 }
453
454 fn rebuild_lru(&mut self) {
455 let mut entries = Vec::with_capacity(self.total_slices);
456 for file in self.files.values() {
457 for (&start, slice) in &file.slices {
458 entries.push(Reverse(LruEntry {
459 stamp: slice.stamp(),
460 file_id: file.id,
461 start,
462 }));
463 }
464 }
465 self.lru = BinaryHeap::from(entries);
466 }
467
468 fn evict_for(&mut self, max_bytes: usize, needed: usize) -> (u64, usize) {
471 let target = self
472 .current_bytes
473 .saturating_add(needed)
474 .saturating_sub(max_bytes);
475 if target == 0 {
476 return (0, 0);
477 }
478 let mut freed = 0usize;
479 let mut evicted = 0u64;
480 let mut rebuilt = false;
481 while freed < target {
482 let Some(Reverse(entry)) = self.lru.pop() else {
483 if self.total_slices == 0 || rebuilt {
487 break;
488 }
489 self.rebuild_lru();
490 rebuilt = true;
491 continue;
492 };
493 let Some(path) = self.paths.get(&entry.file_id) else {
494 continue; };
496 let Some(file) = self.files.get_mut(path.as_ref()) else {
497 continue;
498 };
499 let Some(slice) = file.slices.get(&entry.start) else {
500 continue; };
502 let current = slice.stamp();
503 if current != entry.stamp {
504 self.lru.push(Reverse(LruEntry {
506 stamp: current,
507 ..entry
508 }));
509 continue;
510 }
511 let slice = file
512 .slices
513 .remove(&entry.start)
514 .expect("slice present under lock");
515 file.total_bytes -= slice.data.len();
516 freed += slice.data.len();
517 evicted += 1;
518 self.total_slices -= 1;
519 }
520 self.current_bytes = self.current_bytes.saturating_sub(freed);
521 (evicted, freed)
522 }
523
524 fn clear(&mut self) {
525 self.files.clear();
526 self.paths.clear();
527 self.lru.clear();
528 self.current_bytes = 0;
529 self.total_slices = 0;
530 }
531}
532
533struct SliceCacheShared {
536 state: RwLock<CacheState>,
537 max_bytes: usize,
539 hits: ShardedCounter,
540 misses: AtomicU64,
541 evicted_slices: AtomicU64,
542 evicted_bytes: AtomicU64,
543 label: super::IndexLabel,
545}
546
547impl SliceCacheShared {
548 fn new(max_bytes: usize) -> Self {
549 Self {
550 state: RwLock::new(CacheState::new()),
551 max_bytes,
552 hits: ShardedCounter::new(),
553 misses: AtomicU64::new(0),
554 evicted_slices: AtomicU64::new(0),
555 evicted_bytes: AtomicU64::new(0),
556 label: super::IndexLabel::default(),
557 }
558 }
559
560 fn try_read(&self, path: &Path, range: Range<u64>) -> Option<OwnedBytes> {
562 let hit = {
563 let state = self.state.read();
564 state.files.get(path).and_then(|file| file.try_read(range))
565 };
566 self.record_lookup(&hit);
567 hit
568 }
569
570 fn try_read_direct(&self, path: &Path, range: Range<u64>) -> Option<OwnedBytes> {
575 let hit = {
576 let state = self.state.write();
577 state.files.get(path).and_then(|file| file.try_read(range))
578 };
579 self.record_lookup(&hit);
580 hit
581 }
582
583 #[inline]
584 fn record_lookup(&self, result: &Option<OwnedBytes>) {
585 match result {
586 Some(data) => {
587 let hits = self.hits.increment();
588 if hits.is_multiple_of(HIT_METRICS_FLUSH_INTERVAL) {
589 crate::observe::slice_cache_hits(
590 &self.label.get(),
591 HIT_METRICS_FLUSH_INTERVAL,
592 data.len(),
593 );
594 }
595 }
596 None => {
597 self.misses.fetch_add(1, Ordering::Relaxed);
598 }
599 }
600 }
601
602 fn insert(&self, path: &Path, range: Range<u64>, data: OwnedBytes) {
604 let data_len = data.len();
605 crate::observe::slice_cache_miss(&self.label.get(), data_len);
606 if data_len > self.max_bytes {
609 return;
610 }
611 let stamp = next_stamp();
612 let (evicted_slices, evicted_bytes) = {
613 let mut state = self.state.write();
614 let evicted = state.evict_for(self.max_bytes, data_len);
620 state.insert_slice(path, range, data, stamp);
621 debug_assert!(state.current_bytes <= self.max_bytes);
622 evicted
623 };
624 if evicted_slices > 0 {
625 self.evicted_slices
626 .fetch_add(evicted_slices, Ordering::Relaxed);
627 self.evicted_bytes
628 .fetch_add(evicted_bytes as u64, Ordering::Relaxed);
629 crate::observe::slice_cache_evicted(&self.label.get(), evicted_slices, evicted_bytes);
630 }
631 }
632}
633
634pub struct SliceCachingDirectory<D: Directory> {
642 inner: Arc<D>,
643 shared: Arc<SliceCacheShared>,
644 file_sizes: Arc<RwLock<HashMap<PathBuf, u64>>>,
646}
647
648impl<D: Directory> SliceCachingDirectory<D> {
649 pub fn new(inner: D, max_bytes: usize) -> Self {
651 Self {
652 inner: Arc::new(inner),
653 shared: Arc::new(SliceCacheShared::new(max_bytes)),
654 file_sizes: Arc::new(RwLock::new(HashMap::new())),
655 }
656 }
657
658 pub fn inner(&self) -> &D {
660 &self.inner
661 }
662
663 fn try_cache_read(&self, path: &Path, range: Range<u64>) -> Option<OwnedBytes> {
665 self.shared.try_read_direct(path, range)
666 }
667
668 fn cache_insert(&self, path: &Path, range: Range<u64>, data: OwnedBytes) {
670 self.shared.insert(path, range, data)
671 }
672
673 fn invalidate(&self, path: &Path) {
674 {
675 let mut state = self.shared.state.write();
676 state.remove_file(path);
677 }
678 self.file_sizes.write().remove(path);
679 }
680
681 pub fn stats(&self) -> SliceCacheStats {
683 let state = self.shared.state.read();
684 let mut total_slices = 0;
685 let mut files_cached = 0;
686
687 for fc in state.files.values() {
688 if !fc.slices.is_empty() {
689 files_cached += 1;
690 total_slices += fc.slices.len();
691 }
692 }
693
694 SliceCacheStats {
695 total_bytes: state.current_bytes,
696 max_bytes: self.shared.max_bytes,
697 total_slices,
698 files_cached,
699 hits: self.shared.hits.load(),
700 misses: self.shared.misses.load(Ordering::Relaxed),
701 evicted_slices: self.shared.evicted_slices.load(Ordering::Relaxed),
702 evicted_bytes: self.shared.evicted_bytes.load(Ordering::Relaxed),
703 }
704 }
705
706 pub fn serialize(&self) -> Vec<u8> {
722 let state = self.shared.state.read();
723 let file_sizes = self.file_sizes.read();
724 let mut buf = Vec::new();
725
726 buf.extend_from_slice(SLICE_CACHE_MAGIC);
728 buf.extend_from_slice(&SLICE_CACHE_VERSION.to_le_bytes());
729
730 let non_empty: Vec<_> = state
732 .files
733 .iter()
734 .filter(|(_, fc)| !fc.slices.is_empty())
735 .collect();
736 buf.extend_from_slice(&(non_empty.len() as u32).to_le_bytes());
737
738 for (path, file_cache) in non_empty {
739 let path_str = path.to_string_lossy();
741 let path_bytes = path_str.as_bytes();
742 buf.extend_from_slice(&(path_bytes.len() as u32).to_le_bytes());
743 buf.extend_from_slice(path_bytes);
744
745 let cache_data = file_cache.serialize();
747 buf.extend_from_slice(&cache_data);
748 }
749
750 buf.extend_from_slice(&(file_sizes.len() as u32).to_le_bytes());
752 for (path, &size) in file_sizes.iter() {
753 let path_str = path.to_string_lossy();
754 let path_bytes = path_str.as_bytes();
755 buf.extend_from_slice(&(path_bytes.len() as u32).to_le_bytes());
756 buf.extend_from_slice(path_bytes);
757 buf.extend_from_slice(&size.to_le_bytes());
758 }
759
760 buf
761 }
762
763 pub fn deserialize(&self, data: &[u8]) -> io::Result<()> {
768 let mut pos = 0;
769
770 if data.len() < 16 {
772 return Err(io::Error::new(
773 io::ErrorKind::InvalidData,
774 "slice cache too short",
775 ));
776 }
777 if &data[pos..pos + 8] != SLICE_CACHE_MAGIC {
778 return Err(io::Error::new(
779 io::ErrorKind::InvalidData,
780 "invalid slice cache magic",
781 ));
782 }
783 pos += 8;
784
785 let version = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
787 pos += 4;
788 if version != 2 {
789 return Err(io::Error::new(
790 io::ErrorKind::InvalidData,
791 format!("unsupported slice cache version: {} (expected 2)", version),
792 ));
793 }
794
795 let num_files = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
797 pos += 4;
798
799 let max_bytes = self.shared.max_bytes;
800 let counter = next_stamp();
801 let mut state = self.shared.state.write();
802
803 for _ in 0..num_files {
804 if pos + 4 > data.len() {
806 return Err(io::Error::new(
807 io::ErrorKind::InvalidData,
808 "truncated path length",
809 ));
810 }
811 let path_len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
812 pos += 4;
813
814 if pos + path_len > data.len() {
816 return Err(io::Error::new(io::ErrorKind::InvalidData, "truncated path"));
817 }
818 let path_str = std::str::from_utf8(&data[pos..pos + path_len])
819 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
820 let path = PathBuf::from(path_str);
821 pos += path_len;
822
823 let (file_cache, consumed) =
825 FileSliceCache::deserialize(&data[pos..], 0, counter, max_bytes)?;
826 pos += consumed;
827
828 state.replace_file(&path, file_cache);
829 state.evict_for(max_bytes, 0);
830 }
831
832 state.current_bytes = state.files.values().map(|cache| cache.total_bytes).sum();
835 state.total_slices = state.files.values().map(|cache| cache.slices.len()).sum();
836 state.evict_for(max_bytes, 0);
837 drop(state);
838
839 if pos + 4 <= data.len() {
841 let num_sizes = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
842 pos += 4;
843
844 let mut file_sizes = self.file_sizes.write();
845 for _ in 0..num_sizes {
846 if pos + 4 > data.len() {
847 break;
848 }
849 let path_len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
850 pos += 4;
851
852 if pos + path_len > data.len() {
853 break;
854 }
855 let path_str = match std::str::from_utf8(&data[pos..pos + path_len]) {
856 Ok(s) => s,
857 Err(_) => break,
858 };
859 let path = PathBuf::from(path_str);
860 pos += path_len;
861
862 if pos + 8 > data.len() {
863 break;
864 }
865 let size = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
866 pos += 8;
867
868 file_sizes.insert(path, size);
869 }
870 }
871
872 Ok(())
873 }
874
875 pub fn serialize_to_writer<W: Write>(&self, mut writer: W) -> io::Result<()> {
877 let data = self.serialize();
878 writer.write_all(&data)
879 }
880
881 pub fn deserialize_from_reader<R: Read>(&self, mut reader: R) -> io::Result<()> {
883 let mut data = Vec::new();
884 reader.read_to_end(&mut data)?;
885 self.deserialize(&data)
886 }
887
888 pub fn is_empty(&self) -> bool {
890 self.shared.state.read().current_bytes == 0
891 }
892
893 pub fn clear(&self) {
895 self.shared.state.write().clear();
896 }
897}
898
899#[derive(Debug, Clone)]
901pub struct SliceCacheStats {
902 pub total_bytes: usize,
903 pub max_bytes: usize,
904 pub total_slices: usize,
905 pub files_cached: usize,
906 pub hits: u64,
908 pub misses: u64,
910 pub evicted_slices: u64,
912 pub evicted_bytes: u64,
914}
915
916#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
917#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
918impl<D: Directory> Directory for SliceCachingDirectory<D> {
919 async fn exists(&self, path: &Path) -> io::Result<bool> {
920 self.inner.exists(path).await
921 }
922
923 async fn file_size(&self, path: &Path) -> io::Result<u64> {
924 {
926 let file_sizes = self.file_sizes.read();
927 if let Some(&size) = file_sizes.get(path) {
928 return Ok(size);
929 }
930 }
931
932 let size = self.inner.file_size(path).await?;
934 {
935 let mut file_sizes = self.file_sizes.write();
936 file_sizes.insert(path.to_path_buf(), size);
937 }
938 Ok(size)
939 }
940
941 async fn open_read(&self, path: &Path) -> io::Result<FileHandle> {
942 let file_size = self.file_size(path).await?;
944 let full_range = 0..file_size;
945
946 if let Some(data) = self.try_cache_read(path, full_range.clone()) {
948 return Ok(FileHandle::from_bytes(data));
949 }
950
951 let handle = self.inner.open_read(path).await?;
953 let bytes = handle.read_bytes().await?;
954
955 self.cache_insert(path, full_range, bytes.clone());
957
958 Ok(FileHandle::from_bytes(bytes))
959 }
960
961 async fn read_range(&self, path: &Path, range: Range<u64>) -> io::Result<OwnedBytes> {
962 if let Some(data) = self.try_cache_read(path, range.clone()) {
964 return Ok(data);
965 }
966
967 let data = self.inner.read_range(path, range.clone()).await?;
969
970 self.cache_insert(path, range, data.clone());
972
973 Ok(data)
974 }
975
976 async fn list_files(&self, prefix: &Path) -> io::Result<Vec<PathBuf>> {
977 self.inner.list_files(prefix).await
978 }
979
980 async fn open_lazy(&self, path: &Path) -> io::Result<FileHandle> {
981 let file_size = self.file_size(path).await?;
983
984 let path: Arc<Path> = Arc::from(path);
987 let shared = Arc::clone(&self.shared);
988 let inner = Arc::clone(&self.inner);
989
990 let read_fn: RangeReadFn = Arc::new(move |range: Range<u64>| {
991 let path = Arc::clone(&path);
992 let shared = Arc::clone(&shared);
993 let inner = Arc::clone(&inner);
994
995 Box::pin(async move {
996 if let Some(data) = shared.try_read(&path, range.clone()) {
998 return Ok(data);
999 }
1000
1001 let data = inner.read_range(&path, range.clone()).await?;
1003
1004 shared.insert(&path, range, data.clone());
1006
1007 Ok(data)
1008 })
1009 });
1010
1011 Ok(FileHandle::lazy_labeled(
1012 file_size,
1013 read_fn,
1014 self.shared.label.get(),
1015 ))
1016 }
1017
1018 fn local_path(&self, path: &Path) -> Option<PathBuf> {
1019 self.inner.local_path(path)
1020 }
1021
1022 fn set_index_label(&self, label: &str) {
1023 self.shared.label.set(label);
1024 self.inner.set_index_label(label);
1025 }
1026}
1027
1028#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1031#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1032impl<D: super::DirectoryWriter> super::DirectoryWriter for SliceCachingDirectory<D> {
1033 async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
1034 self.invalidate(path);
1036 self.inner.write(path, data).await
1038 }
1039
1040 async fn delete(&self, path: &Path) -> io::Result<()> {
1041 self.invalidate(path);
1042 self.inner.delete(path).await
1044 }
1045
1046 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
1047 {
1049 let mut state = self.shared.state.write();
1050 state.rename_file(from, to);
1051 }
1052 {
1054 let mut file_sizes = self.file_sizes.write();
1055 if let Some(size) = file_sizes.remove(from) {
1056 file_sizes.insert(to.to_path_buf(), size);
1057 }
1058 }
1059 self.inner.rename(from, to).await
1061 }
1062
1063 async fn link(&self, from: &Path, to: &Path) -> io::Result<()> {
1064 self.inner.link(from, to).await
1067 }
1068
1069 async fn sync(&self) -> io::Result<()> {
1070 self.inner.sync().await
1071 }
1072
1073 async fn streaming_writer(&self, path: &Path) -> io::Result<Box<dyn super::StreamingWriter>> {
1074 self.invalidate(path);
1076 self.inner.streaming_writer(path).await
1077 }
1078}
1079
1080#[cfg(test)]
1081mod tests {
1082 use super::*;
1083 use crate::directories::{DirectoryWriter, RamDirectory};
1084
1085 #[tokio::test]
1086 async fn test_slice_cache_basic() {
1087 let ram = RamDirectory::new();
1088 ram.write(Path::new("test.bin"), &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
1089 .await
1090 .unwrap();
1091
1092 let cached = SliceCachingDirectory::new(ram, 1024);
1093
1094 let data = cached
1096 .read_range(Path::new("test.bin"), 2..5)
1097 .await
1098 .unwrap();
1099 assert_eq!(data.as_slice(), &[2, 3, 4]);
1100
1101 let data = cached
1103 .read_range(Path::new("test.bin"), 2..5)
1104 .await
1105 .unwrap();
1106 assert_eq!(data.as_slice(), &[2, 3, 4]);
1107
1108 let stats = cached.stats();
1109 assert_eq!(stats.total_slices, 1);
1110 assert_eq!(stats.total_bytes, 3);
1111 assert_eq!(stats.hits, 1);
1112 assert_eq!(stats.misses, 1);
1113 }
1114
1115 #[tokio::test]
1116 async fn slice_cache_hits_reuse_the_cached_backing_allocation() {
1117 let ram = RamDirectory::new();
1118 ram.write(Path::new("test.bin"), &[7; 64]).await.unwrap();
1119 let cached = SliceCachingDirectory::new(ram, 64);
1120
1121 let miss = cached
1122 .read_range(Path::new("test.bin"), 8..56)
1123 .await
1124 .unwrap();
1125 let hit = cached
1126 .read_range(Path::new("test.bin"), 8..56)
1127 .await
1128 .unwrap();
1129
1130 assert_eq!(miss.as_slice(), hit.as_slice());
1131 assert_eq!(miss.as_slice().as_ptr(), hit.as_slice().as_ptr());
1132 }
1133
1134 #[tokio::test]
1135 async fn oversized_slice_bypasses_cache_instead_of_exceeding_limit() {
1136 let ram = RamDirectory::new();
1137 ram.write(Path::new("test.bin"), &[3; 32]).await.unwrap();
1138 let cached = SliceCachingDirectory::new(ram, 8);
1139
1140 let data = cached
1141 .read_range(Path::new("test.bin"), 0..32)
1142 .await
1143 .unwrap();
1144 assert_eq!(data.len(), 32);
1145 assert_eq!(cached.stats().total_bytes, 0);
1146 }
1147
1148 #[tokio::test]
1149 async fn test_slice_cache_overlap_merge() {
1150 let ram = RamDirectory::new();
1151 ram.write(Path::new("test.bin"), &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
1152 .await
1153 .unwrap();
1154
1155 let cached = SliceCachingDirectory::new(ram, 1024);
1156
1157 cached
1159 .read_range(Path::new("test.bin"), 2..5)
1160 .await
1161 .unwrap();
1162
1163 cached
1165 .read_range(Path::new("test.bin"), 4..7)
1166 .await
1167 .unwrap();
1168
1169 let stats = cached.stats();
1170 assert_eq!(stats.total_slices, 1);
1172 assert_eq!(stats.total_bytes, 5); let data = cached
1176 .read_range(Path::new("test.bin"), 3..6)
1177 .await
1178 .unwrap();
1179 assert_eq!(data.as_slice(), &[3, 4, 5]);
1180 }
1181
1182 #[tokio::test]
1188 async fn overlap_merge_bridges_multiple_slices_and_keeps_adjacent_ones_apart() {
1189 let ram = RamDirectory::new();
1190 let bytes: Vec<u8> = (0..64).collect();
1191 ram.write(Path::new("test.bin"), &bytes).await.unwrap();
1192 let cached = SliceCachingDirectory::new(ram, 1024);
1193 let path = Path::new("test.bin");
1194
1195 for range in [4..8, 12..16, 20..24, 40..44] {
1197 cached.read_range(path, range).await.unwrap();
1198 }
1199 assert_eq!(cached.stats().total_slices, 4);
1200
1201 cached.read_range(path, 8..12).await.unwrap();
1204 assert_eq!(cached.stats().total_slices, 5);
1205 assert_eq!(cached.stats().total_bytes, 20);
1206
1207 cached.read_range(path, 6..22).await.unwrap();
1210 let stats = cached.stats();
1211 assert_eq!(stats.total_slices, 2);
1212 assert_eq!(stats.total_bytes, 20 + 4);
1213
1214 let misses_before = cached.stats().misses;
1216 let data = cached.read_range(path, 10..14).await.unwrap();
1217 assert_eq!(data.as_slice(), &[10, 11, 12, 13]);
1218 assert_eq!(cached.stats().misses, misses_before);
1219 assert_eq!(cached.stats().total_slices, 2);
1220
1221 let data = cached.read_range(path, 4..24).await.unwrap();
1223 assert_eq!(data.as_slice(), &bytes[4..24]);
1224 let data = cached.read_range(path, 40..44).await.unwrap();
1225 assert_eq!(data.as_slice(), &bytes[40..44]);
1226 }
1227
1228 #[tokio::test]
1229 async fn test_slice_cache_eviction() {
1230 let ram = RamDirectory::new();
1231 ram.write(Path::new("test.bin"), &[0; 100]).await.unwrap();
1232
1233 let cached = SliceCachingDirectory::new(ram, 50);
1235
1236 cached
1238 .read_range(Path::new("test.bin"), 0..30)
1239 .await
1240 .unwrap();
1241
1242 cached
1244 .read_range(Path::new("test.bin"), 50..80)
1245 .await
1246 .unwrap();
1247
1248 let stats = cached.stats();
1249 assert!(stats.total_bytes <= 50);
1250 assert_eq!(stats.evicted_slices, 1);
1251 assert_eq!(stats.evicted_bytes, 30);
1252 }
1253
1254 #[tokio::test]
1258 async fn eviction_is_lru_across_files_after_hits_refresh_recency() {
1259 let ram = RamDirectory::new();
1260 ram.write(Path::new("a.bin"), &[1; 64]).await.unwrap();
1261 ram.write(Path::new("b.bin"), &[2; 64]).await.unwrap();
1262 let cached = SliceCachingDirectory::new(ram, 32);
1263
1264 cached.read_range(Path::new("a.bin"), 0..10).await.unwrap(); cached.read_range(Path::new("b.bin"), 0..10).await.unwrap();
1266 cached.read_range(Path::new("a.bin"), 20..30).await.unwrap();
1267 cached.read_range(Path::new("a.bin"), 2..8).await.unwrap();
1269
1270 cached.read_range(Path::new("a.bin"), 40..50).await.unwrap();
1272 let stats = cached.stats();
1273 assert_eq!(stats.total_bytes, 30);
1274 assert_eq!(stats.evicted_slices, 1);
1275
1276 let misses = cached.stats().misses;
1277 cached.read_range(Path::new("a.bin"), 0..10).await.unwrap();
1278 cached.read_range(Path::new("a.bin"), 20..30).await.unwrap();
1279 assert_eq!(cached.stats().misses, misses, "refreshed slices survived");
1280 cached.read_range(Path::new("b.bin"), 0..10).await.unwrap();
1281 assert_eq!(cached.stats().misses, misses + 1, "LRU slice was evicted");
1282 }
1283
1284 #[tokio::test]
1286 async fn rename_keeps_cached_slices_evictable_and_readable() {
1287 let ram = RamDirectory::new();
1288 ram.write(Path::new("old.bin"), &[9; 64]).await.unwrap();
1289 let cached = SliceCachingDirectory::new(ram, 16);
1290 cached.read_range(Path::new("old.bin"), 0..8).await.unwrap();
1291 cached
1292 .rename(Path::new("old.bin"), Path::new("new.bin"))
1293 .await
1294 .unwrap();
1295
1296 let misses = cached.stats().misses;
1297 let data = cached.read_range(Path::new("new.bin"), 0..8).await.unwrap();
1298 assert_eq!(data.as_slice(), &[9; 8]);
1299 assert_eq!(cached.stats().misses, misses);
1300
1301 cached
1303 .read_range(Path::new("new.bin"), 16..24)
1304 .await
1305 .unwrap();
1306 cached
1307 .read_range(Path::new("new.bin"), 32..40)
1308 .await
1309 .unwrap();
1310 let stats = cached.stats();
1311 assert!(stats.total_bytes <= 16);
1312 assert_eq!(stats.evicted_slices, 1);
1313 }
1314
1315 #[tokio::test]
1316 async fn test_slice_cache_serialize_deserialize() {
1317 let ram = RamDirectory::new();
1318 ram.write(Path::new("file1.bin"), &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
1319 .await
1320 .unwrap();
1321 ram.write(Path::new("file2.bin"), &[10, 11, 12, 13, 14, 15])
1322 .await
1323 .unwrap();
1324
1325 let cached = SliceCachingDirectory::new(ram.clone(), 1024);
1326
1327 cached
1329 .read_range(Path::new("file1.bin"), 2..6)
1330 .await
1331 .unwrap();
1332 cached
1333 .read_range(Path::new("file2.bin"), 1..4)
1334 .await
1335 .unwrap();
1336
1337 let stats = cached.stats();
1338 assert_eq!(stats.files_cached, 2);
1339 assert_eq!(stats.total_bytes, 7); let serialized = cached.serialize();
1343 assert!(!serialized.is_empty());
1344
1345 let cached2 = SliceCachingDirectory::new(ram.clone(), 1024);
1347 assert!(cached2.is_empty());
1348
1349 cached2.deserialize(&serialized).unwrap();
1350
1351 let stats2 = cached2.stats();
1352 assert_eq!(stats2.files_cached, 2);
1353 assert_eq!(stats2.total_bytes, 7);
1354
1355 let data = cached2
1357 .read_range(Path::new("file1.bin"), 2..6)
1358 .await
1359 .unwrap();
1360 assert_eq!(data.as_slice(), &[2, 3, 4, 5]);
1361
1362 let data = cached2
1363 .read_range(Path::new("file2.bin"), 1..4)
1364 .await
1365 .unwrap();
1366 assert_eq!(data.as_slice(), &[11, 12, 13]);
1367 assert_eq!(cached2.stats().misses, 0);
1368 }
1369
1370 #[tokio::test]
1371 async fn test_slice_cache_serialize_empty() {
1372 let ram = RamDirectory::new();
1373 let cached = SliceCachingDirectory::new(ram, 1024);
1374
1375 let serialized = cached.serialize();
1377 assert!(!serialized.is_empty()); let cached2 = SliceCachingDirectory::new(RamDirectory::new(), 1024);
1381 cached2.deserialize(&serialized).unwrap();
1382 assert!(cached2.is_empty());
1383 }
1384
1385 #[tokio::test]
1386 async fn deserialization_enforces_the_destination_cache_limit() {
1387 let ram = RamDirectory::new();
1388 ram.write(Path::new("test.bin"), &[1; 64]).await.unwrap();
1389 let source = SliceCachingDirectory::new(ram.clone(), 64);
1390 source
1391 .read_range(Path::new("test.bin"), 0..64)
1392 .await
1393 .unwrap();
1394
1395 let destination = SliceCachingDirectory::new(ram, 8);
1396 destination.deserialize(&source.serialize()).unwrap();
1397 assert!(destination.stats().total_bytes <= 8);
1398 }
1399
1400 #[tokio::test]
1403 async fn lazy_handle_reads_hit_the_shared_cache() {
1404 let ram = RamDirectory::new();
1405 ram.write(Path::new("test.bin"), &[4; 128]).await.unwrap();
1406 let cached = SliceCachingDirectory::new(ram, 1024);
1407
1408 cached
1409 .read_range(Path::new("test.bin"), 0..64)
1410 .await
1411 .unwrap();
1412 let handle = cached.open_lazy(Path::new("test.bin")).await.unwrap();
1413 let data = handle.read_bytes_range(8..40).await.unwrap();
1414 assert_eq!(data.len(), 32);
1415 assert_eq!(cached.stats().hits, 1);
1416 assert_eq!(cached.stats().misses, 1);
1417
1418 let data = handle.read_bytes_range(64..128).await.unwrap();
1419 assert_eq!(data.len(), 64);
1420 assert_eq!(cached.stats().misses, 2);
1421 assert_eq!(cached.stats().total_bytes, 128);
1422 }
1423
1424 #[tokio::test]
1427 async fn concurrent_lazy_handle_hits_are_counted_exactly() {
1428 const THREADS: usize = 8;
1429 const HITS_PER_THREAD: usize = 250;
1430
1431 let ram = RamDirectory::new();
1432 ram.write(Path::new("test.bin"), &[7; 4096]).await.unwrap();
1433 let cached = SliceCachingDirectory::new(ram, 4096);
1434 cached
1435 .read_range(Path::new("test.bin"), 0..4096)
1436 .await
1437 .unwrap();
1438 let handle = cached.open_lazy(Path::new("test.bin")).await.unwrap();
1439
1440 std::thread::scope(|scope| {
1441 for thread in 0..THREADS {
1442 let handle = &handle;
1443 scope.spawn(move || {
1444 for hit in 0..HITS_PER_THREAD {
1445 let start = ((thread * 31 + hit * 7) % (4096 - 64)) as u64;
1446 let bytes =
1447 futures::executor::block_on(handle.read_bytes_range(start..start + 64))
1448 .unwrap();
1449 assert_eq!(bytes.len(), 64);
1450 }
1451 });
1452 }
1453 });
1454
1455 assert_eq!(cached.stats().hits, (THREADS * HITS_PER_THREAD) as u64);
1456 assert_eq!(cached.stats().misses, 1);
1457 }
1458}