use rustc_hash::FxHasher;
use std::hash::{Hash, Hasher};
use std::sync::RwLock;
const SHARDS: usize = 64;
const MAX_KEY: usize = 16;
const MAX_IDS: usize = MAX_KEY;
const LONG_SHARE: usize = 4;
#[derive(Clone, Copy)]
struct Slot {
key: [u8; MAX_KEY],
ids: [u32; MAX_IDS],
klen: u8,
nids: u8,
}
const EMPTY: u8 = 0;
const VACANT: Slot = Slot {
key: [0; MAX_KEY],
ids: [0; MAX_IDS],
klen: EMPTY,
nids: 0,
};
#[derive(Clone, Default)]
struct LongSlot {
key: Box<[u8]>,
ids: Box<[u32]>,
}
struct Tiers {
inline: Box<[Slot]>,
long: Box<[LongSlot]>,
}
type Shard = RwLock<Tiers>;
pub(crate) struct ChunkCache {
shards: Box<[Shard]>,
mask: usize,
long_mask: usize,
}
impl ChunkCache {
pub(crate) fn new(capacity: usize) -> Self {
let per_shard = (capacity / SHARDS).next_power_of_two().max(1);
let long_per_shard = (per_shard / LONG_SHARE).next_power_of_two().max(1);
Self {
shards: (0..SHARDS)
.map(|_| {
RwLock::new(Tiers {
inline: vec![VACANT; per_shard].into_boxed_slice(),
long: vec![LongSlot::default(); long_per_shard].into_boxed_slice(),
})
})
.collect(),
mask: per_shard - 1,
long_mask: long_per_shard - 1,
}
}
pub(crate) fn shard_hash(key: &[u8]) -> u64 {
let mut hasher = FxHasher::default();
key.hash(&mut hasher);
hasher.finish()
}
#[inline]
fn locate(&self, hash: u64, key_len: usize) -> (&Shard, usize) {
let mask = if key_len > MAX_KEY {
self.long_mask
} else {
self.mask
};
(
&self.shards[(hash >> 32) as usize % SHARDS],
hash as usize & mask,
)
}
pub(crate) fn extend_into(&self, hash: u64, key: &[u8], out: &mut Vec<u32>) -> bool {
let (shard, index) = self.locate(hash, key.len());
let Ok(tiers) = shard.read() else {
return false;
};
if key.len() > MAX_KEY {
let slot = &tiers.long[index];
if &*slot.key != key {
return false;
}
out.extend_from_slice(&slot.ids);
return true;
}
let slot = &tiers.inline[index];
if slot.klen as usize != key.len() || &slot.key[..key.len()] != key {
return false;
}
out.extend_from_slice(&slot.ids[..slot.nids as usize]);
true
}
pub(crate) fn put(&self, hash: u64, key: &[u8], ids: &[u32]) {
if key.is_empty() {
return;
}
let (shard, index) = self.locate(hash, key.len());
let Ok(mut tiers) = shard.write() else {
return;
};
if key.len() > MAX_KEY {
tiers.long[index] = LongSlot {
key: key.into(),
ids: ids.into(),
};
return;
}
debug_assert!(ids.len() <= MAX_IDS, "ids outnumber the chunk's bytes");
let slot = &mut tiers.inline[index];
slot.key[..key.len()].copy_from_slice(key);
slot.ids[..ids.len()].copy_from_slice(ids);
slot.klen = key.len() as u8;
slot.nids = ids.len() as u8;
}
pub(crate) fn clear(&self) {
for shard in &self.shards {
if let Ok(mut tiers) = shard.write() {
tiers.inline.fill(VACANT);
tiers.long.fill_with(LongSlot::default);
}
}
}
pub(crate) fn len(&self) -> usize {
self.shards
.iter()
.map(|shard| {
shard
.read()
.map(|tiers| {
tiers.inline.iter().filter(|s| s.klen != EMPTY).count()
+ tiers.long.iter().filter(|s| !s.key.is_empty()).count()
})
.unwrap_or(0)
})
.sum()
}
}
impl std::fmt::Debug for ChunkCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChunkCache")
.field("shards", &SHARDS)
.field("len", &self.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(cache: &ChunkCache, key: &[u8], ids: &[u32]) -> Option<Vec<u32>> {
let hash = ChunkCache::shard_hash(key);
cache.put(hash, key, ids);
let mut out = Vec::new();
cache.extend_into(hash, key, &mut out).then_some(out)
}
#[test]
fn a_chunk_too_long_to_inline_is_still_cached() {
let cache = ChunkCache::new(1024);
let key = "那么,线性代数又是如何来解决这些问题的呢".as_bytes();
assert!(key.len() > MAX_KEY, "test key must exercise the boxed tier");
assert_eq!(
roundtrip(&cache, key, &[1, 2, 3, 4, 5]).as_deref(),
Some(&[1, 2, 3, 4, 5][..])
);
}
#[test]
fn a_short_chunk_round_trips_through_the_inline_tier() {
let cache = ChunkCache::new(1024);
assert_eq!(
roundtrip(&cache, b"hello", &[7, 8]).as_deref(),
Some(&[7, 8][..])
);
}
#[test]
fn clear_empties_both_tiers() {
let cache = ChunkCache::new(1024);
roundtrip(&cache, b"short", &[1]);
roundtrip(&cache, "那么,线性代数又是如何来解决".as_bytes(), &[2]);
assert_eq!(cache.len(), 2);
cache.clear();
assert_eq!(cache.len(), 0);
}
#[test]
fn a_different_key_is_a_miss() {
let cache = ChunkCache::new(1024);
roundtrip(&cache, b"alpha", &[1]);
let mut out = Vec::new();
assert!(!cache.extend_into(ChunkCache::shard_hash(b"beta"), b"beta", &mut out));
let long = "这是一个很长的中文词组用来测试".as_bytes();
roundtrip(&cache, long, &[9]);
let other = "另一个完全不同的中文词组测试内容".as_bytes();
assert!(!cache.extend_into(ChunkCache::shard_hash(other), other, &mut out));
assert!(out.is_empty());
}
}