From bb4ce39e50cc06742c8c19cdb2f1ba896ae01b67 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 16:17:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20CodeForge=20=E6=94=B9=E7=94=A8=E6=9C=89?= =?UTF-8?q?=E7=95=8C=20Mutex=EF=BC=8CKernelConfig=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20max=5Fcached=5Fmodules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 21 ++++- Cargo.toml | 1 + crates/oxide_code_cache/Cargo.toml | 2 +- crates/oxide_code_cache/src/lib.rs | 93 ++++++++++++++----- .../tests/structural_hash_tests.rs | 4 +- crates/oxide_kernel/src/kernel.rs | 25 ++++- 6 files changed, 120 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e56ce4b5..67ada84c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -460,6 +460,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "futures-core" version = "0.3.32" @@ -536,7 +542,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -546,6 +552,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] @@ -694,6 +702,15 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +[[package]] +name = "lru" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1040,7 +1057,7 @@ dependencies = [ name = "oxide_code_cache" version = "0.1.0" dependencies = [ - "dashmap", + "lru", "oxide_bytecode", ] diff --git a/Cargo.toml b/Cargo.toml index 0001a246..6e83ad8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ ryu = "1" memchr = "2" regex = "1" indexmap = "2" +lru = "0.18" smallvec = { version = "1", features = ["union"] } chrono = { version = "0.4", default-features = false, features = ["clock"] } unicode-normalization = "0.1" diff --git a/crates/oxide_code_cache/Cargo.toml b/crates/oxide_code_cache/Cargo.toml index 2e7cadf3..eeb4d051 100644 --- a/crates/oxide_code_cache/Cargo.toml +++ b/crates/oxide_code_cache/Cargo.toml @@ -8,4 +8,4 @@ description = "OxideJS - Compiler-independent compiled module cache" [dependencies] oxide_bytecode = { path = "../oxide_bytecode" } -dashmap.workspace = true +lru.workspace = true diff --git a/crates/oxide_code_cache/src/lib.rs b/crates/oxide_code_cache/src/lib.rs index 17e7b3a2..fd70cf7e 100644 --- a/crates/oxide_code_cache/src/lib.rs +++ b/crates/oxide_code_cache/src/lib.rs @@ -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>, + map: Mutex>>, } 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> { - 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 { 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(&self, hash: u64, compile: F) -> Result, String> where F: FnOnce() -> Result, { - 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); + } } diff --git a/crates/oxide_compiler/tests/structural_hash_tests.rs b/crates/oxide_compiler/tests/structural_hash_tests.rs index ed059349..ec1602e0 100644 --- a/crates/oxide_compiler/tests/structural_hash_tests.rs +++ b/crates/oxide_compiler/tests/structural_hash_tests.rs @@ -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"); diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index 60ed7784..feb30351 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -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, 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