use std::num::NonZeroUsize;
use std::sync::Arc;
use lru::LruCache;
use super::super::InactivePoolBackend;
use super::{Block, BlockMetadata, Registered, SequenceHash};
use crate::tinylfu::FrequencyTracker;
use anyhow::{Result, bail};
pub struct MultiLruBackend<T: BlockMetadata> {
priority_pools: [LruCache<SequenceHash, Block<T, Registered>>; 4],
frequency_tracker: Arc<dyn FrequencyTracker<u128>>,
frequency_thresholds: [u8; 3],
}
impl<T: BlockMetadata> MultiLruBackend<T> {
pub fn new_with_thresholds(
block_count: NonZeroUsize,
thresholds: &[u8; 3],
frequency_tracker: Arc<dyn FrequencyTracker<u128>>,
) -> Result<Self> {
if !(thresholds[0] < thresholds[1] && thresholds[1] < thresholds[2]) {
bail!("Thresholds must be in ascending order: {:?}", thresholds);
}
if thresholds[2] > 15 {
bail!(
"Maximum threshold cannot exceed 15 (4-bit counter limit), got: {:?}",
thresholds
);
}
if thresholds[0] < 1 {
bail!(
"Cold threshold must be >= 1 to distinguish from never-accessed blocks, got: {:?}",
thresholds
);
}
Ok(Self {
priority_pools: [
LruCache::new(block_count),
LruCache::new(block_count),
LruCache::new(block_count),
LruCache::new(block_count),
],
frequency_tracker,
frequency_thresholds: *thresholds,
})
}
fn calculate_priority_level(&self, seq_hash: SequenceHash) -> usize {
let frequency = self.frequency_tracker.count(seq_hash.as_u128());
let [t1, t2, t3] = self.frequency_thresholds;
if frequency < t1 as u32 {
0 } else if frequency < t2 as u32 {
1 } else if frequency < t3 as u32 {
2 } else {
3 }
}
}
impl<T: BlockMetadata> InactivePoolBackend<T> for MultiLruBackend<T> {
fn find_matches(&mut self, hashes: &[SequenceHash], touch: bool) -> Vec<Block<T, Registered>> {
let mut matches = Vec::with_capacity(hashes.len());
for hash in hashes {
let mut found = false;
for pool in &mut self.priority_pools {
if let Some(block) = pool.pop(hash) {
matches.push(block);
if touch {
self.frequency_tracker.touch(hash.as_u128());
}
found = true;
break;
}
}
if !found {
break;
}
}
matches
}
fn find_match(&mut self, hash: SequenceHash, touch: bool) -> Option<Block<T, Registered>> {
for pool in &mut self.priority_pools {
if let Some(block) = pool.pop(&hash) {
if touch {
self.frequency_tracker.touch(hash.as_u128());
}
return Some(block);
}
}
None
}
fn scan_matches(
&mut self,
hashes: &[SequenceHash],
touch: bool,
) -> Vec<(SequenceHash, Block<T, Registered>)> {
let mut matches = Vec::new();
for hash in hashes {
for pool in &mut self.priority_pools {
if let Some(block) = pool.pop(hash) {
if touch {
self.frequency_tracker.touch(hash.as_u128());
}
matches.push((*hash, block));
break; }
}
}
matches
}
fn allocate(&mut self, count: usize) -> Vec<Block<T, Registered>> {
let mut allocated = Vec::with_capacity(count);
for _ in 0..count {
let mut found = false;
for pool in &mut self.priority_pools {
if let Some((_seq_hash, block)) = pool.pop_lru() {
allocated.push(block);
found = true;
break;
}
}
if !found {
break;
}
}
allocated
}
fn insert(&mut self, block: Block<T, Registered>) {
let seq_hash = block.sequence_hash();
let level = self.calculate_priority_level(seq_hash);
debug_assert!(
self.priority_pools[level].len() < self.priority_pools[level].cap().get(),
"MultiLRU level {} insert would cause eviction! len={}, cap={}. \
This indicates insufficient capacity for all blocks.",
level,
self.priority_pools[level].len(),
self.priority_pools[level].cap().get()
);
self.priority_pools[level].put(seq_hash, block);
}
fn len(&self) -> usize {
self.priority_pools.iter().map(|pool| pool.len()).sum()
}
fn has_block(&self, seq_hash: SequenceHash) -> bool {
self.priority_pools
.iter()
.any(|pool| pool.peek(&seq_hash).is_some())
}
fn allocate_all(&mut self) -> Vec<Block<T, Registered>> {
let total_len: usize = self.priority_pools.iter().map(|p| p.len()).sum();
let mut allocated = Vec::with_capacity(total_len);
for pool in &mut self.priority_pools {
while let Some((_seq_hash, block)) = pool.pop_lru() {
allocated.push(block);
}
}
allocated
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pools::tests::fixtures::*;
use crate::tinylfu::TinyLFUTracker;
impl<T: BlockMetadata> MultiLruBackend<T> {
pub fn new(
capacity: NonZeroUsize,
frequency_tracker: Arc<dyn FrequencyTracker<u128>>,
) -> Self {
Self::new_with_thresholds(capacity, &[2, 6, 15], frequency_tracker).unwrap()
}
}
#[test]
fn test_multi_lru_priority_levels() {
let frequency_tracker = Arc::new(TinyLFUTracker::new(100));
let mut backend =
MultiLruBackend::new(NonZeroUsize::new(12).unwrap(), frequency_tracker.clone());
let (block1, hash1) = create_registered_block(1, &tokens_for_id(1));
let (block2, hash2) = create_registered_block(2, &tokens_for_id(2));
let (block3, hash3) = create_registered_block(3, &tokens_for_id(3));
let (block4, hash4) = create_registered_block(4, &tokens_for_id(4));
frequency_tracker.touch(hash2.as_u128());
frequency_tracker.touch(hash2.as_u128());
for _ in 0..6 {
frequency_tracker.touch(hash3.as_u128());
}
for _ in 0..16 {
frequency_tracker.touch(hash4.as_u128());
}
let _freq1 = frequency_tracker.count(hash1.as_u128());
let _freq2 = frequency_tracker.count(hash2.as_u128());
let _freq3 = frequency_tracker.count(hash3.as_u128());
let _freq4 = frequency_tracker.count(hash4.as_u128());
assert_eq!(backend.calculate_priority_level(hash1), 0); assert_eq!(backend.calculate_priority_level(hash2), 1); assert_eq!(backend.calculate_priority_level(hash3), 2); assert_eq!(backend.calculate_priority_level(hash4), 3);
backend.insert(block1);
backend.insert(block2);
backend.insert(block3);
backend.insert(block4);
assert_eq!(backend.len(), 4);
assert!(backend.has_block(hash1));
assert!(backend.has_block(hash2));
assert!(backend.has_block(hash3));
assert!(backend.has_block(hash4));
}
#[test]
fn test_multi_lru_eviction_order() {
let frequency_tracker = Arc::new(TinyLFUTracker::new(100));
let mut backend =
MultiLruBackend::new(NonZeroUsize::new(8).unwrap(), frequency_tracker.clone());
let (block1, hash1) = create_registered_block(1, &tokens_for_id(1));
let (block2, hash2) = create_registered_block(2, &tokens_for_id(2));
let (block3, hash3) = create_registered_block(3, &tokens_for_id(3));
for _ in 0..6 {
frequency_tracker.touch(hash3.as_u128());
}
backend.insert(block1);
backend.insert(block2);
backend.insert(block3);
let allocated = backend.allocate(2);
assert_eq!(allocated.len(), 2);
assert_eq!(allocated[0].block_id(), 1);
assert_eq!(allocated[1].block_id(), 2);
assert!(!backend.has_block(hash1));
assert!(!backend.has_block(hash2));
assert!(backend.has_block(hash3));
}
#[test]
fn test_multi_lru_find_matches() {
let frequency_tracker = Arc::new(TinyLFUTracker::new(100));
let mut backend = MultiLruBackend::new_with_thresholds(
NonZeroUsize::new(8).unwrap(),
&[2, 4, 8],
frequency_tracker.clone(),
)
.unwrap();
let (block1, hash1) = create_registered_block(1, &tokens_for_id(1));
let (block2, hash2) = create_registered_block(2, &tokens_for_id(2));
let (block3, hash3) = create_registered_block(3, &tokens_for_id(3));
for _ in 0..3 {
frequency_tracker.touch(hash2.as_u128());
}
for _ in 0..10 {
frequency_tracker.touch(hash3.as_u128());
}
backend.insert(block1);
backend.insert(block2);
backend.insert(block3);
let matches = backend.find_matches(&[hash1, hash2, hash3], true);
assert_eq!(matches.len(), 3);
assert_eq!(backend.len(), 0);
}
#[test]
fn test_multi_lru_capacity_distribution() {
let frequency_tracker = Arc::new(TinyLFUTracker::new(100));
let mut backend = MultiLruBackend::new_with_thresholds(
NonZeroUsize::new(16).unwrap(),
&[2, 6, 15],
frequency_tracker.clone(),
)
.unwrap();
let (block1, hash1) = create_registered_block(1, &tokens_for_id(1)); let (block2, hash2) = create_registered_block(2, &tokens_for_id(2)); let (block3, hash3) = create_registered_block(3, &tokens_for_id(3)); let (block4, hash4) = create_registered_block(4, &tokens_for_id(4));
for _ in 0..3 {
frequency_tracker.touch(hash2.as_u128()); }
for _ in 0..7 {
frequency_tracker.touch(hash3.as_u128()); }
for _ in 0..15 {
frequency_tracker.touch(hash4.as_u128()); }
assert_eq!(backend.calculate_priority_level(hash1), 0); assert_eq!(backend.calculate_priority_level(hash2), 1); assert_eq!(backend.calculate_priority_level(hash3), 2); assert_eq!(backend.calculate_priority_level(hash4), 3);
backend.insert(block1);
backend.insert(block2);
backend.insert(block3);
backend.insert(block4);
assert_eq!(backend.len(), 4);
assert!(backend.has_block(hash1));
assert!(backend.has_block(hash2));
assert!(backend.has_block(hash3));
assert!(backend.has_block(hash4));
let allocated = backend.allocate(4);
assert_eq!(allocated.len(), 4);
assert_eq!(backend.len(), 0);
}
}