use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use dashmap::DashMap;
use tower_lsp_server::ls_types::{SemanticToken, Uri};
use crate::document::ast::ParsedDoc;
use crate::index::file_index::FileIndex;
pub(crate) const PARSED_CACHE_CAP: usize = 2048;
pub(crate) const ANALYSIS_CACHE_CAP: usize = 512;
pub(crate) const OWNED_PROGRAM_CACHE_CAP: usize = 256;
pub(crate) struct AnalysisCacheEntry {
pub(crate) source: Arc<str>,
pub(crate) decl_version: u64,
pub(crate) decl_index: Option<Arc<FileIndex>>,
pub(crate) analysis: Option<Arc<mir_analyzer::FileAnalysis>>,
}
pub(crate) struct CacheRegistry {
pub(crate) token_cache: DashMap<Uri, (String, Arc<Vec<SemanticToken>>)>,
pub(crate) text_cache: DashMap<Uri, Arc<str>>,
pub(crate) parsed_cache: DashMap<Uri, (Arc<str>, Arc<ParsedDoc>)>,
pub(crate) analysis_cache: DashMap<Uri, AnalysisCacheEntry>,
pub(crate) decl_version: AtomicU64,
pub(crate) parse_count: AtomicU64,
pub(crate) owned_program_cache: DashMap<Uri, (Arc<str>, Arc<php_ast::owned::Program>)>,
pub(crate) vendor_index_cache: DashMap<Uri, Arc<FileIndex>>,
access_tick: AtomicU64,
last_access: DashMap<Uri, u64>,
}
impl CacheRegistry {
pub(crate) fn new() -> Self {
CacheRegistry {
token_cache: DashMap::new(),
text_cache: DashMap::new(),
parsed_cache: DashMap::new(),
analysis_cache: DashMap::new(),
decl_version: AtomicU64::new(0),
parse_count: AtomicU64::new(0),
owned_program_cache: DashMap::new(),
vendor_index_cache: DashMap::new(),
access_tick: AtomicU64::new(0),
last_access: DashMap::new(),
}
}
pub(crate) fn touch(&self, uri: &Uri) {
let tick = self.access_tick.fetch_add(1, Ordering::Relaxed);
if let Some(mut existing) = self.last_access.get_mut(uri) {
*existing = tick;
} else {
self.last_access.insert(uri.clone(), tick);
}
}
pub(crate) fn shed_stale<V>(&self, map: &DashMap<Uri, V>, cap: usize) {
let len = map.len();
if len < cap {
return;
}
let to_evict = cap / 2;
if to_evict == 0 {
return;
}
let mut ticks: Vec<u64> = map
.iter()
.map(|e| self.last_access.get(e.key()).map(|t| *t).unwrap_or(0))
.collect();
let (_, &mut threshold, _) = ticks.select_nth_unstable(to_evict - 1);
let to_remove: Vec<Uri> = map
.iter()
.filter(|e| {
let tick = self.last_access.get(e.key()).map(|t| *t).unwrap_or(0);
tick <= threshold
})
.take(to_evict)
.map(|e| e.key().clone())
.collect();
for uri in &to_remove {
map.remove(uri);
}
}
pub(crate) fn evict(&self, uri: &Uri) {
self.token_cache.remove(uri);
self.text_cache.remove(uri);
self.parsed_cache.remove(uri);
self.analysis_cache.remove(uri);
self.owned_program_cache.remove(uri);
self.vendor_index_cache.remove(uri);
self.last_access.remove(uri);
}
pub(crate) fn evict_analysis(&self, uri: &Uri) {
if let Some(mut entry) = self.analysis_cache.get_mut(uri) {
entry.analysis = None;
}
}
pub(crate) fn evict_analysis_all(&self) {
self.analysis_cache.clear();
}
pub(crate) fn evict_tokens(&self, uri: &Uri) {
self.token_cache.remove(uri);
}
pub(crate) fn store_token(
&self,
uri: &Uri,
result_id: String,
tokens: Arc<Vec<SemanticToken>>,
) {
self.token_cache.insert(uri.clone(), (result_id, tokens));
}
pub(crate) fn get_token(&self, uri: &Uri, result_id: &str) -> Option<Arc<Vec<SemanticToken>>> {
self.token_cache
.get(uri)
.filter(|e| e.0.as_str() == result_id)
.map(|e| Arc::clone(&e.1))
}
pub(crate) fn insert_parsed(&self, uri: Uri, text: Arc<str>, doc: Arc<ParsedDoc>) {
self.shed_stale(&self.parsed_cache, PARSED_CACHE_CAP);
self.touch(&uri);
self.parsed_cache.insert(uri, (text, doc));
}
pub(crate) fn decl_version(&self) -> u64 {
self.decl_version.load(Ordering::Acquire)
}
pub(crate) fn bump_decl_version(&self) {
self.decl_version.fetch_add(1, Ordering::Release);
}
pub(crate) fn bump_parse_count(&self) {
self.parse_count.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn parse_count(&self) -> u64 {
self.parse_count.load(Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn uri(n: usize) -> Uri {
format!("file:///f{n}.php").parse::<Uri>().unwrap()
}
#[test]
fn shed_stale_noop_below_cap() {
let reg = CacheRegistry::new();
let map: DashMap<Uri, ()> = DashMap::new();
for i in 0..5 {
map.insert(uri(i), ());
reg.touch(&uri(i));
}
reg.shed_stale(&map, 10);
assert_eq!(map.len(), 5, "below cap: nothing evicted");
for i in 0..5 {
assert!(
map.contains_key(&uri(i)),
"uri({i}) should still be present"
);
}
}
#[test]
fn shed_stale_evicts_oldest_half_at_cap() {
let reg = CacheRegistry::new();
let map: DashMap<Uri, ()> = DashMap::new();
for i in 0..10 {
map.insert(uri(i), ());
reg.touch(&uri(i));
}
reg.shed_stale(&map, 10);
assert_eq!(map.len(), 5, "half of cap should be evicted");
for i in 0..5 {
assert!(
!map.contains_key(&uri(i)),
"uri({i}) is oldest, should be evicted"
);
}
for i in 5..10 {
assert!(
map.contains_key(&uri(i)),
"uri({i}) is newest, should survive"
);
}
}
#[test]
fn shed_stale_treats_untouched_entries_as_oldest() {
let reg = CacheRegistry::new();
let map: DashMap<Uri, ()> = DashMap::new();
reg.touch(&uri(999));
map.insert(uri(0), ());
map.insert(uri(1), ());
map.insert(uri(2), ());
reg.touch(&uri(2));
map.insert(uri(3), ());
reg.touch(&uri(3));
reg.shed_stale(&map, 4);
assert_eq!(map.len(), 2);
assert!(!map.contains_key(&uri(0)));
assert!(!map.contains_key(&uri(1)));
assert!(map.contains_key(&uri(2)));
assert!(map.contains_key(&uri(3)));
}
}