Files
OxideJS-New/crates/oxide_code_cache/src/lib.rs
T
2026-08-05 21:46:47 +08:00

199 lines
6.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! oxide_code_cache:编译结果缓存(LRU,与编译器解耦)。
//!
//! `CodeForge` 以调用方提供的模块哈希为键缓存 `CompiledModule`(`Arc` 共享)。
//! 缓存自身不解析/编译 JS;键的计算与编译回调均由编译器侧提供。
//! LRU 上限约束内存,供 eval 循环/REPL 等重复编译场景复用产物。
use std::num::NonZeroUsize;
use std::sync::{Arc, Mutex};
use lru::LruCache;
use oxide_bytecode::CompiledModule;
mod code_cache_log;
/// Shared compiled-module cache keyed by a caller-provided safe module hash.
///
/// The cache intentionally does not know how to parse or compile JavaScript.
/// Compiler-aware callers compute the key and provide the compile callback.
///
/// Bounded by an LRU eviction policy: at most `capacity` modules are retained,
/// so an eval loop that compiles unbounded distinct sources cannot grow the
/// cache without limit.
pub struct CodeForge {
map: Mutex<LruCache<u64, Arc<CompiledModule>>>,
}
impl CodeForge {
/// 构造容量为 `capacity` 的缓存(容量为 0 会 panic)。
pub fn new(capacity: NonZeroUsize) -> Self {
Self {
map: Mutex::new(LruCache::new(capacity)),
}
}
/// 按哈希取缓存模块;未命中返回 None(命中会提升 LRU 位置)。
pub fn get(&self, hash: u64) -> Option<Arc<CompiledModule>> {
let result = self.map.lock().unwrap().get(&hash).map(Arc::clone);
if result.is_some() {
code_cache_debug!("CodeForge hit: hash={:#x}", hash);
}
result
}
/// 插入模块并返回共享 `Arc`;超出容量时逐出最久未用项。
pub fn insert(&self, hash: u64, module: CompiledModule) -> Arc<CompiledModule> {
let module = Arc::new(module);
let mut cache = self.map.lock().unwrap();
let evicted_hash = if cache.len() == cache.cap().get() && !cache.contains(&hash) {
cache.peek_lru().map(|(k, _)| *k)
} else {
None
};
cache.put(hash, Arc::clone(&module));
drop(cache);
match evicted_hash {
Some(evicted_hash) => code_cache_debug!("CodeForge evict: hash={:#x}", evicted_hash),
None => code_cache_debug!("CodeForge insert: hash={:#x}", hash),
}
module
}
/// 命中返回缓存模块,未命中则调用 `compile` 编译后缓存并返回。
/// debug 构建下命中时会重编译校验 bytecode,检测结构哈希碰撞。
pub fn get_or_insert_with<F>(&self, hash: u64, compile: F) -> Result<Arc<CompiledModule>, String>
where
F: Fn() -> Result<CompiledModule, String>,
{
{
let mut cache = self.map.lock().unwrap();
if let Some(module) = cache.get(&hash) {
let module = Arc::clone(module);
drop(cache);
code_cache_debug!("CodeForge hit: hash={:#x}", hash);
#[cfg(debug_assertions)]
{
let fresh = compile()?;
debug_assert_eq!(
module.bytecode, fresh.bytecode,
"structural hash collision: cached bytecode differs from recompiled for hash {hash}",
);
}
return Ok(module);
}
}
let module = Arc::new(compile()?);
self.map.lock().unwrap().put(hash, Arc::clone(&module));
code_cache_debug!("CodeForge miss: hash={:#x}", hash);
Ok(module)
}
/// 当前缓存条目数。
pub fn len(&self) -> usize {
self.map.lock().unwrap().len()
}
/// 缓存是否为空。
pub fn is_empty(&self) -> bool {
self.map.lock().unwrap().is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn forge(capacity: usize) -> CodeForge {
CodeForge::new(NonZeroUsize::new(capacity).unwrap())
}
#[test]
fn cache_returns_same_arc_for_same_hash() {
let forge = forge(16);
let first = forge.insert(1, CompiledModule::new());
let second = forge.get_or_insert_with(1, || Ok(CompiledModule::new())).expect("cache hit");
assert!(Arc::ptr_eq(&first, &second));
}
#[test]
fn cache_misses_for_different_hashes() {
let forge = forge(16);
let first = forge.insert(1, CompiledModule::new());
let second = forge.insert(2, CompiledModule::new());
assert!(!Arc::ptr_eq(&first, &second));
}
#[test]
fn lru_eviction_enforces_cap() {
let forge = forge(2);
forge.insert(1, CompiledModule::new());
forge.insert(2, CompiledModule::new());
forge.insert(3, CompiledModule::new());
assert_eq!(forge.len(), 2);
assert!(forge.get(1).is_none());
assert!(forge.get(2).is_some());
assert!(forge.get(3).is_some());
}
#[test]
fn lru_eviction_evicts_least_recently_used() {
let forge = forge(3);
forge.insert(1, CompiledModule::new());
forge.insert(2, CompiledModule::new());
forge.insert(3, CompiledModule::new());
// Touch key 1 so key 2 becomes the least-recently-used entry.
assert!(forge.get(1).is_some());
forge.insert(4, CompiledModule::new());
assert!(forge.get(1).is_some());
assert!(forge.get(2).is_none());
assert!(forge.get(3).is_some());
assert!(forge.get(4).is_some());
}
#[test]
fn len_never_exceeds_cap_under_many_distinct_inserts() {
let forge = forge(10);
for hash in 0..100 {
forge.insert(hash, CompiledModule::new());
}
assert_eq!(forge.len(), 10);
}
#[test]
#[cfg_attr(debug_assertions, should_panic(expected = "structural hash collision"))]
fn cache_hit_debug_verifies_bytecode() {
let forge = forge(16);
let mut m1 = CompiledModule::new();
m1.bytecode = vec![1, 2, 3];
forge.insert(42, m1);
let _ = forge
.get_or_insert_with(42, || {
let mut m = CompiledModule::new();
m.bytecode = vec![4, 5, 6];
Ok(m)
})
.unwrap();
}
#[test]
fn cache_hit_returns_cached_not_recompiled() {
let forge = forge(16);
let mut m = CompiledModule::new();
m.bytecode = vec![1, 2, 3];
let cached = forge.insert(99, m);
let result = forge
.get_or_insert_with(99, || {
let mut m = CompiledModule::new();
m.bytecode = vec![1, 2, 3];
Ok(m)
})
.unwrap();
assert!(Arc::ptr_eq(&cached, &result));
}
}