mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 02:31:13 +08:00
refactor: 字符串值改用 JsString 指针表示,键与字面量改用 PermInterner
This commit is contained in:
@@ -4,7 +4,7 @@ use oxide_types::value::JsValue;
|
||||
|
||||
use crate::kernel::BuiltinDirtySet;
|
||||
use crate::shape_forge::{ShapeForge, EMPTY_SHAPE_ID};
|
||||
use crate::string_forge::StringForge;
|
||||
use crate::string_forge::PermInterner;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! bind_method {
|
||||
@@ -219,12 +219,12 @@ pub struct BuiltinWorld {
|
||||
pub stub_objects: Vec<P<JsObject>>,
|
||||
}
|
||||
|
||||
fn intern_label(string_forge: &StringForge, label: &str) -> u32 {
|
||||
fn intern_label(string_forge: &PermInterner, label: &str) -> u32 {
|
||||
string_forge.intern(label).0
|
||||
}
|
||||
|
||||
fn make_pair(
|
||||
string_forge: &StringForge, shape_forge: &ShapeForge, name: &str, si_prototype: u32, si_constructor: u32,
|
||||
string_forge: &PermInterner, shape_forge: &ShapeForge, name: &str, si_prototype: u32, si_constructor: u32,
|
||||
si_name: u32,
|
||||
) -> (P<JsObject>, P<JsObject>) {
|
||||
intern_label(string_forge, name);
|
||||
@@ -258,7 +258,7 @@ struct BuiltinLabels {
|
||||
name: u32,
|
||||
}
|
||||
|
||||
fn builtin_labels(string_forge: &StringForge) -> BuiltinLabels {
|
||||
fn builtin_labels(string_forge: &PermInterner) -> BuiltinLabels {
|
||||
let labels = BuiltinLabels {
|
||||
prototype: intern_label(string_forge, "prototype"),
|
||||
constructor: intern_label(string_forge, "constructor"),
|
||||
@@ -271,7 +271,7 @@ fn builtin_labels(string_forge: &StringForge) -> BuiltinLabels {
|
||||
}
|
||||
|
||||
fn make_named_pair(
|
||||
string_forge: &StringForge, shape_forge: &ShapeForge, labels: BuiltinLabels, name: &str,
|
||||
string_forge: &PermInterner, shape_forge: &ShapeForge, labels: BuiltinLabels, name: &str,
|
||||
) -> (P<JsObject>, P<JsObject>) {
|
||||
make_pair(string_forge, shape_forge, name, labels.prototype, labels.constructor, labels.name)
|
||||
}
|
||||
@@ -324,7 +324,7 @@ struct TypedArrayFamily {
|
||||
}
|
||||
|
||||
fn make_typed_array_family(
|
||||
string_forge: &StringForge, shape_forge: &ShapeForge, labels: BuiltinLabels, object_proto: &P<JsObject>,
|
||||
string_forge: &PermInterner, shape_forge: &ShapeForge, labels: BuiltinLabels, object_proto: &P<JsObject>,
|
||||
) -> TypedArrayFamily {
|
||||
let obj_proto_val = JsValue::from_js_object(object_proto.as_ptr() as *mut JsObject);
|
||||
let typed_array_proto = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, obj_proto_val));
|
||||
@@ -472,7 +472,7 @@ impl BuiltinWorld {
|
||||
JsValue::from_js_object(self.function_proto.as_ptr() as *mut JsObject)
|
||||
}
|
||||
|
||||
pub fn new(string_forge: &StringForge, shape_forge: &ShapeForge) -> Self {
|
||||
pub fn new(string_forge: &PermInterner, shape_forge: &ShapeForge) -> Self {
|
||||
let labels = builtin_labels(string_forge);
|
||||
|
||||
let (object_proto, object_constructor) = make_named_pair(string_forge, shape_forge, labels, "Object");
|
||||
@@ -578,7 +578,7 @@ impl BuiltinWorld {
|
||||
}
|
||||
|
||||
pub fn rebuild_with_dirty(
|
||||
current: &BuiltinWorld, string_forge: &StringForge, shape_forge: &ShapeForge, dirty: &BuiltinDirtySet,
|
||||
current: &BuiltinWorld, string_forge: &PermInterner, shape_forge: &ShapeForge, dirty: &BuiltinDirtySet,
|
||||
) -> BuiltinWorld {
|
||||
let labels = builtin_labels(string_forge);
|
||||
|
||||
@@ -796,7 +796,7 @@ impl BuiltinWorld {
|
||||
world
|
||||
}
|
||||
|
||||
pub fn bind_object_methods(&self, methods: &ObjectMethods, string_forge: &StringForge, shape_forge: &ShapeForge) {
|
||||
pub fn bind_object_methods(&self, methods: &ObjectMethods, string_forge: &PermInterner, shape_forge: &ShapeForge) {
|
||||
let ctor_ptr = P::as_ptr(&self.object_constructor) as *mut JsObject;
|
||||
let ctor = unsafe { &mut *ctor_ptr };
|
||||
bind_methods!(
|
||||
@@ -843,7 +843,7 @@ impl BuiltinWorld {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind_array_methods(&self, methods: &ArrayMethods, string_forge: &StringForge, shape_forge: &ShapeForge) {
|
||||
pub fn bind_array_methods(&self, methods: &ArrayMethods, string_forge: &PermInterner, shape_forge: &ShapeForge) {
|
||||
let ctor_ptr = P::as_ptr(&self.array_constructor) as *mut JsObject;
|
||||
let ctor = unsafe { &mut *ctor_ptr };
|
||||
bind_methods!(self, ctor, string_forge, shape_forge, ("isArray", methods.is_array, 1),);
|
||||
@@ -895,7 +895,7 @@ impl BuiltinWorld {
|
||||
debug_assert!(shape_forge.lookup_position(proto.shape_id(), si).is_some());
|
||||
}
|
||||
|
||||
pub fn bind_error_methods(&self, methods: &ErrorMethods, string_forge: &StringForge, shape_forge: &ShapeForge) {
|
||||
pub fn bind_error_methods(&self, methods: &ErrorMethods, string_forge: &PermInterner, shape_forge: &ShapeForge) {
|
||||
let ctor_ptr = P::as_ptr(&self.error_constructor) as *mut JsObject;
|
||||
let ctor = unsafe { &mut *ctor_ptr };
|
||||
bind_methods!(
|
||||
@@ -924,7 +924,7 @@ impl BuiltinWorld {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn bind_string_methods(&self, methods: &StringMethods, string_forge: &StringForge, shape_forge: &ShapeForge) {
|
||||
pub fn bind_string_methods(&self, methods: &StringMethods, string_forge: &PermInterner, shape_forge: &ShapeForge) {
|
||||
let ctor_ptr = P::as_ptr(&self.string_constructor) as *mut JsObject;
|
||||
let ctor = unsafe { &mut *ctor_ptr };
|
||||
bind_methods!(self, ctor, string_forge, shape_forge, ("fromCharCode", methods.from_char_code, 1),);
|
||||
@@ -966,7 +966,7 @@ impl BuiltinWorld {
|
||||
}
|
||||
|
||||
pub fn bind_function_methods(
|
||||
&self, methods: &FunctionMethods, string_forge: &StringForge, shape_forge: &ShapeForge,
|
||||
&self, methods: &FunctionMethods, string_forge: &PermInterner, shape_forge: &ShapeForge,
|
||||
) {
|
||||
let proto_ptr = P::as_ptr(&self.function_proto) as *mut JsObject;
|
||||
let proto = unsafe { &mut *proto_ptr };
|
||||
@@ -984,7 +984,7 @@ impl BuiltinWorld {
|
||||
}
|
||||
|
||||
pub fn bind_method(
|
||||
&self, proto: &mut JsObject, shape_forge: &ShapeForge, string_forge: &StringForge, method_name: &str,
|
||||
&self, proto: &mut JsObject, shape_forge: &ShapeForge, string_forge: &PermInterner, method_name: &str,
|
||||
native_fn_ptr: NativeFnPtr, arg_count: u8,
|
||||
) -> Result<(), String> {
|
||||
Self::bind_method_static(
|
||||
@@ -999,7 +999,7 @@ impl BuiltinWorld {
|
||||
}
|
||||
|
||||
pub fn bind_method_static(
|
||||
proto: &mut JsObject, shape_forge: &ShapeForge, string_forge: &StringForge, method_name: &str,
|
||||
proto: &mut JsObject, shape_forge: &ShapeForge, string_forge: &PermInterner, method_name: &str,
|
||||
native_fn_ptr: NativeFnPtr, arg_count: u8, wrapper_proto: JsValue,
|
||||
) -> Result<(), String> {
|
||||
let si = string_forge.intern(method_name).0;
|
||||
@@ -1022,7 +1022,7 @@ impl BuiltinWorld {
|
||||
wrapper.set_shape_id(name_shape);
|
||||
wrapper
|
||||
.ensure_hash_props()
|
||||
.push(JsValue::string(string_forge.intern(method_name).0, 0));
|
||||
.push(JsValue::perm_string(string_forge.string_ptr(si)));
|
||||
let wrapper_val = JsValue::from_js_object(Box::into_raw(wrapper));
|
||||
let new_shape = shape_forge.make_shape(proto.shape_id(), si);
|
||||
proto.set_shape_id(new_shape);
|
||||
@@ -1036,10 +1036,10 @@ impl BuiltinWorld {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::shape_forge::{ShapeForge, EMPTY_SHAPE_ID};
|
||||
use crate::string_forge::StringForge;
|
||||
use crate::string_forge::PermInterner;
|
||||
|
||||
fn make_world() -> BuiltinWorld {
|
||||
let sf = StringForge::new();
|
||||
let sf = PermInterner::new();
|
||||
let sh = ShapeForge::new();
|
||||
BuiltinWorld::new(&sf, &sh)
|
||||
}
|
||||
@@ -1115,7 +1115,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn builtin_rebuild_with_dirty_reuses_clean_fields() {
|
||||
let sf = StringForge::new();
|
||||
let sf = PermInterner::new();
|
||||
let sh = ShapeForge::new();
|
||||
let w = BuiltinWorld::new(&sf, &sh);
|
||||
let rebuilt = BuiltinWorld::rebuild_with_dirty(&w, &sf, &sh, &crate::kernel::BuiltinDirtySet::default());
|
||||
@@ -1127,7 +1127,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn builtin_rebuild_with_dirty_replaces_only_dirty_group() {
|
||||
let sf = StringForge::new();
|
||||
let sf = PermInterner::new();
|
||||
let sh = ShapeForge::new();
|
||||
let w = BuiltinWorld::new(&sf, &sh);
|
||||
let dirty = crate::kernel::BuiltinDirtySet {
|
||||
@@ -1144,7 +1144,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn builtin_rebuild_with_dirty_repairs_ctor_proto_links() {
|
||||
let sf = StringForge::new();
|
||||
let sf = PermInterner::new();
|
||||
let sh = ShapeForge::new();
|
||||
let w = BuiltinWorld::new(&sf, &sh);
|
||||
let dirty = crate::kernel::BuiltinDirtySet {
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::code_forge::CodeForge;
|
||||
use crate::logging::{init_logging, LogLevel, SUBSYSTEM_COUNT};
|
||||
use crate::prop_forge::PropForge;
|
||||
use crate::shape_forge::{ShapeForge, EMPTY_SHAPE_ID};
|
||||
use crate::string_forge::StringForge;
|
||||
use crate::string_forge::PermInterner;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KernelConfig {
|
||||
@@ -105,7 +105,7 @@ impl Default for KernelConfig {
|
||||
/// Never rebuilt after construction — forge tables are append-only.
|
||||
pub struct KernelCore {
|
||||
pub config: KernelConfig,
|
||||
pub string_forge: Arc<StringForge>,
|
||||
pub perm_interner: Arc<PermInterner>,
|
||||
pub shape_forge: Arc<ShapeForge>,
|
||||
pub code_forge: Arc<CodeForge>,
|
||||
pub prop_forge: Arc<PropForge>,
|
||||
@@ -114,7 +114,7 @@ pub struct KernelCore {
|
||||
impl KernelCore {
|
||||
pub fn new(config: KernelConfig) -> Arc<Self> {
|
||||
init_logging(&config.log_levels);
|
||||
let string_forge = Arc::new(StringForge::new());
|
||||
let perm_interner = Arc::new(PermInterner::new());
|
||||
let shape_forge = Arc::new(ShapeForge::new());
|
||||
let code_forge = Arc::new(CodeForge::new(
|
||||
NonZeroUsize::new(config.max_cached_modules).expect("max_cached_modules must be greater than zero"),
|
||||
@@ -122,15 +122,15 @@ impl KernelCore {
|
||||
let prop_forge = Arc::new(PropForge::new());
|
||||
Arc::new(Self {
|
||||
config,
|
||||
string_forge,
|
||||
perm_interner,
|
||||
shape_forge,
|
||||
code_forge,
|
||||
prop_forge,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn string_forge(&self) -> &Arc<StringForge> {
|
||||
&self.string_forge
|
||||
pub fn perm_interner(&self) -> &Arc<PermInterner> {
|
||||
&self.perm_interner
|
||||
}
|
||||
|
||||
pub fn shape_forge(&self) -> &Arc<ShapeForge> {
|
||||
@@ -166,7 +166,8 @@ impl KernelCore {
|
||||
}
|
||||
|
||||
pub fn sweep_runner_forges(&self) {
|
||||
self.string_forge.maybe_sweep(self.config.max_dead_strings.or(Some(1)));
|
||||
// The key interner is append-only (no per-run sweep); only the transient
|
||||
// shape/prop tables need bounding at the test262 per-test boundary.
|
||||
// test262 creates a fresh VM/session per test. At this boundary no JS object
|
||||
// from prior tests may retain transient shapes/templates.
|
||||
if self.shape_forge.len() > 50_000 {
|
||||
@@ -394,9 +395,9 @@ impl KernelSession {
|
||||
fn new_global_object(core: &KernelCore) -> P<JsObject> {
|
||||
let mut global_obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null());
|
||||
|
||||
let si_nan = core.string_forge.intern("NaN").0;
|
||||
let si_undef = core.string_forge.intern("undefined").0;
|
||||
let si_infinity = core.string_forge.intern("Infinity").0;
|
||||
let si_nan = core.perm_interner.intern("NaN").0;
|
||||
let si_undef = core.perm_interner.intern("undefined").0;
|
||||
let si_infinity = core.perm_interner.intern("Infinity").0;
|
||||
|
||||
let nan_shape = core.shape_forge.make_shape(EMPTY_SHAPE_ID, si_nan);
|
||||
global_obj.set_shape_id(nan_shape);
|
||||
@@ -416,7 +417,7 @@ impl KernelSession {
|
||||
/// Build a fresh session from a KernelCore. All string/shape intern calls hit
|
||||
/// cache on second and subsequent calls — net cost is < 0.5 ms.
|
||||
pub fn new(core: &KernelCore) -> Self {
|
||||
let builtin_world = Arc::new(BuiltinWorld::new(&core.string_forge, &core.shape_forge));
|
||||
let builtin_world = Arc::new(BuiltinWorld::new(&core.perm_interner, &core.shape_forge));
|
||||
let global_object = Self::new_global_object(core);
|
||||
let builtin_snapshot = BuiltinSnapshot::new(&builtin_world, &global_object);
|
||||
|
||||
@@ -536,7 +537,7 @@ impl KernelSession {
|
||||
if dirty.any_builtin_dirty() {
|
||||
self.builtin_world = Arc::new(BuiltinWorld::rebuild_with_dirty(
|
||||
&self.builtin_world,
|
||||
core.string_forge.as_ref(),
|
||||
core.perm_interner.as_ref(),
|
||||
core.shape_forge.as_ref(),
|
||||
&dirty,
|
||||
));
|
||||
@@ -553,8 +554,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_kernel_new() {
|
||||
let core = KernelCore::new(KernelConfig::minimal());
|
||||
let (i1, _) = core.string_forge().intern("test");
|
||||
let (i2, _) = core.string_forge().intern("test");
|
||||
let (i1, _) = core.perm_interner().intern("test");
|
||||
let (i2, _) = core.perm_interner().intern("test");
|
||||
assert_eq!(i1, i2);
|
||||
}
|
||||
|
||||
@@ -575,8 +576,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_kernel_string_forge() {
|
||||
let core = KernelCore::new(KernelConfig::minimal());
|
||||
let (i1, _) = core.string_forge().intern("hello");
|
||||
let (i2, _) = core.string_forge().intern("hello");
|
||||
let (i1, _) = core.perm_interner().intern("hello");
|
||||
let (i2, _) = core.perm_interner().intern("hello");
|
||||
assert_eq!(i1, i2);
|
||||
}
|
||||
|
||||
@@ -596,9 +597,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_session_rebuild_shares_forges() {
|
||||
let core = KernelCore::new(KernelConfig::minimal());
|
||||
let (i1, _) = core.string_forge().intern("hello");
|
||||
let (i1, _) = core.perm_interner().intern("hello");
|
||||
let _s2 = KernelSession::new(&core);
|
||||
let (i2, _) = core.string_forge().intern("hello");
|
||||
let (i2, _) = core.perm_interner().intern("hello");
|
||||
assert_eq!(i1, i2);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,142 +1,137 @@
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::RwLock;
|
||||
|
||||
use hashbrown::hash_map::DefaultHashBuilder as FxBuildHasher;
|
||||
use hashbrown::HashMap;
|
||||
use dashmap::DashMap;
|
||||
use oxide_types::object::JsString;
|
||||
use rustc_hash::FxHasher;
|
||||
|
||||
use crate::{kernel_debug, kernel_trace};
|
||||
|
||||
pub fn hash16(s: &str) -> u16 {
|
||||
let mut h = rustc_hash::FxHasher::default();
|
||||
/// Full 64-bit content hash. Replaces the old 16-bit `hash16` so the interner's
|
||||
/// hash→candidate map has negligible collision risk.
|
||||
fn hash64(s: &str) -> u64 {
|
||||
let mut h = FxHasher::default();
|
||||
s.hash(&mut h);
|
||||
(h.finish() >> 48) as u16
|
||||
h.finish()
|
||||
}
|
||||
|
||||
pub struct StringEntry {
|
||||
pub(crate) data: String,
|
||||
pub(crate) ref_count: AtomicU32,
|
||||
pub hash: u16,
|
||||
/// One interned key. `data` is a leaked `&'static str` — permanent keys are never
|
||||
/// freed (append-only by design), so the leak is the storage model, not a bug.
|
||||
#[derive(Clone, Copy)]
|
||||
struct PermEntry {
|
||||
data: &'static str,
|
||||
hash: u64,
|
||||
}
|
||||
|
||||
struct StringForgeInner {
|
||||
map: HashMap<String, u32, FxBuildHasher>,
|
||||
entries: Vec<StringEntry>,
|
||||
}
|
||||
|
||||
/// Global string interning pool shared by all VMs.
|
||||
/// Append-only, never-move, lock-free-read key interner shared by all VMs.
|
||||
///
|
||||
/// Concurrency model:
|
||||
/// `StringForge` keeps a single `RwLock` around the interner state.
|
||||
/// - `lookup()` takes a shared read lock and scales across readers.
|
||||
/// - `intern()` uses a read-check then write-check pattern to keep hot-path hits cheap.
|
||||
/// - New string insertion is serialized, which matches the workload here: most strings
|
||||
/// are interned early and later accesses are dominated by reads.
|
||||
pub struct StringForge {
|
||||
inner: RwLock<StringForgeInner>,
|
||||
/// Replaces the old ref-counted `StringForge` (and its buggy `maybe_sweep`
|
||||
/// renumber path). Keys (property names, method names) are interned once and
|
||||
/// addressed by a stable `u32` id that the shape/IC system keys on. Runtime
|
||||
/// string *values* are no longer interned here — they are heap `JsString`
|
||||
/// pointers (see `oxide_vm::Vm::new_string`).
|
||||
///
|
||||
/// Concurrency:
|
||||
/// - `hash_map` (`DashMap`) gives sharded, lock-free reads of the hash→candidates
|
||||
/// mapping on the hot intern path.
|
||||
/// - `entries` sits behind a short `RwLock`; reads copy out a `&'static str`
|
||||
/// (Copy) so the borrow outlives the guard.
|
||||
pub struct PermInterner {
|
||||
hash_map: DashMap<u64, Vec<u32>>,
|
||||
entries: RwLock<Vec<PermEntry>>,
|
||||
/// Lazily materialized permanent `JsString` values, indexed by entry id.
|
||||
/// Used when a permanent key must also exist as a JS string *value*
|
||||
/// (e.g. method names exposed as property values at builtin init).
|
||||
permanent_strings: RwLock<Vec<Option<Box<JsString>>>>,
|
||||
}
|
||||
|
||||
impl StringForge {
|
||||
impl PermInterner {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: RwLock::new(StringForgeInner {
|
||||
map: HashMap::with_hasher(FxBuildHasher::default()),
|
||||
entries: Vec::new(),
|
||||
}),
|
||||
hash_map: DashMap::new(),
|
||||
entries: RwLock::new(Vec::new()),
|
||||
permanent_strings: RwLock::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intern(&self, s: &str) -> (u32, u16) {
|
||||
{
|
||||
let inner = self.inner.read().unwrap();
|
||||
if let Some(&idx) = inner.map.get(s) {
|
||||
let entry = &inner.entries[idx as usize];
|
||||
entry.ref_count.fetch_add(1, Ordering::Release);
|
||||
kernel_trace!("StringForge intern hit idx={}", idx);
|
||||
return (idx, entry.hash);
|
||||
/// Intern a key. Returns its stable id and full 64-bit hash. Each unique
|
||||
/// string is stored exactly once (single allocation, no double `to_string`).
|
||||
pub fn intern(&self, s: &str) -> (u32, u64) {
|
||||
let hash = hash64(s);
|
||||
|
||||
// Fast path: lock-free candidate read, short entries read-lock.
|
||||
if let Some(candidates) = self.hash_map.get(&hash) {
|
||||
let entries = self.entries.read().unwrap();
|
||||
for &id in candidates.iter() {
|
||||
if entries[id as usize].data == s {
|
||||
kernel_trace!("PermInterner intern hit id={}", id);
|
||||
return (id, hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
if let Some(&idx) = inner.map.get(s) {
|
||||
let entry = &inner.entries[idx as usize];
|
||||
entry.ref_count.fetch_add(1, Ordering::Release);
|
||||
kernel_trace!("StringForge intern hit idx={}", idx);
|
||||
return (idx, entry.hash);
|
||||
}
|
||||
|
||||
let h = hash16(s);
|
||||
let idx = inner.entries.len() as u32;
|
||||
inner.map.insert(s.to_string(), idx);
|
||||
inner.entries.push(StringEntry {
|
||||
data: s.to_string(),
|
||||
ref_count: AtomicU32::new(1),
|
||||
hash: h,
|
||||
});
|
||||
kernel_debug!("StringForge intern new idx={} len={}", idx, s.len());
|
||||
(idx, h)
|
||||
}
|
||||
|
||||
pub fn lookup(&self, idx: u32) -> Option<String> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner.entries.get(idx as usize).map(|e| e.data.clone())
|
||||
}
|
||||
|
||||
pub fn get_hash(&self, idx: u32) -> Option<u16> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner.entries.get(idx as usize).map(|e| e.hash)
|
||||
}
|
||||
|
||||
pub fn decref(&self, idx: u32) {
|
||||
let inner = self.inner.read().unwrap();
|
||||
if let Some(entry) = inner.entries.get(idx as usize) {
|
||||
entry.ref_count.fetch_sub(1, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maybe_sweep(&self, max_dead: Option<usize>) {
|
||||
let threshold = match max_dead {
|
||||
Some(t) => t,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let dead_count = inner
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|e| e.ref_count.load(Ordering::Acquire) == 0)
|
||||
.count();
|
||||
|
||||
if dead_count < threshold {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut new_map: HashMap<String, u32, FxBuildHasher> = HashMap::with_hasher(FxBuildHasher::default());
|
||||
let mut new_entries = Vec::with_capacity(inner.entries.len() - dead_count);
|
||||
|
||||
for entry in inner.entries.drain(..) {
|
||||
let rc = entry.ref_count.load(Ordering::Acquire);
|
||||
if rc > 0 {
|
||||
let new_idx = new_entries.len() as u32;
|
||||
new_map.insert(entry.data.clone(), new_idx);
|
||||
new_entries.push(entry);
|
||||
// Slow path: append under write lock, re-checking for a racing insert.
|
||||
let mut entries = self.entries.write().unwrap();
|
||||
if let Some(candidates) = self.hash_map.get(&hash) {
|
||||
for &id in candidates.iter() {
|
||||
if entries[id as usize].data == s {
|
||||
return (id, hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner.map = new_map;
|
||||
inner.entries = new_entries;
|
||||
let id = entries.len() as u32;
|
||||
let leaked: &'static str = Box::leak(s.to_string().into_boxed_str());
|
||||
entries.push(PermEntry { data: leaked, hash });
|
||||
drop(entries);
|
||||
self.hash_map.entry(hash).or_default().push(id);
|
||||
kernel_debug!("PermInterner intern new id={} len={}", id, s.len());
|
||||
(id, hash)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.read().unwrap().entries.len()
|
||||
/// Resolve a key id to its text with zero clone. The returned `&'static str`
|
||||
/// is valid for the program lifetime (keys are never freed).
|
||||
pub fn lookup(&self, id: u32) -> Option<&'static str> {
|
||||
let entries = self.entries.read().unwrap();
|
||||
entries.get(id as usize).map(|e| e.data)
|
||||
}
|
||||
|
||||
/// Full 64-bit hash for a key id.
|
||||
pub fn get_hash(&self, id: u32) -> Option<u64> {
|
||||
let entries = self.entries.read().unwrap();
|
||||
entries.get(id as usize).map(|e| e.hash)
|
||||
}
|
||||
|
||||
/// Total number of unique keys interned.
|
||||
pub fn entry_count(&self) -> u32 {
|
||||
self.entries.read().unwrap().len() as u32
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
self.entry_count() == 0
|
||||
}
|
||||
|
||||
/// Materialize (once) and return a stable pointer to a permanent `JsString`
|
||||
/// for the given key id. The `JsString` lives for the program lifetime.
|
||||
pub fn string_ptr(&self, id: u32) -> *const JsString {
|
||||
{
|
||||
let perm = self.permanent_strings.read().unwrap();
|
||||
if let Some(Some(boxed)) = perm.get(id as usize) {
|
||||
return boxed.as_ref() as *const JsString;
|
||||
}
|
||||
}
|
||||
let text = self.lookup(id).unwrap_or("");
|
||||
let mut perm = self.permanent_strings.write().unwrap();
|
||||
if perm.len() <= id as usize {
|
||||
perm.resize_with(id as usize + 1, || None);
|
||||
}
|
||||
if perm[id as usize].is_none() {
|
||||
perm[id as usize] = Some(Box::new(JsString::new(text.to_string())));
|
||||
}
|
||||
perm[id as usize].as_ref().unwrap().as_ref() as *const JsString
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StringForge {
|
||||
impl Default for PermInterner {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
@@ -147,67 +142,63 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hash16_same() {
|
||||
assert_eq!(hash16("hello"), hash16("hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash16_different() {
|
||||
assert_ne!(hash16("hello"), hash16("world"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intern_dedup() {
|
||||
let string_forge = StringForge::new();
|
||||
let (i1, h1) = string_forge.intern("abc");
|
||||
let (i2, h2) = string_forge.intern("abc");
|
||||
fn intern_dedup() {
|
||||
let interner = PermInterner::new();
|
||||
let (i1, h1) = interner.intern("abc");
|
||||
let (i2, h2) = interner.intern("abc");
|
||||
assert_eq!(i1, i2);
|
||||
assert_eq!(h1, h2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intern_different() {
|
||||
let string_forge = StringForge::new();
|
||||
let (i1, _) = string_forge.intern("x");
|
||||
let (i2, _) = string_forge.intern("y");
|
||||
fn intern_different() {
|
||||
let interner = PermInterner::new();
|
||||
let (i1, _) = interner.intern("x");
|
||||
let (i2, _) = interner.intern("y");
|
||||
assert_ne!(i1, i2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_found() {
|
||||
let string_forge = StringForge::new();
|
||||
let (idx, _) = string_forge.intern("hello");
|
||||
assert_eq!(string_forge.lookup(idx), Some("hello".to_string()));
|
||||
fn lookup_zero_clone() {
|
||||
let interner = PermInterner::new();
|
||||
let (id, _) = interner.intern("hello");
|
||||
assert_eq!(interner.lookup(id), Some("hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_not_found() {
|
||||
let string_forge = StringForge::new();
|
||||
assert_eq!(string_forge.lookup(99999), None);
|
||||
fn lookup_not_found() {
|
||||
let interner = PermInterner::new();
|
||||
assert_eq!(interner.lookup(99999), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ref_count_and_decref() {
|
||||
let string_forge = StringForge::new();
|
||||
let (idx, _) = string_forge.intern("s");
|
||||
assert_eq!(string_forge.lookup(idx), Some("s".to_string()));
|
||||
string_forge.decref(idx);
|
||||
assert_eq!(string_forge.lookup(idx), Some("s".to_string()));
|
||||
fn entry_count_monotonic() {
|
||||
let interner = PermInterner::new();
|
||||
assert_eq!(interner.entry_count(), 0);
|
||||
interner.intern("a");
|
||||
interner.intern("b");
|
||||
interner.intern("a");
|
||||
assert_eq!(interner.entry_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maybe_sweep_noop_when_none() {
|
||||
let string_forge = StringForge::new();
|
||||
string_forge.intern("live");
|
||||
string_forge.maybe_sweep(None);
|
||||
assert!(string_forge.lookup(0).is_some());
|
||||
fn many_unique_no_collision() {
|
||||
let interner = PermInterner::new();
|
||||
for i in 0..10_000 {
|
||||
let s = format!("key{i}");
|
||||
let (id, _) = interner.intern(&s);
|
||||
assert_eq!(interner.lookup(id), Some(&*Box::leak(s.into_boxed_str())));
|
||||
}
|
||||
assert_eq!(interner.entry_count(), 10_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maybe_sweep_skip_below_threshold() {
|
||||
let string_forge = StringForge::new();
|
||||
string_forge.intern("live");
|
||||
string_forge.maybe_sweep(Some(100));
|
||||
assert_eq!(string_forge.lookup(0), Some("live".to_string()));
|
||||
fn string_ptr_roundtrip() {
|
||||
let interner = PermInterner::new();
|
||||
let (id, _) = interner.intern("perm");
|
||||
let ptr = interner.string_ptr(id);
|
||||
assert_eq!(unsafe { (*ptr).as_str() }, "perm");
|
||||
// Second call returns the same stable pointer (materialized once).
|
||||
assert_eq!(interner.string_ptr(id), ptr);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user