use bytes::Bytes;
use lru::LruCache;
use std::num::NonZeroUsize;
pub const DEFAULT_TREE_NODE_CACHE_BYTES: usize = 64 * 1024 * 1024;
pub struct TreeNodeCache {
entries: LruCache<[u8; 32], Bytes>,
bytes: usize,
cap_bytes: usize,
hits: u64,
misses: u64,
}
impl std::fmt::Debug for TreeNodeCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TreeNodeCache")
.field("len", &self.entries.len())
.field("bytes", &self.bytes)
.field("cap_bytes", &self.cap_bytes)
.field("hits", &self.hits)
.field("misses", &self.misses)
.finish()
}
}
impl TreeNodeCache {
pub fn new() -> Self {
Self::with_capacity_bytes(DEFAULT_TREE_NODE_CACHE_BYTES)
}
pub fn with_capacity_bytes(cap_bytes: usize) -> Self {
Self {
entries: LruCache::unbounded(),
bytes: 0,
cap_bytes,
hits: 0,
misses: 0,
}
}
pub fn get(&mut self, hash: &[u8; 32]) -> Option<Bytes> {
if self.cap_bytes == 0 {
self.misses = self.misses.saturating_add(1);
return None;
}
match self.entries.get(hash) {
Some(bytes) => {
self.hits = self.hits.saturating_add(1);
Some(bytes.clone())
}
None => {
self.misses = self.misses.saturating_add(1);
None
}
}
}
pub(crate) fn insert(&mut self, hash: [u8; 32], bytes: Bytes) {
if self.cap_bytes == 0 || bytes.len() > self.cap_bytes {
return;
}
let new_len = bytes.len();
if let Some(prev) = self.entries.put(hash, bytes) {
self.bytes = self.bytes.saturating_sub(prev.len());
}
self.bytes = self.bytes.saturating_add(new_len);
while self.bytes > self.cap_bytes {
match self.entries.pop_lru() {
Some((_, evicted)) => {
self.bytes = self.bytes.saturating_sub(evicted.len());
}
None => break, }
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn bytes(&self) -> usize {
self.bytes
}
pub fn cap_bytes(&self) -> usize {
self.cap_bytes
}
pub fn hits(&self) -> u64 {
self.hits
}
pub fn misses(&self) -> u64 {
self.misses
}
pub fn hit_ratio(&self) -> f64 {
let total = self.hits.saturating_add(self.misses);
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.bytes = 0;
self.hits = 0;
self.misses = 0;
}
pub fn remove(&mut self, hash: &[u8; 32]) {
if let Some(bytes) = self.entries.pop(hash) {
self.bytes = self.bytes.saturating_sub(bytes.len());
}
}
}
impl Default for TreeNodeCache {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
fn _force_import_nonzero(_: NonZeroUsize) {}
#[cfg(test)]
mod tests {
use super::*;
fn h(byte: u8) -> [u8; 32] {
[byte; 32]
}
#[test]
fn empty_cache_misses() {
let mut c = TreeNodeCache::new();
assert!(c.get(&h(1)).is_none());
assert_eq!(c.hits(), 0);
assert_eq!(c.misses(), 1);
}
#[test]
fn insert_and_get_round_trip() {
let mut c = TreeNodeCache::with_capacity_bytes(1024);
c.insert(h(1), Bytes::from(vec![1, 2, 3]));
let got = c.get(&h(1)).unwrap();
assert_eq!(got.as_ref(), &[1u8, 2, 3]);
assert_eq!(c.hits(), 1);
assert_eq!(c.misses(), 0);
assert_eq!(c.len(), 1);
assert_eq!(c.bytes(), 3);
}
#[test]
fn cap_zero_disables_cache() {
let mut c = TreeNodeCache::with_capacity_bytes(0);
c.insert(h(1), Bytes::from(vec![1, 2, 3]));
assert!(c.get(&h(1)).is_none());
assert_eq!(c.len(), 0);
}
#[test]
fn oversize_single_entry_rejected() {
let mut c = TreeNodeCache::with_capacity_bytes(2);
c.insert(h(1), Bytes::from(vec![1, 2, 3])); assert!(c.get(&h(1)).is_none());
assert_eq!(c.len(), 0);
}
#[test]
fn lru_evicts_least_recently_used() {
let mut c = TreeNodeCache::with_capacity_bytes(10);
c.insert(h(1), Bytes::from(vec![0; 4]));
c.insert(h(2), Bytes::from(vec![0; 4]));
let _ = c.get(&h(1));
c.insert(h(3), Bytes::from(vec![0; 4]));
assert!(c.get(&h(1)).is_some(), "h(1) was touched, stays cached");
assert!(c.get(&h(2)).is_none(), "h(2) was LRU, evicted");
assert!(c.get(&h(3)).is_some());
}
#[test]
fn re_insert_replaces_value_without_double_counting() {
let mut c = TreeNodeCache::with_capacity_bytes(100);
c.insert(h(1), Bytes::from(vec![0; 10]));
c.insert(h(1), Bytes::from(vec![0; 20]));
assert_eq!(c.len(), 1);
assert_eq!(c.bytes(), 20);
}
#[test]
fn clear_resets_state() {
let mut c = TreeNodeCache::with_capacity_bytes(100);
c.insert(h(1), Bytes::from(vec![0; 10]));
let _ = c.get(&h(1));
c.clear();
assert_eq!(c.len(), 0);
assert_eq!(c.bytes(), 0);
assert_eq!(c.hits(), 0);
assert_eq!(c.misses(), 0);
}
#[test]
fn remove_drops_one_entry_only() {
let mut c = TreeNodeCache::with_capacity_bytes(100);
c.insert(h(1), Bytes::from(vec![0; 10]));
c.insert(h(2), Bytes::from(vec![0; 20]));
c.remove(&h(1));
assert!(c.get(&h(1)).is_none());
assert!(c.get(&h(2)).is_some());
assert_eq!(c.bytes(), 20);
}
#[test]
fn remove_unknown_hash_is_noop() {
let mut c = TreeNodeCache::with_capacity_bytes(100);
c.insert(h(1), Bytes::from(vec![0; 10]));
c.remove(&h(99));
assert_eq!(c.len(), 1);
assert_eq!(c.bytes(), 10);
}
#[test]
fn hit_ratio_reflects_ratio() {
let mut c = TreeNodeCache::with_capacity_bytes(100);
c.insert(h(1), Bytes::from(vec![0; 5]));
let _ = c.get(&h(1)); let _ = c.get(&h(2)); let _ = c.get(&h(1)); assert!((c.hit_ratio() - (2.0 / 3.0)).abs() < 1e-9);
}
}