use parking_lot::Mutex;
use std::collections::{HashMap, VecDeque};
use std::hash::Hash;
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Clone, Debug)]
pub struct CacheConfig {
pub max_items: usize,
pub max_bytes: usize,
pub ttl: Option<Duration>,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_items: 10_000,
max_bytes: 50 * 1024 * 1024, ttl: None,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub current_items: usize,
pub current_bytes: usize,
pub hit_rate: f64,
}
impl CacheStats {
pub fn calculate_hit_rate(hits: u64, misses: u64) -> f64 {
let total = hits + misses;
if total == 0 { 0.0 } else { hits as f64 / total as f64 }
}
}
#[derive(Clone)]
struct CacheEntry<V> {
value: V,
last_accessed: Instant,
_inserted_at: Instant,
size_bytes: usize,
}
impl<V> CacheEntry<V> {
fn new(value: V, size_bytes: usize) -> Self {
let now = Instant::now();
Self { value, last_accessed: now, _inserted_at: now, size_bytes }
}
fn is_expired(&self, ttl: Duration) -> bool {
self.last_accessed.elapsed() > ttl
}
fn touch(&mut self) {
self.last_accessed = Instant::now();
}
}
pub struct BoundedLruCache<K, V>
where
K: Hash + Eq + Clone,
{
entries: Arc<Mutex<HashMap<K, CacheEntry<V>>>>,
access_order: Arc<Mutex<VecDeque<K>>>,
config: CacheConfig,
stats: Arc<Mutex<CacheStats>>,
}
impl<K, V> BoundedLruCache<K, V>
where
K: Hash + Eq + Clone,
{
pub fn new(config: CacheConfig) -> Self {
Self {
entries: Arc::new(Mutex::new(HashMap::new())),
access_order: Arc::new(Mutex::new(VecDeque::new())),
config,
stats: Arc::new(Mutex::new(CacheStats::default())),
}
}
pub fn default() -> Self {
Self::new(CacheConfig::default())
}
pub fn insert_with_size(&self, key: K, value: V, size_bytes: usize) -> bool {
let mut entries = self.entries.lock();
let mut access_order = self.access_order.lock();
let mut stats = self.stats.lock();
if let Some(entry) = entries.get_mut(&key) {
let old_size = entry.size_bytes;
entry.value = value;
entry.size_bytes = size_bytes;
entry.touch();
if let Some(pos) = access_order.iter().position(|k| k == &key) {
access_order.remove(pos);
}
access_order.push_back(key.clone());
stats.current_bytes = stats.current_bytes - old_size + size_bytes;
stats.current_items = entries.len();
return true;
}
while !entries.is_empty()
&& (entries.len() >= self.config.max_items
|| stats.current_bytes + size_bytes > self.config.max_bytes)
{
if let Some(lru_key) = access_order.pop_front() {
if let Some(entry) = entries.remove(&lru_key) {
stats.current_bytes -= entry.size_bytes;
stats.evictions += 1;
}
} else {
break;
}
}
if entries.len() >= self.config.max_items
|| stats.current_bytes + size_bytes > self.config.max_bytes
{
return false;
}
entries.insert(key.clone(), CacheEntry::new(value, size_bytes));
access_order.push_back(key);
stats.current_bytes += size_bytes;
stats.current_items = entries.len();
true
}
pub fn insert(&self, key: K, value: V)
where
V: EstimateSize,
{
let size_bytes = value.estimate_size();
self.insert_with_size(key, value, size_bytes);
}
pub fn get(&self, key: &K) -> Option<V>
where
V: Clone,
{
let mut entries = self.entries.lock();
let mut access_order = self.access_order.lock();
let mut stats = self.stats.lock();
if let Some(ttl) = self.config.ttl {
if let Some(entry) = entries.get(key) {
if entry.is_expired(ttl) {
entries.remove(key);
if let Some(pos) = access_order.iter().position(|k| k == key) {
access_order.remove(pos);
}
stats.misses += 1;
stats.hit_rate = CacheStats::calculate_hit_rate(stats.hits, stats.misses);
return None;
}
}
}
if let Some(entry) = entries.get_mut(key) {
entry.touch();
if let Some(pos) = access_order.iter().position(|k| k == key) {
let key_clone = access_order.remove(pos);
if let Some(key_clone) = key_clone {
access_order.push_back(key_clone);
}
}
stats.hits += 1;
stats.hit_rate = CacheStats::calculate_hit_rate(stats.hits, stats.misses);
Some(entry.value.clone())
} else {
stats.misses += 1;
stats.hit_rate = CacheStats::calculate_hit_rate(stats.hits, stats.misses);
None
}
}
pub fn peek(&self, key: &K) -> Option<V>
where
V: Clone,
{
let mut entries = self.entries.lock();
let mut access_order = self.access_order.lock();
let mut stats = self.stats.lock();
if let Some(ttl) = self.config.ttl {
if entries.get(key).is_some_and(|entry| entry.is_expired(ttl)) {
if let Some(entry) = entries.remove(key) {
stats.current_bytes -= entry.size_bytes;
stats.current_items = entries.len();
}
if let Some(pos) = access_order.iter().position(|k| k == key) {
access_order.remove(pos);
}
return None;
}
}
entries.get(key).map(|entry| entry.value.clone())
}
pub fn remove(&self, key: &K) -> Option<V>
where
V: Clone,
{
let mut entries = self.entries.lock();
let mut access_order = self.access_order.lock();
let mut stats = self.stats.lock();
if let Some(entry) = entries.remove(key) {
stats.current_bytes -= entry.size_bytes;
stats.current_items = entries.len();
if let Some(pos) = access_order.iter().position(|k| k == key) {
access_order.remove(pos);
}
Some(entry.value)
} else {
None
}
}
pub fn clear(&self) {
let mut entries = self.entries.lock();
let mut access_order = self.access_order.lock();
let mut stats = self.stats.lock();
entries.clear();
access_order.clear();
stats.current_bytes = 0;
stats.current_items = 0;
}
pub fn is_empty(&self) -> bool {
let entries = self.entries.lock();
entries.is_empty()
}
pub fn len(&self) -> usize {
let entries = self.entries.lock();
entries.len()
}
pub fn stats(&self) -> CacheStats {
let stats = self.stats.lock();
stats.clone()
}
pub fn config(&self) -> &CacheConfig {
&self.config
}
}
pub trait EstimateSize {
fn estimate_size(&self) -> usize;
}
impl EstimateSize for String {
fn estimate_size(&self) -> usize {
self.len()
}
}
impl<T> EstimateSize for Vec<T>
where
T: EstimateSize,
{
fn estimate_size(&self) -> usize {
self.iter().map(|t| t.estimate_size()).sum()
}
}
impl<K, V> EstimateSize for HashMap<K, V>
where
K: EstimateSize,
V: EstimateSize,
{
fn estimate_size(&self) -> usize {
self.iter().map(|(k, v)| k.estimate_size() + v.estimate_size()).sum()
}
}
impl EstimateSize for str {
fn estimate_size(&self) -> usize {
self.len()
}
}
impl<T: EstimateSize + ?Sized> EstimateSize for &T {
fn estimate_size(&self) -> usize {
(**self).estimate_size()
}
}
impl EstimateSize for [u8] {
fn estimate_size(&self) -> usize {
self.len()
}
}
impl EstimateSize for () {
fn estimate_size(&self) -> usize {
0
}
}
impl<T> EstimateSize for Option<T>
where
T: EstimateSize,
{
fn estimate_size(&self) -> usize {
self.as_ref().map(|t| t.estimate_size()).unwrap_or(0)
}
}
impl<T, E> EstimateSize for Result<T, E>
where
T: EstimateSize,
E: EstimateSize,
{
fn estimate_size(&self) -> usize {
match self {
Ok(t) => t.estimate_size(),
Err(e) => e.estimate_size(),
}
}
}
#[derive(Clone, Debug)]
pub struct AstCacheConfig {
pub max_nodes: usize,
pub max_bytes: usize,
}
impl Default for AstCacheConfig {
fn default() -> Self {
Self {
max_nodes: 10_000,
max_bytes: 50 * 1024 * 1024, }
}
}
#[derive(Clone, Debug)]
pub struct SymbolCacheConfig {
pub max_symbols: usize,
pub max_bytes: usize,
}
impl Default for SymbolCacheConfig {
fn default() -> Self {
Self {
max_symbols: 50_000,
max_bytes: 30 * 1024 * 1024, }
}
}
#[derive(Clone, Debug)]
pub struct WorkspaceCacheConfig {
pub max_files: usize,
pub max_bytes: usize,
}
impl Default for WorkspaceCacheConfig {
fn default() -> Self {
Self {
max_files: 1_000,
max_bytes: 20 * 1024 * 1024, }
}
}
#[derive(Clone, Debug, Default)]
pub struct CombinedWorkspaceCacheConfig {
pub ast: AstCacheConfig,
pub symbol: SymbolCacheConfig,
pub workspace: WorkspaceCacheConfig,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_insert_get() {
let cache = BoundedLruCache::<String, String>::default();
cache.insert("key1".to_string(), "value1".to_string());
assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string()));
}
#[test]
fn test_cache_miss() {
let cache = BoundedLruCache::<String, String>::default();
assert_eq!(cache.get(&"nonexistent".to_string()), None);
}
#[test]
fn test_cache_eviction() {
let config = CacheConfig { max_items: 2, max_bytes: 100, ttl: None };
let cache = BoundedLruCache::<String, String>::new(config);
cache.insert("key1".to_string(), "value1".to_string());
cache.insert("key2".to_string(), "value2".to_string());
cache.insert("key3".to_string(), "value3".to_string());
assert_eq!(cache.get(&"key1".to_string()), None);
assert_eq!(cache.get(&"key2".to_string()), Some("value2".to_string()));
assert_eq!(cache.get(&"key3".to_string()), Some("value3".to_string()));
}
#[test]
fn test_cache_stats() {
let cache = BoundedLruCache::<String, String>::default();
cache.insert("key1".to_string(), "value1".to_string());
cache.get(&"key1".to_string());
cache.get(&"key2".to_string());
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
assert_eq!(stats.hit_rate, 0.5);
}
#[test]
fn test_cache_clear() {
let cache = BoundedLruCache::<String, String>::default();
cache.insert("key1".to_string(), "value1".to_string());
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
#[test]
fn test_cache_remove() {
let cache = BoundedLruCache::<String, String>::default();
cache.insert("key1".to_string(), "value1".to_string());
assert_eq!(cache.remove(&"key1".to_string()), Some("value1".to_string()));
assert_eq!(cache.get(&"key1".to_string()), None);
}
#[test]
fn test_cache_peek_does_not_change_stats() {
let cache = BoundedLruCache::<String, String>::default();
cache.insert("key1".to_string(), "value1".to_string());
assert_eq!(cache.peek(&"key1".to_string()), Some("value1".to_string()));
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
#[test]
fn test_estimate_size_string() {
let s = "hello world";
assert_eq!(s.estimate_size(), 11);
}
#[test]
fn test_estimate_size_vec() {
let v = vec!["hello", "world"];
assert_eq!(v.estimate_size(), 10);
}
}