use lru::LruCache;
use parking_lot::RwLock;
use std::num::NonZeroUsize;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
#[repr(align(64))]
#[derive(Debug, Clone)]
pub struct AlignedVector {
data: Vec<f32>,
}
impl AlignedVector {
pub fn new(data: Vec<f32>) -> Self {
Self { data }
}
pub fn zeros(len: usize) -> Self {
Self {
data: vec![0.0; len],
}
}
pub fn as_slice(&self) -> &[f32] {
&self.data
}
pub fn as_mut_slice(&mut self) -> &mut [f32] {
&mut self.data
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn into_vec(self) -> Vec<f32> {
self.data
}
}
impl From<Vec<f32>> for AlignedVector {
fn from(data: Vec<f32>) -> Self {
Self::new(data)
}
}
impl AsRef<[f32]> for AlignedVector {
fn as_ref(&self) -> &[f32] {
&self.data
}
}
impl AsMut<[f32]> for AlignedVector {
fn as_mut(&mut self) -> &mut [f32] {
&mut self.data
}
}
#[derive(Debug, Clone)]
struct AccessStats {
access_count: u64,
last_access: Instant,
first_access: Instant,
time_in_cache: Duration,
}
impl AccessStats {
fn new() -> Self {
let now = Instant::now();
Self {
access_count: 1,
last_access: now,
first_access: now,
time_in_cache: Duration::from_secs(0),
}
}
fn record_access(&mut self) {
self.access_count += 1;
self.last_access = Instant::now();
self.time_in_cache = self.last_access.duration_since(self.first_access);
}
fn access_frequency(&self) -> f64 {
if self.time_in_cache.as_secs_f64() > 0.0 {
self.access_count as f64 / self.time_in_cache.as_secs_f64()
} else {
self.access_count as f64
}
}
}
#[derive(Debug, Clone)]
struct CachedEmbedding {
vector: AlignedVector,
stats: AccessStats,
}
pub struct HotEmbeddingCache {
cache: Arc<RwLock<LruCache<String, CachedEmbedding>>>,
hits: Arc<AtomicU64>,
misses: Arc<AtomicU64>,
capacity: usize,
prefetch_queue: Arc<RwLock<Vec<String>>>,
}
impl HotEmbeddingCache {
pub fn new(capacity: usize) -> Self {
Self {
cache: Arc::new(RwLock::new(LruCache::new(
NonZeroUsize::new(capacity).expect("cache capacity must be non-zero"),
))),
hits: Arc::new(AtomicU64::new(0)),
misses: Arc::new(AtomicU64::new(0)),
capacity,
prefetch_queue: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn get(&self, key: &str) -> Option<AlignedVector> {
let mut cache = self.cache.write();
if let Some(entry) = cache.get_mut(key) {
entry.stats.record_access();
self.hits.fetch_add(1, Ordering::Relaxed);
Some(entry.vector.clone())
} else {
self.misses.fetch_add(1, Ordering::Relaxed);
None
}
}
pub fn insert(&self, key: String, vector: Vec<f32>) {
let aligned = AlignedVector::new(vector);
let entry = CachedEmbedding {
vector: aligned,
stats: AccessStats::new(),
};
self.cache.write().put(key, entry);
}
pub fn stats(&self) -> HotCacheStats {
let hits = self.hits.load(Ordering::Relaxed);
let misses = self.misses.load(Ordering::Relaxed);
let total = hits + misses;
let hit_rate = if total > 0 {
hits as f64 / total as f64
} else {
0.0
};
let cache = self.cache.read();
let size = cache.len();
HotCacheStats {
hits,
misses,
hit_rate,
size,
capacity: self.capacity,
}
}
pub fn clear(&self) {
self.cache.write().clear();
self.hits.store(0, Ordering::Relaxed);
self.misses.store(0, Ordering::Relaxed);
}
pub fn len(&self) -> usize {
self.cache.read().len()
}
pub fn is_empty(&self) -> bool {
self.cache.read().is_empty()
}
pub fn prefetch(&self, keys: Vec<String>) {
let mut queue = self.prefetch_queue.write();
queue.extend(keys);
}
pub fn get_hot_keys(&self, top_n: usize) -> Vec<String> {
let cache = self.cache.read();
let mut entries: Vec<_> = cache
.iter()
.map(|(k, v)| (k.clone(), v.stats.access_frequency()))
.collect();
entries.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
entries.into_iter().take(top_n).map(|(k, _)| k).collect()
}
}
#[derive(Debug, Clone)]
pub struct HotCacheStats {
pub hits: u64,
pub misses: u64,
pub hit_rate: f64,
pub size: usize,
pub capacity: usize,
}
pub struct AdaptiveCacheStrategy {
target_size: Arc<RwLock<usize>>,
min_size: usize,
max_size: usize,
target_hit_rate: f64,
adjustment_factor: f64,
}
impl AdaptiveCacheStrategy {
pub fn new(min_size: usize, max_size: usize, target_hit_rate: f64) -> Self {
Self {
target_size: Arc::new(RwLock::new((min_size + max_size) / 2)),
min_size,
max_size,
target_hit_rate,
adjustment_factor: 1.1, }
}
pub fn adjust(&self, current_hit_rate: f64) -> usize {
let mut target = self.target_size.write();
if current_hit_rate < self.target_hit_rate {
let new_size =
(*target as f64 * self.adjustment_factor).min(self.max_size as f64) as usize;
*target = new_size;
} else if current_hit_rate > self.target_hit_rate + 0.05 {
let new_size =
(*target as f64 / self.adjustment_factor).max(self.min_size as f64) as usize;
*target = new_size;
}
*target
}
pub fn target_size(&self) -> usize {
*self.target_size.read()
}
pub fn reset(&self) {
*self.target_size.write() = (self.min_size + self.max_size) / 2;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvalidationPolicy {
TTL(Duration),
Event,
Never,
}
pub struct CacheInvalidator {
policy: InvalidationPolicy,
last_invalidation: Arc<RwLock<Instant>>,
}
impl CacheInvalidator {
pub fn new(policy: InvalidationPolicy) -> Self {
Self {
policy,
last_invalidation: Arc::new(RwLock::new(Instant::now())),
}
}
pub fn should_invalidate(&self) -> bool {
match self.policy {
InvalidationPolicy::TTL(ttl) => {
let elapsed = self.last_invalidation.read().elapsed();
elapsed >= ttl
}
InvalidationPolicy::Event => false, InvalidationPolicy::Never => false,
}
}
pub fn invalidate(&self) {
*self.last_invalidation.write() = Instant::now();
}
pub fn time_since_invalidation(&self) -> Duration {
self.last_invalidation.read().elapsed()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aligned_vector_creation() {
let data = vec![1.0, 2.0, 3.0, 4.0];
let aligned = AlignedVector::new(data.clone());
assert_eq!(aligned.len(), 4);
assert_eq!(aligned.as_slice(), &data[..]);
}
#[test]
fn test_aligned_vector_alignment() {
let aligned = AlignedVector::zeros(100);
assert_eq!(
std::mem::align_of::<AlignedVector>(),
64,
"AlignedVector struct should be aligned to 64 bytes"
);
let ptr = aligned.as_slice().as_ptr() as usize;
assert_eq!(
ptr % std::mem::align_of::<f32>(),
0,
"Data pointer should be properly aligned for f32"
);
}
#[test]
fn test_hot_cache_basic() {
let cache = HotEmbeddingCache::new(10);
cache.insert("key1".to_string(), vec![1.0, 2.0, 3.0]);
cache.insert("key2".to_string(), vec![4.0, 5.0, 6.0]);
let vec1 = cache
.get("key1")
.expect("test: key1 should be present in cache");
assert_eq!(vec1.as_slice(), &[1.0, 2.0, 3.0]);
let vec2 = cache
.get("key2")
.expect("test: key2 should be present in cache");
assert_eq!(vec2.as_slice(), &[4.0, 5.0, 6.0]);
assert!(cache.get("key3").is_none());
}
#[test]
fn test_hot_cache_stats() {
let cache = HotEmbeddingCache::new(10);
cache.insert("key1".to_string(), vec![1.0, 2.0, 3.0]);
cache.get("key1");
cache.get("key2");
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
assert_eq!(stats.hit_rate, 0.5);
}
#[test]
fn test_hot_cache_lru() {
let cache = HotEmbeddingCache::new(2);
cache.insert("key1".to_string(), vec![1.0]);
cache.insert("key2".to_string(), vec![2.0]);
cache.insert("key3".to_string(), vec![3.0]);
assert!(cache.get("key1").is_none());
assert!(cache.get("key2").is_some());
assert!(cache.get("key3").is_some());
}
#[test]
fn test_adaptive_strategy() {
let strategy = AdaptiveCacheStrategy::new(100, 1000, 0.8);
let initial_size = strategy.target_size();
assert_eq!(initial_size, 550);
let new_size = strategy.adjust(0.5);
assert!(new_size > initial_size);
strategy.reset();
let new_size = strategy.adjust(0.95);
assert!(new_size < initial_size);
}
#[test]
fn test_cache_invalidator_ttl() {
let invalidator = CacheInvalidator::new(InvalidationPolicy::TTL(Duration::from_millis(10)));
assert!(!invalidator.should_invalidate());
std::thread::sleep(Duration::from_millis(15));
assert!(invalidator.should_invalidate());
}
#[test]
fn test_cache_invalidator_never() {
let invalidator = CacheInvalidator::new(InvalidationPolicy::Never);
std::thread::sleep(Duration::from_millis(10));
assert!(!invalidator.should_invalidate());
}
#[test]
fn test_hot_keys_tracking() {
let cache = HotEmbeddingCache::new(10);
cache.insert("key1".to_string(), vec![1.0]);
cache.insert("key2".to_string(), vec![2.0]);
cache.insert("key3".to_string(), vec![3.0]);
std::thread::sleep(Duration::from_millis(1));
for _ in 0..5 {
cache.get("key1");
}
for _ in 0..2 {
cache.get("key2");
}
let hot_keys = cache.get_hot_keys(2);
assert_eq!(hot_keys.len(), 2);
assert!(hot_keys.contains(&"key1".to_string()));
assert!(hot_keys.contains(&"key2".to_string()));
}
}