#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
use crate::nosync::Mutex;
#[cfg(feature = "std")]
use std::sync::Mutex;
#[cfg(not(feature = "std"))]
use alloc::collections::BTreeMap;
#[cfg(feature = "std")]
use std::collections::HashMap;
use crate::chunked_read::ChunkInfo;
pub type ChunkCoord = Vec<u64>;
pub const DEFAULT_CACHE_BYTES: usize = 1024 * 1024;
pub const DEFAULT_MAX_SLOTS: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChunkCacheConfig {
max_bytes: usize,
max_slots: usize,
cache_index: bool,
}
impl ChunkCacheConfig {
pub const fn new() -> Self {
Self {
max_bytes: DEFAULT_CACHE_BYTES,
max_slots: DEFAULT_MAX_SLOTS,
cache_index: true,
}
}
pub const fn from_h5p_cache(rdcc_nslots: usize, rdcc_nbytes: usize) -> Self {
Self {
max_bytes: rdcc_nbytes,
max_slots: rdcc_nslots,
cache_index: true,
}
}
pub const fn disabled() -> Self {
Self {
max_bytes: 0,
max_slots: 0,
cache_index: false,
}
}
pub const fn with_max_bytes(mut self, max_bytes: usize) -> Self {
self.max_bytes = max_bytes;
self
}
pub const fn with_max_slots(mut self, max_slots: usize) -> Self {
self.max_slots = max_slots;
self
}
pub const fn with_index_cache(mut self, enabled: bool) -> Self {
self.cache_index = enabled;
self
}
pub const fn max_bytes(&self) -> usize {
self.max_bytes
}
pub const fn max_slots(&self) -> usize {
self.max_slots
}
pub const fn index_cache_enabled(&self) -> bool {
self.cache_index
}
}
impl Default for ChunkCacheConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ChunkCacheStats {
index_loaded: bool,
cached_chunks: usize,
cached_bytes: usize,
}
impl ChunkCacheStats {
pub const fn index_loaded(&self) -> bool {
self.index_loaded
}
pub const fn cached_chunks(&self) -> usize {
self.cached_chunks
}
pub const fn cached_bytes(&self) -> usize {
self.cached_bytes
}
}
struct CachedChunk {
coord: ChunkCoord,
data: Vec<u8>,
last_access: u64,
stored_by: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CachePass(u64);
impl CachePass {
pub const LRU: CachePass = CachePass(0);
}
pub struct ChunkCache {
inner: Mutex<CacheInner>,
}
struct CacheInner {
#[cfg(feature = "std")]
index: Option<HashMap<ChunkCoord, ChunkInfo>>,
#[cfg(not(feature = "std"))]
index: Option<BTreeMap<ChunkCoord, ChunkInfo>>,
slots: Vec<CachedChunk>,
current_bytes: usize,
max_bytes: usize,
max_slots: usize,
tick: u64,
pass: u64,
cache_index: bool,
}
impl ChunkCache {
pub fn new() -> Self {
Self::with_capacity(DEFAULT_CACHE_BYTES, DEFAULT_MAX_SLOTS)
}
pub fn with_capacity(max_bytes: usize, max_slots: usize) -> Self {
Self::with_config(
ChunkCacheConfig::new()
.with_max_bytes(max_bytes)
.with_max_slots(max_slots),
)
}
pub fn with_config(config: ChunkCacheConfig) -> Self {
Self {
inner: Mutex::new(CacheInner {
index: None,
slots: Vec::with_capacity(config.max_slots.min(64)),
current_bytes: 0,
max_bytes: config.max_bytes,
max_slots: config.max_slots,
tick: 0,
pass: 0,
cache_index: config.cache_index,
}),
}
}
pub fn stats(&self) -> ChunkCacheStats {
let inner = self.inner.lock().unwrap();
ChunkCacheStats {
index_loaded: inner.index.is_some(),
cached_chunks: inner.slots.len(),
cached_bytes: inner.current_bytes,
}
}
pub fn populate_index(&self, chunks: &[ChunkInfo], rank: usize) {
let mut inner = self.inner.lock().unwrap();
if !inner.cache_index {
return;
}
if inner.index.is_some() {
return; }
#[cfg(feature = "std")]
let mut map = HashMap::with_capacity(chunks.len());
#[cfg(not(feature = "std"))]
let mut map = BTreeMap::new();
for ci in chunks {
let coord: ChunkCoord = ci.offsets.iter().take(rank).copied().collect();
map.insert(coord, ci.clone());
}
inner.index = Some(map);
}
pub fn all_indexed_chunks(&self) -> Option<Vec<ChunkInfo>> {
let inner = self.inner.lock().unwrap();
inner.index.as_ref().map(|m| m.values().cloned().collect())
}
pub fn with_decompressed<R>(&self, coord: &[u64], f: impl FnOnce(&[u8]) -> R) -> Option<R> {
let mut inner = self.inner.lock().unwrap();
inner.tick += 1;
let tick = inner.tick;
for slot in inner.slots.iter_mut() {
if slot.coord.as_slice() == coord {
slot.last_access = tick;
return Some(f(&slot.data));
}
}
None
}
pub fn begin_pass(&self) -> CachePass {
let mut inner = self.inner.lock().unwrap();
inner.pass += 1;
CachePass(inner.pass)
}
fn reserve(inner: &mut CacheInner, pass: CachePass, coord: &[u64], data_len: usize) -> bool {
if inner.max_bytes == 0 || inner.max_slots == 0 || data_len > inner.max_bytes {
return false;
}
inner.tick += 1;
let tick = inner.tick;
for slot in inner.slots.iter_mut() {
if slot.coord == coord {
slot.last_access = tick;
return false; }
}
let evicts_its_own = pass == CachePass::LRU;
let reclaimable = |slot: &&CachedChunk| evicts_its_own || slot.stored_by != pass.0;
let (freed_slots, freed_bytes) = inner
.slots
.iter()
.filter(reclaimable)
.fold((0usize, 0usize), |(n, b), slot| {
(n + 1, b + slot.data.len())
});
let (least_slots, least_bytes) = (
inner.slots.len() - freed_slots,
inner.current_bytes - freed_bytes,
);
if least_slots >= inner.max_slots
|| (least_bytes + data_len > inner.max_bytes && least_slots > 0)
{
return false;
}
while inner.slots.len() >= inner.max_slots
|| (inner.current_bytes + data_len > inner.max_bytes && !inner.slots.is_empty())
{
let lru_idx = inner
.slots
.iter()
.enumerate()
.filter(|(_, s)| reclaimable(s))
.min_by_key(|(_, s)| s.last_access)
.map(|(i, _)| i);
let Some(lru_idx) = lru_idx else {
debug_assert!(
false,
"the feasibility check above admitted a chunk this pass cannot make room for"
);
return false;
};
let removed = inner.slots.swap_remove(lru_idx);
inner.current_bytes -= removed.data.len();
}
inner.current_bytes += data_len;
true
}
pub fn put_decompressed(&self, pass: CachePass, coord: &[u64], data: Vec<u8>) {
let mut inner = self.inner.lock().unwrap();
if !Self::reserve(&mut inner, pass, coord, data.len()) {
return;
}
let last_access = inner.tick;
inner.slots.push(CachedChunk {
coord: coord.to_vec(),
data,
last_access,
stored_by: pass.0,
});
}
pub fn decompressed_coords(&self) -> Vec<ChunkCoord> {
let inner = self.inner.lock().unwrap();
inner.slots.iter().map(|s| s.coord.clone()).collect()
}
pub fn put_decompressed_slice(&self, pass: CachePass, coord: &[u64], data: &[u8]) {
let mut inner = self.inner.lock().unwrap();
if !Self::reserve(&mut inner, pass, coord, data.len()) {
return;
}
let last_access = inner.tick;
inner.slots.push(CachedChunk {
coord: coord.to_vec(),
data: data.to_vec(),
last_access,
stored_by: pass.0,
});
}
pub fn clear(&self) {
let mut inner = self.inner.lock().unwrap();
inner.index = None;
inner.slots.clear();
inner.current_bytes = 0;
inner.tick = 0;
}
}
impl Default for ChunkCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_chunk(offsets: Vec<u64>, address: u64, size: u32) -> ChunkInfo {
ChunkInfo {
chunk_size: size,
filter_mask: 0,
offsets,
address,
}
}
#[test]
fn index_populate_and_lookup() {
let cache = ChunkCache::new();
let chunks = vec![
make_chunk(vec![0, 0, 0], 0x1000, 80),
make_chunk(vec![10, 0, 0], 0x2000, 80),
];
cache.populate_index(&chunks, 2); assert!(cache.stats().index_loaded());
let mut addrs: Vec<u64> = cache
.all_indexed_chunks()
.unwrap()
.iter()
.map(|c| c.address)
.collect();
addrs.sort_unstable();
assert_eq!(addrs, vec![0x1000, 0x2000]);
}
fn get_decompressed(cache: &ChunkCache, coord: &[u64]) -> Option<Vec<u8>> {
cache.with_decompressed(coord, <[u8]>::to_vec)
}
#[test]
fn decompressed_cache_hit() {
let cache = ChunkCache::new();
cache.put_decompressed(cache.begin_pass(), &[0, 0], vec![1, 2, 3, 4]);
let got = get_decompressed(&cache, &[0, 0]).unwrap();
assert_eq!(got, vec![1, 2, 3, 4]);
}
#[test]
fn lru_eviction_by_slots() {
let cache = ChunkCache::with_capacity(1024 * 1024, 2);
cache.put_decompressed(cache.begin_pass(), &[0], vec![1; 10]);
cache.put_decompressed(cache.begin_pass(), &[1], vec![2; 10]);
assert_eq!(cache.stats().cached_chunks(), 2);
get_decompressed(&cache, &[0]);
cache.put_decompressed(cache.begin_pass(), &[2], vec![3; 10]);
assert_eq!(cache.stats().cached_chunks(), 2);
assert!(get_decompressed(&cache, &[0]).is_some());
assert!(get_decompressed(&cache, &[1]).is_none()); assert!(get_decompressed(&cache, &[2]).is_some());
}
#[test]
fn lru_eviction_by_bytes() {
let cache = ChunkCache::with_capacity(50, 100);
cache.put_decompressed(cache.begin_pass(), &[0], vec![0; 20]);
cache.put_decompressed(cache.begin_pass(), &[1], vec![0; 20]);
assert_eq!(cache.stats().cached_bytes(), 40);
cache.put_decompressed(cache.begin_pass(), &[2], vec![0; 20]);
assert!(cache.stats().cached_bytes() <= 50);
assert!(get_decompressed(&cache, &[0]).is_none()); }
#[test]
fn put_decompressed_slice_only_copies_when_admitted() {
let cache = ChunkCache::with_config(ChunkCacheConfig::disabled());
cache.put_decompressed_slice(cache.begin_pass(), &[0], &[1, 2, 3]);
assert_eq!(cache.stats().cached_chunks(), 0);
let cache = ChunkCache::with_capacity(1024, 16);
cache.put_decompressed_slice(cache.begin_pass(), &[0], &[1, 2, 3, 4]);
assert_eq!(get_decompressed(&cache, &[0]).unwrap(), vec![1, 2, 3, 4]);
let cache = ChunkCache::with_capacity(2, 16);
cache.put_decompressed_slice(cache.begin_pass(), &[0], &[1, 2, 3, 4]);
assert_eq!(cache.stats().cached_chunks(), 0);
}
#[test]
fn a_pass_fills_the_cache_and_then_stops_evicting_itself() {
let cache = ChunkCache::with_capacity(1024 * 1024, 2);
let pass = cache.begin_pass();
for c in 0..4u64 {
cache.put_decompressed(pass, &[c], vec![c as u8; 10]);
}
assert_eq!(cache.stats().cached_chunks(), 2);
assert!(get_decompressed(&cache, &[0]).is_some());
assert!(get_decompressed(&cache, &[1]).is_some());
assert!(get_decompressed(&cache, &[2]).is_none());
assert!(get_decompressed(&cache, &[3]).is_none());
let next = cache.begin_pass();
cache.put_decompressed(next, &[9], vec![9; 10]);
cache.put_decompressed(next, &[8], vec![8; 10]);
assert_eq!(cache.stats().cached_chunks(), 2);
assert!(get_decompressed(&cache, &[9]).is_some());
assert!(get_decompressed(&cache, &[8]).is_some());
}
#[test]
fn a_pass_marked_lru_evicts_its_own_chunks() {
let cache = ChunkCache::with_capacity(1024 * 1024, 2);
for c in 0..4u64 {
cache.put_decompressed(CachePass::LRU, &[c], vec![c as u8; 10]);
}
assert_eq!(cache.stats().cached_chunks(), 2);
assert!(get_decompressed(&cache, &[2]).is_some());
assert!(get_decompressed(&cache, &[3]).is_some());
assert!(get_decompressed(&cache, &[0]).is_none());
assert_ne!(cache.begin_pass(), CachePass::LRU);
}
#[test]
fn a_pass_that_cannot_make_room_evicts_nothing() {
let cache = ChunkCache::with_capacity(100, 16);
let first = cache.begin_pass();
cache.put_decompressed(first, &[0], vec![0; 10]);
let second = cache.begin_pass();
cache.put_decompressed(second, &[1], vec![1; 80]);
assert_eq!(cache.stats().cached_chunks(), 2);
cache.put_decompressed(second, &[2], vec![2; 80]);
assert_eq!(cache.stats().cached_chunks(), 2);
assert_eq!(cache.stats().cached_bytes(), 90);
assert!(get_decompressed(&cache, &[0]).is_some());
}
#[test]
fn a_full_pass_does_not_copy_the_chunk_it_cannot_store() {
let cache = ChunkCache::with_capacity(1024 * 1024, 1);
let pass = cache.begin_pass();
cache.put_decompressed_slice(pass, &[0], &[1; 10]);
cache.put_decompressed_slice(pass, &[1], &[2; 10]);
assert_eq!(cache.stats().cached_chunks(), 1);
assert_eq!(cache.stats().cached_bytes(), 10);
assert!(get_decompressed(&cache, &[0]).is_some());
}
#[test]
fn oversized_chunk_not_cached() {
let cache = ChunkCache::with_capacity(10, 16);
cache.put_decompressed(cache.begin_pass(), &[0], vec![0; 100]); assert_eq!(cache.stats().cached_chunks(), 0);
}
#[test]
fn disabled_cache_retains_no_index_or_chunks() {
let cache = ChunkCache::with_config(ChunkCacheConfig::disabled());
let chunks = vec![make_chunk(vec![0, 0], 0x1000, 80)];
cache.populate_index(&chunks, 1);
assert!(!cache.stats().index_loaded());
cache.put_decompressed(cache.begin_pass(), &[0], vec![1, 2, 3]);
assert_eq!(cache.stats().cached_chunks(), 0);
assert_eq!(cache.stats().cached_bytes(), 0);
}
#[test]
fn h5p_cache_constructor_maps_raw_data_chunk_settings() {
let config = ChunkCacheConfig::from_h5p_cache(521, 2 * 1024 * 1024);
assert_eq!(config.max_slots(), 521);
assert_eq!(config.max_bytes(), 2 * 1024 * 1024);
assert!(config.index_cache_enabled());
}
#[test]
fn clear_resets_everything() {
let cache = ChunkCache::new();
let chunks = vec![make_chunk(vec![0, 0], 0x1000, 80)];
cache.populate_index(&chunks, 1);
cache.put_decompressed(cache.begin_pass(), &[0], vec![1, 2, 3]);
cache.clear();
assert!(!cache.stats().index_loaded());
assert_eq!(cache.stats().cached_chunks(), 0);
assert_eq!(cache.stats().cached_bytes(), 0);
}
#[test]
fn duplicate_insert_is_noop() {
let cache = ChunkCache::new();
cache.put_decompressed(cache.begin_pass(), &[0], vec![1, 2, 3]);
cache.put_decompressed(cache.begin_pass(), &[0], vec![1, 2, 3]); assert_eq!(cache.stats().cached_chunks(), 1);
assert_eq!(cache.stats().cached_bytes(), 3);
}
}