210 lines
6.9 KiB
Rust
210 lines
6.9 KiB
Rust
use std::hash::{Hash, Hasher};
|
||
use std::sync::RwLock;
|
||
|
||
use dashmap::DashMap;
|
||
use oxide_types::object::JsString;
|
||
use rustc_hash::FxHasher;
|
||
|
||
use crate::{kernel_debug, kernel_trace};
|
||
|
||
/// 完整 64 位内容哈希。替代旧的 16 位 `hash16`,使 interner 的
|
||
/// hash→候选表碰撞风险可忽略。
|
||
fn hash64(s: &str) -> u64 {
|
||
let mut h = FxHasher::default();
|
||
s.hash(&mut h);
|
||
h.finish()
|
||
}
|
||
|
||
/// 一条 intern 过的键。`data` 是泄漏的 `&'static str`——永久键从不释放
|
||
/// (按设计 append-only),所以泄漏即存储模型,而非 bug。
|
||
#[derive(Clone, Copy)]
|
||
struct PermEntry {
|
||
data: &'static str,
|
||
hash: u64,
|
||
}
|
||
|
||
/// 所有 VM 共享的 append-only、永不移动、读无锁的键 interner。
|
||
///
|
||
/// 取代旧的引用计数 `StringForge`(及其有缺陷的 `maybe_sweep` 重编号路径)。
|
||
/// 键(属性名、方法名)只 intern 一次,以 shape/IC 系统所依赖的稳定 `u32` id
|
||
/// 寻址。运行时字符串*值*不再在此 intern——它们是堆上的 `JsString`
|
||
/// 指针(见 `oxide_vm::Vm::new_string`)。
|
||
///
|
||
/// 并发性:
|
||
/// - `hash_map`(`DashMap`)在热 intern 路径上提供分片、无锁的
|
||
/// hash→候选映射读。
|
||
/// - `entries` 位于短 `RwLock` 之后;读时拷贝出 `&'static str`(Copy),
|
||
/// 使借用存活时间超过锁守卫。
|
||
pub struct PermInterner {
|
||
hash_map: DashMap<u64, Vec<u32>>,
|
||
entries: RwLock<Vec<PermEntry>>,
|
||
/// 惰性物化的永久 `JsString` 值,按下标索引。
|
||
/// 用于永久键还必须以 JS 字符串*值*存在的情形
|
||
/// (如 builtin 初始化时作为属性值暴露的方法名)。
|
||
permanent_strings: RwLock<Vec<Option<Box<JsString>>>>,
|
||
}
|
||
|
||
impl PermInterner {
|
||
/// 创建空 intern 表。
|
||
pub fn new() -> Self {
|
||
Self {
|
||
hash_map: DashMap::new(),
|
||
entries: RwLock::new(Vec::new()),
|
||
permanent_strings: RwLock::new(Vec::new()),
|
||
}
|
||
}
|
||
|
||
/// Intern 一个键。返回其稳定 id 与完整 64 位哈希。每个唯一字符串
|
||
/// 恰好存储一次(单次分配,无双重 `to_string`)。
|
||
pub fn intern(&self, s: &str) -> (u32, u64) {
|
||
let hash = hash64(s);
|
||
|
||
// 快路径:无锁候选读,短 entries 读锁。
|
||
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 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);
|
||
}
|
||
}
|
||
}
|
||
let id = entries.len() as u32;
|
||
let leaked: &'static str = Box::leak(s.to_string().into_boxed_str());
|
||
entries.push(PermEntry { data: leaked, hash });
|
||
let entry_count = entries.len();
|
||
drop(entries);
|
||
self.hash_map.entry(hash).or_default().push(id);
|
||
kernel_debug!("PermInterner intern new id={} len={}", id, s.len());
|
||
if entry_count % 1000 == 0 {
|
||
kernel_debug!("PermInterner stats: {} strings", entry_count);
|
||
}
|
||
(id, hash)
|
||
}
|
||
|
||
/// 以零克隆解析键 id 的文本。返回的 `&'static str`
|
||
/// 在整个程序生命周期内有效(键从不释放)。
|
||
pub fn lookup(&self, id: u32) -> Option<&'static str> {
|
||
let entries = self.entries.read().unwrap();
|
||
entries.get(id as usize).map(|e| e.data)
|
||
}
|
||
|
||
/// 键 id 的完整 64 位哈希。
|
||
pub fn get_hash(&self, id: u32) -> Option<u64> {
|
||
let entries = self.entries.read().unwrap();
|
||
entries.get(id as usize).map(|e| e.hash)
|
||
}
|
||
|
||
/// 全部唯一 intern 键的数量。
|
||
pub fn entry_count(&self) -> u32 {
|
||
self.entries.read().unwrap().len() as u32
|
||
}
|
||
|
||
/// 是否尚无任何 intern 过的 key。
|
||
pub fn is_empty(&self) -> bool {
|
||
self.entry_count() == 0
|
||
}
|
||
|
||
/// 物化(仅一次)并返回指定键 id 的永久 `JsString` 稳定指针。
|
||
/// 该 `JsString` 在整个程序生命周期内存活。
|
||
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 PermInterner {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
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 intern_different() {
|
||
let interner = PermInterner::new();
|
||
let (i1, _) = interner.intern("x");
|
||
let (i2, _) = interner.intern("y");
|
||
assert_ne!(i1, i2);
|
||
}
|
||
|
||
#[test]
|
||
fn lookup_zero_clone() {
|
||
let interner = PermInterner::new();
|
||
let (id, _) = interner.intern("hello");
|
||
assert_eq!(interner.lookup(id), Some("hello"));
|
||
}
|
||
|
||
#[test]
|
||
fn lookup_not_found() {
|
||
let interner = PermInterner::new();
|
||
assert_eq!(interner.lookup(99999), None);
|
||
}
|
||
|
||
#[test]
|
||
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 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 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");
|
||
// 二次调用返回同一稳定指针(仅物化一次)。
|
||
assert_eq!(interner.string_ptr(id), ptr);
|
||
}
|
||
}
|