mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: CodeForge 改用有界 Mutex<LruCache>,KernelConfig 新增 max_cached_modules
This commit is contained in:
@@ -8,4 +8,4 @@ description = "OxideJS - Compiler-independent compiled module cache"
|
||||
|
||||
[dependencies]
|
||||
oxide_bytecode = { path = "../oxide_bytecode" }
|
||||
dashmap.workspace = true
|
||||
lru.workspace = true
|
||||
|
||||
@@ -1,57 +1,65 @@
|
||||
#![doc = "OxideJS - Compiler-independent compiled module cache"]
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use lru::LruCache;
|
||||
use oxide_bytecode::CompiledModule;
|
||||
|
||||
/// 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: DashMap<u64, Arc<CompiledModule>>,
|
||||
map: Mutex<LruCache<u64, Arc<CompiledModule>>>,
|
||||
}
|
||||
|
||||
impl CodeForge {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(capacity: NonZeroUsize) -> Self {
|
||||
Self {
|
||||
map: DashMap::with_shard_amount(16),
|
||||
map: Mutex::new(LruCache::new(capacity)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, hash: u64) -> Option<Arc<CompiledModule>> {
|
||||
self.map.get(&hash).map(|entry| Arc::clone(&entry))
|
||||
self.map.lock().unwrap().get(&hash).map(Arc::clone)
|
||||
}
|
||||
|
||||
pub fn insert(&self, hash: u64, module: CompiledModule) -> Arc<CompiledModule> {
|
||||
let module = Arc::new(module);
|
||||
Arc::clone(self.map.entry(hash).or_insert(module).value())
|
||||
self.map.lock().unwrap().put(hash, Arc::clone(&module));
|
||||
module
|
||||
}
|
||||
|
||||
pub fn get_or_insert_with<F>(&self, hash: u64, compile: F) -> Result<Arc<CompiledModule>, String>
|
||||
where
|
||||
F: FnOnce() -> Result<CompiledModule, String>,
|
||||
{
|
||||
if let Some(module) = self.get(hash) {
|
||||
return Ok(module);
|
||||
// Two-phase: look up under the lock, then release the lock BEFORE
|
||||
// running the compile closure so a slow compilation never blocks other
|
||||
// cache users. Re-acquire the lock only to insert the freshly built
|
||||
// module.
|
||||
{
|
||||
let mut cache = self.map.lock().unwrap();
|
||||
if let Some(module) = cache.get(&hash) {
|
||||
return Ok(Arc::clone(module));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.insert(hash, compile()?))
|
||||
let module = Arc::new(compile()?);
|
||||
self.map.lock().unwrap().put(hash, Arc::clone(&module));
|
||||
Ok(module)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
self.map.lock().unwrap().len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.map.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CodeForge {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
self.map.lock().unwrap().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +67,13 @@ impl Default for CodeForge {
|
||||
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 = CodeForge::new();
|
||||
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");
|
||||
|
||||
@@ -70,10 +82,49 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cache_misses_for_different_hashes() {
|
||||
let forge = CodeForge::new();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use oxide_code_cache::CodeForge;
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_parser::Allocator;
|
||||
@@ -24,7 +26,7 @@ fn parse_to_compiled_hash(source: &str) -> u64 {
|
||||
|
||||
#[test]
|
||||
fn structural_hash_hit() {
|
||||
let forge = CodeForge::new();
|
||||
let forge = CodeForge::new(NonZeroUsize::new(512).unwrap());
|
||||
let compiler = Compiler::new();
|
||||
let allocator = Allocator::default();
|
||||
let program = oxide_parser::parse(&allocator, "1 + 2").expect("parse failed");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![allow(clippy::arc_with_non_send_sync)]
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use oxide_types::mem::P;
|
||||
@@ -21,6 +22,7 @@ pub struct KernelConfig {
|
||||
pub max_steps: Option<u64>,
|
||||
pub max_call_depth: usize,
|
||||
pub session_gc_threshold: usize,
|
||||
pub max_cached_modules: usize,
|
||||
pub log_levels: [LogLevel; SUBSYSTEM_COUNT],
|
||||
pub warmup_builtin_shapes: bool,
|
||||
pub warmup_builtin_code: bool,
|
||||
@@ -36,6 +38,7 @@ impl KernelConfig {
|
||||
max_steps: None,
|
||||
max_call_depth: 1024,
|
||||
session_gc_threshold: 8_388_608,
|
||||
max_cached_modules: 512,
|
||||
log_levels: [LogLevel::Off; SUBSYSTEM_COUNT],
|
||||
warmup_builtin_shapes: true,
|
||||
warmup_builtin_code: false,
|
||||
@@ -51,6 +54,7 @@ impl KernelConfig {
|
||||
max_steps: None,
|
||||
max_call_depth: 1024,
|
||||
session_gc_threshold: 8_388_608,
|
||||
max_cached_modules: 512,
|
||||
log_levels: [LogLevel::Off; SUBSYSTEM_COUNT],
|
||||
warmup_builtin_shapes: true,
|
||||
warmup_builtin_code: true,
|
||||
@@ -66,6 +70,7 @@ impl KernelConfig {
|
||||
max_steps: None,
|
||||
max_call_depth: 1024,
|
||||
session_gc_threshold: 8_388_608,
|
||||
max_cached_modules: 512,
|
||||
log_levels: [LogLevel::Off; SUBSYSTEM_COUNT],
|
||||
warmup_builtin_shapes: true,
|
||||
warmup_builtin_code: true,
|
||||
@@ -80,6 +85,14 @@ impl KernelConfig {
|
||||
pub fn set_session_gc_threshold(&mut self, bytes: usize) {
|
||||
self.session_gc_threshold = bytes;
|
||||
}
|
||||
|
||||
pub fn max_cached_modules(&self) -> usize {
|
||||
self.max_cached_modules
|
||||
}
|
||||
|
||||
pub fn set_max_cached_modules(&mut self, cap: usize) {
|
||||
self.max_cached_modules = cap;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KernelConfig {
|
||||
@@ -103,7 +116,9 @@ impl KernelCore {
|
||||
init_logging(&config.log_levels);
|
||||
let string_forge = Arc::new(StringForge::new());
|
||||
let shape_forge = Arc::new(ShapeForge::new());
|
||||
let code_forge = Arc::new(CodeForge::new());
|
||||
let code_forge = Arc::new(CodeForge::new(
|
||||
NonZeroUsize::new(config.max_cached_modules).expect("max_cached_modules must be greater than zero"),
|
||||
));
|
||||
let prop_forge = Arc::new(PropForge::new());
|
||||
Arc::new(Self {
|
||||
config,
|
||||
@@ -142,6 +157,14 @@ impl KernelCore {
|
||||
self.config.session_gc_threshold = bytes;
|
||||
}
|
||||
|
||||
pub fn max_cached_modules(&self) -> usize {
|
||||
self.config.max_cached_modules
|
||||
}
|
||||
|
||||
pub fn set_max_cached_modules(&mut self, cap: usize) {
|
||||
self.config.max_cached_modules = cap;
|
||||
}
|
||||
|
||||
pub fn sweep_runner_forges(&self) {
|
||||
self.string_forge.maybe_sweep(self.config.max_dead_strings.or(Some(1)));
|
||||
// test262 creates a fresh VM/session per test. At this boundary no JS object
|
||||
|
||||
Reference in New Issue
Block a user