#[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;
#[cfg(target_arch = "aarch64")]
pub const CACHE_LINE_SIZE: usize = 128;
#[cfg(target_arch = "x86_64")]
pub const CACHE_LINE_SIZE: usize = 64;
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
pub const CACHE_LINE_SIZE: usize = 64;
#[inline]
pub fn align_to_cache_line(size: usize) -> usize {
(size + CACHE_LINE_SIZE - 1) & !(CACHE_LINE_SIZE - 1)
}
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,
}
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,
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,
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
}
fn accepts_decompressed_len(&self, data_len: usize) -> bool {
let inner = self.inner.lock().unwrap();
inner.max_bytes != 0 && inner.max_slots != 0 && data_len <= inner.max_bytes
}
pub fn put_decompressed(&self, coord: ChunkCoord, data: Vec<u8>) {
let mut inner = self.inner.lock().unwrap();
let data_len = data.len();
if inner.max_bytes == 0 || inner.max_slots == 0 || data_len > inner.max_bytes {
return;
}
inner.tick += 1;
let tick = inner.tick;
for slot in inner.slots.iter_mut() {
if slot.coord == coord {
slot.last_access = tick;
return; }
}
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()
.min_by_key(|(_, s)| s.last_access)
.map(|(i, _)| i)
.unwrap();
let removed = inner.slots.swap_remove(lru_idx);
inner.current_bytes -= removed.data.len();
}
inner.current_bytes += data_len;
inner.slots.push(CachedChunk {
coord,
data,
last_access: tick,
});
}
pub fn put_decompressed_slice(&self, coord: ChunkCoord, data: &[u8]) {
if !self.accepts_decompressed_len(data.len()) {
return;
}
self.put_decompressed(coord, data.to_vec());
}
#[cfg(test)]
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(vec![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(vec![0], vec![1; 10]);
cache.put_decompressed(vec![1], vec![2; 10]);
assert_eq!(cache.stats().cached_chunks(), 2);
get_decompressed(&cache, &[0]);
cache.put_decompressed(vec![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(vec![0], vec![0; 20]);
cache.put_decompressed(vec![1], vec![0; 20]);
assert_eq!(cache.stats().cached_bytes(), 40);
cache.put_decompressed(vec![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(vec![0], &[1, 2, 3]);
assert_eq!(cache.stats().cached_chunks(), 0);
let cache = ChunkCache::with_capacity(1024, 16);
cache.put_decompressed_slice(vec![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(vec![0], &[1, 2, 3, 4]);
assert_eq!(cache.stats().cached_chunks(), 0);
}
#[test]
fn oversized_chunk_not_cached() {
let cache = ChunkCache::with_capacity(10, 16);
cache.put_decompressed(vec![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(vec![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(vec![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(vec![0], vec![1, 2, 3]);
cache.put_decompressed(vec![0], vec![1, 2, 3]); assert_eq!(cache.stats().cached_chunks(), 1);
assert_eq!(cache.stats().cached_bytes(), 3);
}
#[test]
fn align_to_cache_line_values() {
assert_eq!(align_to_cache_line(0), 0);
assert_eq!(align_to_cache_line(1), CACHE_LINE_SIZE);
assert_eq!(align_to_cache_line(CACHE_LINE_SIZE), CACHE_LINE_SIZE);
assert_eq!(
align_to_cache_line(CACHE_LINE_SIZE + 1),
CACHE_LINE_SIZE * 2
);
}
}