use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, LazyLock, RwLock, Weak};
use std::time::Instant;
use cljrs_ir::IrFunction;
pub enum IrCacheEntry {
NotAttempted,
Unsupported,
Cached {
ir: Arc<IrFunction>,
last_access: AtomicU64,
},
}
static PROCESS_EPOCH: LazyLock<Instant> = LazyLock::new(Instant::now);
pub fn now_secs() -> u64 {
PROCESS_EPOCH.elapsed().as_secs()
}
pub fn ir_cache_ttl_secs() -> u64 {
std::env::var("CLJRS_IR_CACHE_TTL")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(600)
}
pub struct IrCache {
entries: RwLock<HashMap<u64, IrCacheEntry>>,
}
impl IrCache {
pub fn new(globals_id: u64) -> Arc<Self> {
let cache = Arc::new(Self {
entries: RwLock::new(HashMap::new()),
});
let mut live = LIVE.write().unwrap();
live.retain(|(_, weak)| weak.strong_count() > 0);
live.push((globals_id, Arc::downgrade(&cache)));
cache
}
pub fn get(&self, id: u64) -> Option<Arc<IrFunction>> {
let guard = self.entries.read().unwrap();
match guard.get(&id) {
Some(IrCacheEntry::Cached { ir, last_access }) => {
last_access.store(now_secs(), Ordering::Relaxed);
Some(ir.clone())
}
_ => None,
}
}
pub fn should_attempt(&self, id: u64) -> bool {
!self.entries.read().unwrap().contains_key(&id)
}
pub fn store(&self, id: u64, ir: Arc<IrFunction>) {
self.entries.write().unwrap().insert(
id,
IrCacheEntry::Cached {
ir,
last_access: AtomicU64::new(now_secs()),
},
);
}
pub fn store_unsupported(&self, id: u64) {
self.entries
.write()
.unwrap()
.insert(id, IrCacheEntry::Unsupported);
}
pub fn invalidate(&self, id: u64) {
self.entries.write().unwrap().remove(&id);
}
pub fn sweep(&self, now: u64, ttl_secs: u64) -> Vec<u64> {
let mut evicted = Vec::new();
let mut guard = self.entries.write().unwrap();
guard.retain(|&id, entry| {
let IrCacheEntry::Cached { last_access, .. } = entry else {
return true;
};
let idle = now.saturating_sub(last_access.load(Ordering::Relaxed));
if idle <= ttl_secs {
return true;
}
if crate::tiered::jit_state::get_native_fn(id).is_some()
|| crate::tiered::jit_state::compile_queued(id)
{
return true;
}
evicted.push(id);
false
});
drop(guard);
for &id in &evicted {
crate::tiered::jit_state::evict_entry_if_cold(id);
crate::tiered::jit_state::stale_osr_code(id);
cljrs_logging::feat_debug!("ir", "evicted idle IR arity_id={}", id);
}
evicted
}
}
#[allow(clippy::type_complexity)]
static LIVE: LazyLock<RwLock<Vec<(u64, Weak<IrCache>)>>> =
LazyLock::new(|| RwLock::new(Vec::new()));
fn live_caches() -> Vec<Arc<IrCache>> {
LIVE.read()
.unwrap()
.iter()
.filter_map(|(_, weak)| weak.upgrade())
.collect()
}
pub fn by_globals_id(globals_id: u64) -> Option<Arc<IrCache>> {
LIVE.read()
.unwrap()
.iter()
.find(|(id, _)| *id == globals_id)
.and_then(|(_, weak)| weak.upgrade())
}
pub fn get_cached(id: u64) -> Option<Arc<IrFunction>> {
live_caches().into_iter().find_map(|cache| cache.get(id))
}
pub fn should_attempt(id: u64) -> bool {
live_caches().iter().all(|cache| cache.should_attempt(id))
}
pub fn invalidate(id: u64) {
for cache in live_caches() {
cache.invalidate(id);
}
}
pub fn sweep_idle(now: u64, ttl_secs: u64) -> Vec<u64> {
let mut evicted = Vec::new();
for cache in live_caches() {
evicted.extend(cache.sweep(now, ttl_secs));
}
evicted
}
#[cfg(test)]
pub(crate) static SWEEP_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
mod tests {
use super::*;
fn dummy_ir() -> Arc<IrFunction> {
Arc::new(IrFunction::new(None, None))
}
fn test_cache() -> Arc<IrCache> {
IrCache::new(u64::MAX)
}
fn sweep_guard() -> std::sync::MutexGuard<'static, ()> {
SWEEP_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner())
}
#[test]
fn sweep_evicts_idle_entry_and_drops_jit_entry() {
let _g = sweep_guard();
let cache = test_cache();
let id = 0xE500_0001;
cache.store(id, dummy_ir());
crate::tiered::jit_state::mark_lower_queued(id);
let stored_at = now_secs();
assert!(sweep_idle(stored_at, 600).is_empty() || !cache.should_attempt(id));
assert!(cache.get(id).is_some());
let evicted = sweep_idle(stored_at + 601, 600);
assert!(evicted.contains(&id));
assert!(cache.get(id).is_none());
assert!(cache.should_attempt(id));
assert!(!crate::tiered::jit_state::lower_queued(id));
}
#[test]
fn sweep_skips_native_published_arity() {
let _g = sweep_guard();
let cache = test_cache();
let id = 0xE500_0002;
cache.store(id, dummy_ir());
crate::tiered::jit_state::store_native_fn(id, 0x1234usize as *const (), 31337);
let evicted = sweep_idle(now_secs() + 10_000, 600);
assert!(!evicted.contains(&id));
assert!(cache.get(id).is_some());
crate::tiered::jit_state::take_native_epoch(id);
cache.invalidate(id);
}
#[test]
fn sweep_skips_queued_compile() {
let _g = sweep_guard();
let cache = test_cache();
let id = 0xE500_0003;
let ir = dummy_ir();
cache.store(id, ir.clone());
for _ in 0..crate::tiered::jit_state::jit_threshold() {
crate::tiered::jit_state::record_call(id, ir.clone(), &[]);
}
assert!(crate::tiered::jit_state::compile_queued(id));
let evicted = sweep_idle(now_secs() + 10_000, 600);
assert!(!evicted.contains(&id));
assert!(cache.get(id).is_some());
cache.invalidate(id);
}
#[test]
fn sweep_never_touches_unsupported() {
let _g = sweep_guard();
let cache = test_cache();
let id = 0xE500_0004;
cache.store_unsupported(id);
let evicted = sweep_idle(now_secs() + 10_000, 600);
assert!(!evicted.contains(&id));
assert!(!cache.should_attempt(id));
}
#[test]
fn get_refreshes_last_access() {
let _g = sweep_guard();
let cache = test_cache();
let id = 0xE500_0005;
cache.store(id, dummy_ir());
let _ = cache.get(id);
let touched_at = now_secs();
let evicted = sweep_idle(touched_at + 599, 600);
assert!(!evicted.contains(&id));
assert!(cache.get(id).is_some());
cache.invalidate(id);
}
#[test]
fn caches_are_per_runtime() {
let _g = sweep_guard();
let a = IrCache::new(0xE5AA_0001);
let b = IrCache::new(0xE5AA_0002);
let id = 0xE500_0006;
a.store(id, dummy_ir());
assert!(a.get(id).is_some());
assert!(b.get(id).is_none(), "b must not see a's entry");
assert!(b.should_attempt(id));
assert!(by_globals_id(0xE5AA_0001).unwrap().get(id).is_some());
assert!(by_globals_id(0xE5AA_0002).unwrap().get(id).is_none());
assert!(get_cached(id).is_some());
drop(a);
assert!(get_cached(id).is_none());
assert!(by_globals_id(0xE5AA_0001).is_none());
drop(b);
}
}