mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 10:41:11 +08:00
feat: CodeForge 改用有界 Mutex<LruCache>,KernelConfig 新增 max_cached_modules
This commit is contained in:
Generated
+19
-2
@@ -460,6 +460,12 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -536,7 +542,7 @@ version = "0.15.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"foldhash",
|
"foldhash 0.1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -546,6 +552,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -694,6 +702,15 @@ version = "0.4.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
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]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1040,7 +1057,7 @@ dependencies = [
|
|||||||
name = "oxide_code_cache"
|
name = "oxide_code_cache"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dashmap",
|
"lru",
|
||||||
"oxide_bytecode",
|
"oxide_bytecode",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ ryu = "1"
|
|||||||
memchr = "2"
|
memchr = "2"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
indexmap = "2"
|
indexmap = "2"
|
||||||
|
lru = "0.18"
|
||||||
smallvec = { version = "1", features = ["union"] }
|
smallvec = { version = "1", features = ["union"] }
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||||
unicode-normalization = "0.1"
|
unicode-normalization = "0.1"
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ description = "OxideJS - Compiler-independent compiled module cache"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
oxide_bytecode = { path = "../oxide_bytecode" }
|
oxide_bytecode = { path = "../oxide_bytecode" }
|
||||||
dashmap.workspace = true
|
lru.workspace = true
|
||||||
|
|||||||
@@ -1,57 +1,65 @@
|
|||||||
#![doc = "OxideJS - Compiler-independent compiled module cache"]
|
#![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;
|
use oxide_bytecode::CompiledModule;
|
||||||
|
|
||||||
/// Shared compiled-module cache keyed by a caller-provided safe module hash.
|
/// Shared compiled-module cache keyed by a caller-provided safe module hash.
|
||||||
///
|
///
|
||||||
/// The cache intentionally does not know how to parse or compile JavaScript.
|
/// The cache intentionally does not know how to parse or compile JavaScript.
|
||||||
/// Compiler-aware callers compute the key and provide the compile callback.
|
/// 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 {
|
pub struct CodeForge {
|
||||||
map: DashMap<u64, Arc<CompiledModule>>,
|
map: Mutex<LruCache<u64, Arc<CompiledModule>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CodeForge {
|
impl CodeForge {
|
||||||
pub fn new() -> Self {
|
pub fn new(capacity: NonZeroUsize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
map: DashMap::with_shard_amount(16),
|
map: Mutex::new(LruCache::new(capacity)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, hash: u64) -> Option<Arc<CompiledModule>> {
|
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> {
|
pub fn insert(&self, hash: u64, module: CompiledModule) -> Arc<CompiledModule> {
|
||||||
let module = Arc::new(module);
|
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>
|
pub fn get_or_insert_with<F>(&self, hash: u64, compile: F) -> Result<Arc<CompiledModule>, String>
|
||||||
where
|
where
|
||||||
F: FnOnce() -> Result<CompiledModule, String>,
|
F: FnOnce() -> Result<CompiledModule, String>,
|
||||||
{
|
{
|
||||||
if let Some(module) = self.get(hash) {
|
// Two-phase: look up under the lock, then release the lock BEFORE
|
||||||
return Ok(module);
|
// 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
let module = Arc::new(compile()?);
|
||||||
Ok(self.insert(hash, compile()?))
|
self.map.lock().unwrap().put(hash, Arc::clone(&module));
|
||||||
|
Ok(module)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.map.len()
|
self.map.lock().unwrap().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.map.is_empty()
|
self.map.lock().unwrap().is_empty()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CodeForge {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,9 +67,13 @@ impl Default for CodeForge {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn forge(capacity: usize) -> CodeForge {
|
||||||
|
CodeForge::new(NonZeroUsize::new(capacity).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cache_returns_same_arc_for_same_hash() {
|
fn cache_returns_same_arc_for_same_hash() {
|
||||||
let forge = CodeForge::new();
|
let forge = forge(16);
|
||||||
let first = forge.insert(1, CompiledModule::new());
|
let first = forge.insert(1, CompiledModule::new());
|
||||||
let second = forge.get_or_insert_with(1, || Ok(CompiledModule::new())).expect("cache hit");
|
let second = forge.get_or_insert_with(1, || Ok(CompiledModule::new())).expect("cache hit");
|
||||||
|
|
||||||
@@ -70,10 +82,49 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cache_misses_for_different_hashes() {
|
fn cache_misses_for_different_hashes() {
|
||||||
let forge = CodeForge::new();
|
let forge = forge(16);
|
||||||
let first = forge.insert(1, CompiledModule::new());
|
let first = forge.insert(1, CompiledModule::new());
|
||||||
let second = forge.insert(2, CompiledModule::new());
|
let second = forge.insert(2, CompiledModule::new());
|
||||||
|
|
||||||
assert!(!Arc::ptr_eq(&first, &second));
|
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_code_cache::CodeForge;
|
||||||
use oxide_compiler::compiler::Compiler;
|
use oxide_compiler::compiler::Compiler;
|
||||||
use oxide_parser::Allocator;
|
use oxide_parser::Allocator;
|
||||||
@@ -24,7 +26,7 @@ fn parse_to_compiled_hash(source: &str) -> u64 {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn structural_hash_hit() {
|
fn structural_hash_hit() {
|
||||||
let forge = CodeForge::new();
|
let forge = CodeForge::new(NonZeroUsize::new(512).unwrap());
|
||||||
let compiler = Compiler::new();
|
let compiler = Compiler::new();
|
||||||
let allocator = Allocator::default();
|
let allocator = Allocator::default();
|
||||||
let program = oxide_parser::parse(&allocator, "1 + 2").expect("parse failed");
|
let program = oxide_parser::parse(&allocator, "1 + 2").expect("parse failed");
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#![allow(clippy::arc_with_non_send_sync)]
|
#![allow(clippy::arc_with_non_send_sync)]
|
||||||
|
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use oxide_types::mem::P;
|
use oxide_types::mem::P;
|
||||||
@@ -21,6 +22,7 @@ pub struct KernelConfig {
|
|||||||
pub max_steps: Option<u64>,
|
pub max_steps: Option<u64>,
|
||||||
pub max_call_depth: usize,
|
pub max_call_depth: usize,
|
||||||
pub session_gc_threshold: usize,
|
pub session_gc_threshold: usize,
|
||||||
|
pub max_cached_modules: usize,
|
||||||
pub log_levels: [LogLevel; SUBSYSTEM_COUNT],
|
pub log_levels: [LogLevel; SUBSYSTEM_COUNT],
|
||||||
pub warmup_builtin_shapes: bool,
|
pub warmup_builtin_shapes: bool,
|
||||||
pub warmup_builtin_code: bool,
|
pub warmup_builtin_code: bool,
|
||||||
@@ -36,6 +38,7 @@ impl KernelConfig {
|
|||||||
max_steps: None,
|
max_steps: None,
|
||||||
max_call_depth: 1024,
|
max_call_depth: 1024,
|
||||||
session_gc_threshold: 8_388_608,
|
session_gc_threshold: 8_388_608,
|
||||||
|
max_cached_modules: 512,
|
||||||
log_levels: [LogLevel::Off; SUBSYSTEM_COUNT],
|
log_levels: [LogLevel::Off; SUBSYSTEM_COUNT],
|
||||||
warmup_builtin_shapes: true,
|
warmup_builtin_shapes: true,
|
||||||
warmup_builtin_code: false,
|
warmup_builtin_code: false,
|
||||||
@@ -51,6 +54,7 @@ impl KernelConfig {
|
|||||||
max_steps: None,
|
max_steps: None,
|
||||||
max_call_depth: 1024,
|
max_call_depth: 1024,
|
||||||
session_gc_threshold: 8_388_608,
|
session_gc_threshold: 8_388_608,
|
||||||
|
max_cached_modules: 512,
|
||||||
log_levels: [LogLevel::Off; SUBSYSTEM_COUNT],
|
log_levels: [LogLevel::Off; SUBSYSTEM_COUNT],
|
||||||
warmup_builtin_shapes: true,
|
warmup_builtin_shapes: true,
|
||||||
warmup_builtin_code: true,
|
warmup_builtin_code: true,
|
||||||
@@ -66,6 +70,7 @@ impl KernelConfig {
|
|||||||
max_steps: None,
|
max_steps: None,
|
||||||
max_call_depth: 1024,
|
max_call_depth: 1024,
|
||||||
session_gc_threshold: 8_388_608,
|
session_gc_threshold: 8_388_608,
|
||||||
|
max_cached_modules: 512,
|
||||||
log_levels: [LogLevel::Off; SUBSYSTEM_COUNT],
|
log_levels: [LogLevel::Off; SUBSYSTEM_COUNT],
|
||||||
warmup_builtin_shapes: true,
|
warmup_builtin_shapes: true,
|
||||||
warmup_builtin_code: true,
|
warmup_builtin_code: true,
|
||||||
@@ -80,6 +85,14 @@ impl KernelConfig {
|
|||||||
pub fn set_session_gc_threshold(&mut self, bytes: usize) {
|
pub fn set_session_gc_threshold(&mut self, bytes: usize) {
|
||||||
self.session_gc_threshold = bytes;
|
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 {
|
impl Default for KernelConfig {
|
||||||
@@ -103,7 +116,9 @@ impl KernelCore {
|
|||||||
init_logging(&config.log_levels);
|
init_logging(&config.log_levels);
|
||||||
let string_forge = Arc::new(StringForge::new());
|
let string_forge = Arc::new(StringForge::new());
|
||||||
let shape_forge = Arc::new(ShapeForge::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());
|
let prop_forge = Arc::new(PropForge::new());
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
config,
|
config,
|
||||||
@@ -142,6 +157,14 @@ impl KernelCore {
|
|||||||
self.config.session_gc_threshold = bytes;
|
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) {
|
pub fn sweep_runner_forges(&self) {
|
||||||
self.string_forge.maybe_sweep(self.config.max_dead_strings.or(Some(1)));
|
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
|
// test262 creates a fresh VM/session per test. At this boundary no JS object
|
||||||
|
|||||||
Reference in New Issue
Block a user