perf: 字符串 +/+= 二元拼接改 Cons rope 惰性扁平化消除 O(n²)
JsString 拆 Flat/Cons 变体:Cons 经独立 ConsNode 保存左右子节点 + O(1) 字节长 + 惰性扁平化产物缓存(OnceLock 原子发布,产物挂节点生命周期)。 concat_strings 接线:总长 ≤ 128B 的小链急切扁平(与 rope 前同款,拼接即 消费场景零开销),大链 O(1) 链接——大段重复拼接从 O(n²) 拷贝降为 O(n) 链接,文本在消费点(.length/==/string_ref/方法族)首次扁平化后缓存。 GC 兼容:mark 收口 mark_string_live 传播 Cons 子节点与产物闭包(显式栈 防深链爆栈);sweep/full_reset 共用 drop 辅助连带释放产物;节点字节记账 用 O(1) byte_len。rope 节点登记 session 字符串表,与执行期字符串 GC (指令边界触发)天然兼容。JsString 结构 32B 与 rope 前同 bin,小串路径 零结构回退;unsafe impl Send/Sync 论证同 StringPtr(perm 恒 Flat)。
This commit is contained in:
@@ -78,22 +78,57 @@ impl Vm {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 字符串拼接热路径:一次预分配写齐两个操作数的 ToString 文本。
|
||||
/// 字符串拼接接线点(二元 `+`/`+=` 共用):O(1) 链接为 Cons(rope)节点,
|
||||
/// 消除逐次整串重拷贝。文本在消费时(`.length`/`==`/string_ref 等)惰性扁平化。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 按字符串操作数字节长预分配容量(余量覆盖数字/布尔等格式化文本)。
|
||||
/// 2. 两个操作数依次走 push_to_string(BigInt 输出十进制,Symbol 由调用方先行拒绝)。
|
||||
/// 3. 生成会话字符串写入目标寄存器。
|
||||
/// 1. 非字符串操作数先转成叶子字符串(`push_to_string` 同文本语义;小整数走
|
||||
/// 永久缓存零分配,其余 `to_string` + owned)。
|
||||
/// 2. 两个字符串经 `new_cons_string` 链接(每链接 1 次节点分配,对比原 O(n)
|
||||
/// 拷贝 + 结果串分配)。
|
||||
///
|
||||
/// # 副作用
|
||||
/// 新建一个会话字符串,登记到 session 生命周期。
|
||||
/// - 新建 Cons 节点 + 可能的数字叶子,登记到 session 生命周期。回收点仅在
|
||||
/// dispatch 指令边界(结果已写寄存器后检查),链接期子节点天然安全,
|
||||
/// 无需在途保护。
|
||||
fn concat_strings(&mut self, lhs: JsValue, rhs: JsValue) -> JsValue {
|
||||
// 小链急切扁平(与 rope 前同款实现):总字节长 ≤ 阈值时单次预分配
|
||||
// 写齐,零叶子转换 / Cons 节点 / 惰性扁平化开销——拼接即消费的小串
|
||||
// 场景与基线逐字节等价(该场景 rope 的 O(1) 链接收益低于其固定开销)。
|
||||
let lbytes = if lhs.is_string() { unsafe { (*lhs.as_string_ptr()).len() } } else { 0 };
|
||||
let rbytes = if rhs.is_string() { unsafe { (*rhs.as_string_ptr()).len() } } else { 0 };
|
||||
let mut buf = String::with_capacity(lbytes + rbytes + 32);
|
||||
coercion::push_to_string(lhs, &mut buf);
|
||||
coercion::push_to_string(rhs, &mut buf);
|
||||
self.new_string_owned(buf)
|
||||
if lbytes + rbytes <= Self::CONS_FLATTEN_BYTES {
|
||||
let mut buf = String::with_capacity(lbytes + rbytes + 32);
|
||||
coercion::push_to_string(lhs, &mut buf);
|
||||
coercion::push_to_string(rhs, &mut buf);
|
||||
return self.new_string_owned(buf);
|
||||
}
|
||||
// 大链走 Cons rope:非字符串操作数先转叶子,再 O(1) 链接(文本惰性扁平化)。
|
||||
let lv = if lhs.is_string() {
|
||||
lhs
|
||||
} else {
|
||||
self.string_leaf(lhs)
|
||||
};
|
||||
let rv = if rhs.is_string() {
|
||||
rhs
|
||||
} else {
|
||||
self.string_leaf(rhs)
|
||||
};
|
||||
self.new_cons_string(lv, rv)
|
||||
}
|
||||
|
||||
/// 把非字符串原语转为叶子字符串:0..=99 小整数命中永久缓存(零分配),
|
||||
/// 其余走 `to_string` + `new_string_owned`(语义与 `push_to_string` 一致)。
|
||||
fn string_leaf(&mut self, v: JsValue) -> JsValue {
|
||||
if v.is_int() {
|
||||
let n = v.as_int();
|
||||
if n >= 0 {
|
||||
if let Some(ptr) = oxide_kernel::string_forge::small_int_ptr(n as u32) {
|
||||
return JsValue::string(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.new_string_owned(coercion::to_string(v))
|
||||
}
|
||||
|
||||
/// CONCAT_N:多操作数拼接(连续 `+` 左结合链摊平)的两阶段 dispatch。
|
||||
|
||||
@@ -127,6 +127,53 @@ impl SessionGc {
|
||||
edges
|
||||
}
|
||||
|
||||
/// 把字符串指针标记为存活并传播 rope 闭包:Cons 迭代传播左右子节点与
|
||||
/// 扁平化产物(显式栈防深链爆栈)。perm 指针进表无害——sweep 只遍历
|
||||
/// `session_string_ptrs`。遗漏子节点 = 子节点被 sweep 释放 → 悬垂 UB,
|
||||
/// 全部字符串 insert 点必须收口此入口。
|
||||
fn mark_string_live(live: &mut HashSet<*mut JsString, FxBuildHasher>, root: *mut JsString) {
|
||||
if root.is_null() {
|
||||
return;
|
||||
}
|
||||
let mut stack = vec![root];
|
||||
while let Some(ptr) = stack.pop() {
|
||||
if ptr.is_null() || !live.insert(ptr) {
|
||||
continue;
|
||||
}
|
||||
// SAFETY: ptr 是合法 JsString 指针(session 或 perm),mark 期存活。
|
||||
let node = unsafe { &*ptr };
|
||||
if node.is_cons() {
|
||||
for child in node.cons_children() {
|
||||
// 子节点与父同属一个 JsString 堆对象族,const 转 mut 仅用于
|
||||
// 集合统一,不引入写操作。
|
||||
stack.push(child as *mut JsString);
|
||||
}
|
||||
let flat = node.flat_cache_ptr();
|
||||
if !flat.is_null() {
|
||||
stack.push(flat as *mut JsString);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放一个 session `JsString`(`Box::into_raw` 分配),连带释放 rope
|
||||
/// 扁平化产物。**不递归子节点**——子节点独立登记主表、各自判定存活,
|
||||
/// 递归即 double-free。
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` 必须是 `new_string`/`new_cons_string` 中 `Box::into_raw` 产生的非空
|
||||
/// 指针,仍登记在 session 字符串表(或由 full_reset 统一清表),且恰好
|
||||
/// 释放一次。
|
||||
pub(crate) unsafe fn drop_session_string_box(ptr: *mut JsString) {
|
||||
if ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
// 连带释放 rope 载荷(ConsNode 与其扁平化产物);Flat 串无载荷。
|
||||
// SAFETY: cons_node_ptr 由 new_cons 产生,本指针恰好释放一次。
|
||||
JsString::drop_cons_node((*ptr).cons_node_ptr());
|
||||
drop(Box::from_raw(ptr));
|
||||
}
|
||||
|
||||
/// 把 `obj` 直接持有的 session 字符串值记入 `live`。JsString 不持有 GC 引用,
|
||||
/// 因此"到达"一个字符串就等于标记它——不存在字符串 DFS 栈。永久字符串也会被
|
||||
/// 无害地记录;sweep 只遍历 `session_string_ptrs`,`live` 中的非 session 指针
|
||||
@@ -135,22 +182,22 @@ impl SessionGc {
|
||||
if let Some(elements) = obj.array_elements_vec() {
|
||||
for value in elements.iter() {
|
||||
if value.is_string() {
|
||||
live.insert(value.as_string_ptr_mut());
|
||||
Self::mark_string_live(live, value.as_string_ptr_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(props) = obj.hash_props_vec() {
|
||||
for value in props.iter() {
|
||||
if value.is_string() {
|
||||
live.insert(value.as_string_ptr_mut());
|
||||
Self::mark_string_live(live, value.as_string_ptr_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
if obj.captured_this().is_string() {
|
||||
live.insert(obj.captured_this().as_string_ptr_mut());
|
||||
Self::mark_string_live(live, obj.captured_this().as_string_ptr_mut());
|
||||
}
|
||||
if obj.home_object().is_string() {
|
||||
live.insert(obj.home_object().as_string_ptr_mut());
|
||||
Self::mark_string_live(live, obj.home_object().as_string_ptr_mut());
|
||||
}
|
||||
// 扫描 upvalue cell 中的字符串引用。
|
||||
for cell_ptr in obj.upvalues_slice() {
|
||||
@@ -159,43 +206,43 @@ impl SessionGc {
|
||||
}
|
||||
let cell = unsafe { &**cell_ptr };
|
||||
if cell.value.is_string() {
|
||||
live.insert(cell.value.as_string_ptr_mut());
|
||||
Self::mark_string_live(live, cell.value.as_string_ptr_mut());
|
||||
}
|
||||
}
|
||||
if obj.is_map() {
|
||||
for value in map::map_native_edges(obj) {
|
||||
if value.is_string() {
|
||||
live.insert(value.as_string_ptr_mut());
|
||||
Self::mark_string_live(live, value.as_string_ptr_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
if obj.is_set() {
|
||||
for value in set::set_native_edges(obj) {
|
||||
if value.is_string() {
|
||||
live.insert(value.as_string_ptr_mut());
|
||||
Self::mark_string_live(live, value.as_string_ptr_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
if obj.is_generator_obj() {
|
||||
for ptr in crate::generator::generator_native_string_edges(obj) {
|
||||
live.insert(ptr);
|
||||
Self::mark_string_live(live, ptr);
|
||||
}
|
||||
}
|
||||
if obj.is_promise_obj() {
|
||||
for value in crate::promise::promise_native_edges(obj) {
|
||||
if value.is_string() {
|
||||
live.insert(value.as_string_ptr_mut());
|
||||
Self::mark_string_live(live, value.as_string_ptr_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
if obj.is_async_obj() {
|
||||
for ptr in crate::async_func::async_native_string_edges(obj) {
|
||||
live.insert(ptr);
|
||||
Self::mark_string_live(live, ptr);
|
||||
}
|
||||
}
|
||||
if obj.is_async_generator_obj() {
|
||||
for ptr in crate::async_generator::async_generator_native_string_edges(obj) {
|
||||
live.insert(ptr);
|
||||
Self::mark_string_live(live, ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,9 +267,7 @@ impl SessionGc {
|
||||
stack.clear();
|
||||
live_strings.clear();
|
||||
for ptr in string_seeds {
|
||||
if !ptr.is_null() {
|
||||
live_strings.insert(ptr);
|
||||
}
|
||||
Self::mark_string_live(live_strings, ptr);
|
||||
}
|
||||
|
||||
for ptr in seeds {
|
||||
@@ -339,16 +384,17 @@ impl SessionGc {
|
||||
Self::drop_session_object_heap_data(obj_ptr) + size_of::<JsObject>() as u64
|
||||
}
|
||||
|
||||
/// 释放一个已死 session `JsString`(由 `Vm::new_string` 经 `Box::into_raw` 分配),
|
||||
/// 返回释放的字节数。与 `Vm::free_session_string_heap_data` 的释放一致,但只
|
||||
/// 选择性作用于单个已死指针。
|
||||
/// 释放一个已死 session `JsString`(由 `Vm::new_string`/`new_cons_string`
|
||||
/// 经 `Box::into_raw` 分配),返回释放的字节数(含 rope 扁平化产物)。
|
||||
/// 与 `Vm::free_session_string_heap_data` 的释放一致,但只选择性作用于
|
||||
/// 单个已死指针。
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` 必须是 `Vm::new_string` 中 `Box::into_raw(Box::new(JsString))` 产生的
|
||||
/// 非空指针,仍存在于 `session_string_ptrs`,且恰好释放一次。
|
||||
/// `ptr` 必须是 `Box::into_raw(Box::new(JsString))` 产生的非空指针,仍存在
|
||||
/// 于 `session_string_ptrs`,且恰好释放一次。
|
||||
unsafe fn drop_dead_session_string(ptr: *mut JsString) -> u64 {
|
||||
let bytes = (size_of::<JsString>() + (*ptr).len()) as u64;
|
||||
drop(Box::from_raw(ptr));
|
||||
Self::drop_session_string_box(ptr);
|
||||
bytes
|
||||
}
|
||||
|
||||
@@ -1473,4 +1519,112 @@ mod tests {
|
||||
assert_eq!(vm.gc_state.session_gc.total_collections, 0);
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&s.as_string_ptr_mut()));
|
||||
}
|
||||
|
||||
// ── rope(Cons)GC 传播闭包 ──
|
||||
|
||||
/// 构造 `left + right` 的 Cons 节点(返回 (父, 左, 右) 三指针)。
|
||||
fn make_cons_pair(vm: &mut Vm, l: &str, r: &str) -> (JsValue, *mut JsString, *mut JsString) {
|
||||
let left = vm.new_string(l);
|
||||
let right = vm.new_string(r);
|
||||
let parent = vm.new_cons_string(left, right);
|
||||
(parent, left.as_string_ptr_mut(), right.as_string_ptr_mut())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_survives_collection_with_children_propagated() {
|
||||
let mut vm = Vm::new();
|
||||
let (parent, l_ptr, r_ptr) = make_cons_pair(&mut vm, "left-part", "right-part");
|
||||
vm.regs[0] = parent;
|
||||
|
||||
collect(&mut vm);
|
||||
|
||||
// 父(寄存器根)+ 子节点(经传播闭包)全部存活,内容可读。
|
||||
let parent_ptr = parent.as_string_ptr_mut();
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&parent_ptr));
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&l_ptr));
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&r_ptr));
|
||||
assert_eq!(unsafe { (*parent_ptr).as_str() }, "left-partright-part");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_children_swept_when_parent_dead() {
|
||||
let mut vm = Vm::new();
|
||||
let (parent, l_ptr, r_ptr) = make_cons_pair(&mut vm, "left", "right");
|
||||
let parent_ptr = parent.as_string_ptr_mut();
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&parent_ptr));
|
||||
|
||||
// 父与子均无根引用 → 整树回收。
|
||||
collect(&mut vm);
|
||||
|
||||
assert!(!vm.gc_state.session_string_ptrs.contains(&parent_ptr));
|
||||
assert!(!vm.gc_state.session_string_ptrs.contains(&l_ptr));
|
||||
assert!(!vm.gc_state.session_string_ptrs.contains(&r_ptr));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_product_freed_with_parent() {
|
||||
let mut vm = Vm::new();
|
||||
let (parent, _, _) = make_cons_pair(&mut vm, "left-part", "right-part");
|
||||
let parent_ptr = parent.as_string_ptr_mut();
|
||||
// 触发扁平化:产物发布到 flat_cache。
|
||||
assert_eq!(unsafe { (*parent_ptr).flat_str() }, "left-partright-part");
|
||||
let flat_ptr = unsafe { (*parent_ptr).flat_cache_ptr() };
|
||||
assert!(!flat_ptr.is_null());
|
||||
|
||||
collect(&mut vm);
|
||||
|
||||
// 父死 → 连带释放产物(不 double-free、不泄漏;产物从不进 session 表)。
|
||||
let flat_mut = flat_ptr as *mut JsString;
|
||||
assert!(!vm.gc_state.session_string_ptrs.contains(&parent_ptr));
|
||||
assert!(!vm.gc_state.session_string_ptrs.contains(&flat_mut));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_deep_chain_mark_iterative() {
|
||||
let mut vm = Vm::new();
|
||||
// 1024 层左倾链:mark 传播用显式栈,不爆栈、不遗漏子节点。
|
||||
let mut chain = vm.new_string("root");
|
||||
for _ in 0..1024 {
|
||||
let leaf = vm.new_string("x");
|
||||
chain = vm.new_cons_string(chain, leaf);
|
||||
}
|
||||
vm.regs[0] = chain;
|
||||
let chain_ptr = chain.as_string_ptr_mut();
|
||||
|
||||
collect(&mut vm);
|
||||
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&chain_ptr));
|
||||
assert_eq!(unsafe { (*chain_ptr).as_str() }, format!("root{}", "x".repeat(1024)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_perm_child_untouched_by_sweep() {
|
||||
let mut vm = Vm::new();
|
||||
let perm = vm.perm_string("perm-leaf");
|
||||
let perm_ptr = perm.as_string_ptr_mut();
|
||||
let session = vm.new_string("session-leaf");
|
||||
let session_ptr = session.as_string_ptr_mut();
|
||||
let parent = vm.new_cons_string(perm, session);
|
||||
vm.regs[0] = parent;
|
||||
|
||||
collect(&mut vm);
|
||||
|
||||
// perm 子节点永不释放(不在 session 表);session 子节点随父存活。
|
||||
assert!(!vm.gc_state.session_string_ptrs.contains(&perm_ptr));
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&session_ptr));
|
||||
assert_eq!(unsafe { (*parent.as_string_ptr_mut()).as_str() }, "perm-leafsession-leaf");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_reset_frees_rope_and_product() {
|
||||
let mut vm = Vm::new();
|
||||
let (parent, _, _) = make_cons_pair(&mut vm, "left-part", "right-part");
|
||||
// 触发扁平化(产物挂在父上)。
|
||||
assert_eq!(unsafe { (*parent.as_string_ptr_mut()).flat_str() }, "left-partright-part");
|
||||
vm.regs[0] = parent;
|
||||
|
||||
// full_reset 清空全部 session 字符串(连带产物):无泄漏、无 double-free。
|
||||
vm.full_reset();
|
||||
assert!(vm.gc_state.session_string_ptrs.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -950,7 +950,7 @@ impl Vm {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: val 是字符串值,其 JsString 指针在生命周期内有效。
|
||||
Some(unsafe { (*val.as_string_ptr()).data.clone() })
|
||||
Some(unsafe { (*val.as_string_ptr()).to_owned_string() })
|
||||
}
|
||||
|
||||
pub(crate) fn thrown_error_kind(&self, val: JsValue) -> &'static str {
|
||||
@@ -998,8 +998,8 @@ impl Vm {
|
||||
return Ok(make_int_key(d as u32));
|
||||
}
|
||||
} else if val.is_string() {
|
||||
// SAFETY: val 是字符串值,把其内容桥接为永久 key id。
|
||||
let s = unsafe { &(*val.as_string_ptr()).data };
|
||||
// SAFETY: val 是字符串值,把其内容桥接为永久 key id(rope 经惰性扁平化)。
|
||||
let s = unsafe { (*val.as_string_ptr()).as_str() };
|
||||
if let Some(i) = canonical_index_of(s) {
|
||||
return Ok(make_int_key(i));
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ impl Vm {
|
||||
let val = self.regs[reg];
|
||||
let s = if val.is_string() {
|
||||
// SAFETY: val 是字符串值。
|
||||
unsafe { (*val.as_string_ptr()).data.clone() }
|
||||
unsafe { (*val.as_string_ptr()).to_owned_string() }
|
||||
} else {
|
||||
to_string_full(val, self)?
|
||||
};
|
||||
@@ -163,7 +163,7 @@ impl Vm {
|
||||
let val = imm[const_idx];
|
||||
if val.is_string() {
|
||||
// SAFETY: val 是字符串值。
|
||||
let s = unsafe { (*val.as_string_ptr()).data.clone() };
|
||||
let s = unsafe { (*val.as_string_ptr()).to_owned_string() };
|
||||
result.push_str(&s);
|
||||
}
|
||||
}
|
||||
@@ -684,7 +684,7 @@ impl Vm {
|
||||
JsValue::from_js_object(proto_ptr),
|
||||
));
|
||||
if src.is_string() {
|
||||
let code_units: Vec<u16> = unsafe { (*src.as_string_ptr()).data.encode_utf16().collect() };
|
||||
let code_units: Vec<u16> = unsafe { (*src.as_string_ptr()).as_str().encode_utf16().collect() };
|
||||
for (i, unit) in code_units.iter().enumerate() {
|
||||
let si = make_int_key(i as u32);
|
||||
let ch_val = match char::from_u32(*unit as u32) {
|
||||
@@ -707,7 +707,7 @@ impl Vm {
|
||||
.and_then(|v| {
|
||||
if v.is_string() {
|
||||
// SAFETY: v 是字符串常量值。
|
||||
Some(unsafe { (*v.as_string_ptr()).data.clone() })
|
||||
Some(unsafe { (*v.as_string_ptr()).to_owned_string() })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -742,7 +742,7 @@ impl Vm {
|
||||
// 字符串包装对象:索引字符是可枚举自有属性(ToObject("str") 的 0..len-1)。
|
||||
if src_obj.type_tag == JsObject::OBJ_TYPE_STRING_OBJ {
|
||||
let raw = src_obj.get_prop_at(0);
|
||||
let s = unsafe { (*raw.as_string_ptr()).data.clone() };
|
||||
let s = unsafe { (*raw.as_string_ptr()).to_owned_string() };
|
||||
let code_units: Vec<u16> = s.encode_utf16().collect();
|
||||
for (i, unit) in code_units.iter().enumerate() {
|
||||
let si = make_int_key(i as u32);
|
||||
@@ -850,7 +850,7 @@ impl Vm {
|
||||
// 字符串源:按索引复制字符(可枚举索引属性)。
|
||||
if src.is_string() {
|
||||
let target = unsafe { &mut *target_val.as_js_object_ptr() };
|
||||
let code_units: Vec<u16> = unsafe { (*src.as_string_ptr()).data.encode_utf16().collect() };
|
||||
let code_units: Vec<u16> = unsafe { (*src.as_string_ptr()).as_str().encode_utf16().collect() };
|
||||
for (i, unit) in code_units.iter().enumerate() {
|
||||
let si = make_int_key(i as u32);
|
||||
let ch_val = match char::from_u32(*unit as u32) {
|
||||
|
||||
@@ -16,6 +16,11 @@ use oxide_types::object::{JsObject, JsString, PropAttributes};
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
impl Vm {
|
||||
/// Cons(rope)节点保留的字节阈值:拼接总长 ≤ 该值时急切扁平为 Flat。
|
||||
/// 小链的"链接 + 首次消费扁平化"双重分配高于直接拷贝,直接扁平更优;
|
||||
/// 超过阈值后 O(n²) 拷贝成本超过节点开销,Cons 的 O(1) 链接才值得。
|
||||
pub(crate) const CONS_FLATTEN_BYTES: usize = 128;
|
||||
|
||||
/// 以最小配置创建独立 VM:新建 `KernelCore` + `KernelSession` 并初始化内置对象。
|
||||
pub fn new() -> Self {
|
||||
let core = KernelCore::new(KernelConfig::minimal());
|
||||
@@ -383,11 +388,53 @@ impl Vm {
|
||||
pub fn new_string_owned(&mut self, s: String) -> JsValue {
|
||||
let len = s.len();
|
||||
let ptr = Box::into_raw(Box::new(JsString::new(s)));
|
||||
self.register_session_string(ptr, len)
|
||||
}
|
||||
|
||||
/// 登记一个 session 字符串并记账(`new_string_owned` 与 `new_cons_string`
|
||||
/// 共用),返回字符串值。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 登记指针到 session 字符串表。
|
||||
/// 2. 记账 `size_of::<JsString>() + bytes`。
|
||||
///
|
||||
/// # 注意事项
|
||||
/// - 本方法**不触发**回收:执行期字符串 GC 统一在 dispatch 指令边界检查
|
||||
/// 水位触发(此时 builtin 局部值已落地为执行根,分配点触发会误释放
|
||||
/// 仅存于局部/构造中的活串)。
|
||||
fn register_session_string(&mut self, ptr: *mut JsString, bytes: usize) -> JsValue {
|
||||
self.gc_state.session_string_ptrs.push(ptr);
|
||||
self.gc_state.session_bytes_allocated += std::mem::size_of::<JsString>() + len;
|
||||
self.gc_state.session_bytes_allocated += std::mem::size_of::<JsString>() + bytes;
|
||||
JsValue::string(ptr)
|
||||
}
|
||||
|
||||
/// 分配 Cons(rope)节点:左右子节点 O(1) 链接,不拷贝文本。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - `left`/`right` 必须均为字符串值;仅在 `+`/`+=` 的 `concat_strings`
|
||||
/// 接线点调用(CONCAT_N 保持急切扁平,不经此路径)。
|
||||
///
|
||||
/// # 副作用
|
||||
/// - 登记节点到 session 字符串表(账目 = `size_of::<JsString>() + byte_len`)。
|
||||
///
|
||||
/// # 注意事项
|
||||
/// - 调用方须保证 `left`/`right` 在调用期间存活:子节点(coerce 新鲜字符串 /
|
||||
/// 新建叶子)要么已是执行根,要么在写入根前不被任何回收点触达——当前
|
||||
/// 回收点仅在指令边界,链接结果写寄存器先于下一次检查,天然满足。
|
||||
/// - 调用方(`concat_strings`)已按字节阈值过滤:仅大链(总长 >
|
||||
/// [`Self::CONS_FLATTEN_BYTES`])进入本方法,小链由其急切扁平。
|
||||
#[inline]
|
||||
pub fn new_cons_string(&mut self, left: JsValue, right: JsValue) -> JsValue {
|
||||
debug_assert!(left.is_string() && right.is_string(), "new_cons_string 只接收字符串操作数");
|
||||
let left_ptr = left.as_string_ptr_mut();
|
||||
let right_ptr = right.as_string_ptr_mut();
|
||||
// SAFETY: 两指针指向存活的 JsString(调用方保证),len() 各为 O(1)。
|
||||
let byte_len = unsafe { (*left_ptr).len() + (*right_ptr).len() };
|
||||
// SAFETY: new_cons 的调用方(本函数)负责保证子节点随节点存活。
|
||||
let ptr = Box::into_raw(Box::new(unsafe { JsString::new_cons(left_ptr, right_ptr) }));
|
||||
self.register_session_string(ptr, byte_len)
|
||||
}
|
||||
|
||||
/// 把字符串 intern 为永久 key id(属性名/方法名),进程生命周期内稳定。
|
||||
pub fn intern_key(&self, s: &str) -> u32 {
|
||||
self.kernel_core.perm_interner().intern(s).0
|
||||
@@ -407,10 +454,10 @@ impl Vm {
|
||||
/// 会引用它们。较轻量的 `reset()` 刻意保留它们,与 session 对象跨 eval 存活一致。
|
||||
fn free_session_string_heap_data(&mut self) {
|
||||
for ptr in self.gc_state.session_string_ptrs.drain(..) {
|
||||
// SAFETY: 每个指针来自 new_string 中的 Box::into_raw(Box::new(JsString)),
|
||||
// 且只在这里恰好释放一次。
|
||||
// SAFETY: 每个指针来自 new_string/new_cons_string 的 Box::into_raw,
|
||||
// 且只在这里(或 sweep)恰好释放一次;内部连带释放 rope 扁平化产物。
|
||||
unsafe {
|
||||
drop(Box::from_raw(ptr));
|
||||
crate::session_gc::SessionGc::drop_session_string_box(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ fn to_str(vm: &Vm, val: JsValue) -> String {
|
||||
fn fmt(vm: &Vm, val: JsValue) -> String {
|
||||
if val.is_string() {
|
||||
// SAFETY: val 已确认是字符串值。
|
||||
unsafe { (*val.as_string_ptr()).data.clone() }
|
||||
unsafe { (*val.as_string_ptr()).to_owned_string() }
|
||||
} else if val.is_bigint() {
|
||||
format!("{}", vm.bigint_value(val))
|
||||
} else {
|
||||
|
||||
@@ -24,7 +24,7 @@ fn keys_to_strings(result: JsValue) -> Vec<String> {
|
||||
if v.is_string() {
|
||||
// SAFETY: 字符串值指向 VM arena(调用方保持 vm 存活)。
|
||||
let s = unsafe { &*v.as_string_ptr() };
|
||||
keys.push(s.data.clone());
|
||||
keys.push(s.to_owned_string());
|
||||
}
|
||||
}
|
||||
keys
|
||||
@@ -117,7 +117,7 @@ fn numeric_literal_key_materializes_as_string() {
|
||||
// `{5:1}` 的键是字符串 "5"(键经 ToPropertyKey 字符串化),读 o[5]/o["5"] 同一键。
|
||||
let r = eval_str("var o={5:1}; Object.getOwnPropertyNames(o)[0]").unwrap();
|
||||
assert!(r.is_string());
|
||||
let s = unsafe { &*r.as_string_ptr() }.data.clone();
|
||||
let s = unsafe { &*r.as_string_ptr() }.to_owned_string();
|
||||
assert_eq!(s, "5");
|
||||
let r = eval_str("var o={5:1}; o[5] + o['5']").unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 2);
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
//! 二元 ConsString(rope)语义回归测试。
|
||||
//!
|
||||
//! 覆盖:`+`/`+=` 拼接产物(rope 节点)在各消费点(`==`/`.length`/string 方法/
|
||||
//! for-of/属性键/模板/JSON/展开)的惰性扁平化正确性、数字/对象叶子混合、CONCAT_N
|
||||
//! 产物作 rope 叶子、低 GC 阈值下跨执行期字符串回收的存活与 mark 传播。
|
||||
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_kernel::kernel::{KernelConfig, KernelCore};
|
||||
use oxide_types::value::JsValue;
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
fn eval(vm: &mut Vm, source: &str) -> Result<JsValue, String> {
|
||||
let allocator = oxide_parser::Allocator::default();
|
||||
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
|
||||
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {e}"))?;
|
||||
vm.run(&module)
|
||||
}
|
||||
|
||||
fn eval_str(source: &str) -> String {
|
||||
let mut vm = Vm::new();
|
||||
let val = eval(&mut vm, source).expect("eval ok");
|
||||
fmt(&vm, val)
|
||||
}
|
||||
|
||||
fn fmt(vm: &Vm, val: JsValue) -> String {
|
||||
if val.is_string() {
|
||||
vm.lookup_str(val).unwrap_or_default()
|
||||
} else if val.is_bigint() {
|
||||
format!("{}", vm.bigint_value(val))
|
||||
} else {
|
||||
format!("{val}")
|
||||
}
|
||||
}
|
||||
|
||||
/// 低 GC 阈值 VM:水位=1 使指令边界检查几乎每指令触发,压 rope 的 mark 传播与
|
||||
/// 跨回收存活路径。
|
||||
fn vm_with_tiny_threshold() -> Vm {
|
||||
let mut cfg = KernelConfig::minimal();
|
||||
cfg.set_session_gc_threshold(1);
|
||||
let core = KernelCore::new(cfg);
|
||||
Vm::with_kernel_core(core)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_binary_concat_content_and_length() {
|
||||
// 二元 `+` 产物为 Cons 节点:内容与长度经惰性扁平化与缓存 O(1) 读取。
|
||||
assert_eq!(eval_str("\"ab\" + \"cd\""), "abcd");
|
||||
assert_eq!(eval_str("(\"ab\" + \"cd\").length"), "4");
|
||||
assert_eq!(eval_str("(\"ab\" + \"cd\" + \"ef\").length"), "6");
|
||||
assert_eq!(eval_str("(\"ab\" + \"cd\").charCodeAt(1)"), "98");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_equality_with_literal_and_rope() {
|
||||
// `==`/`===` 走 string_value_eq 内容比较:rope 与同内容字面量/另一 rope 相等,
|
||||
// 与不同内容不等。
|
||||
assert_eq!(eval_str("(\"ab\" + \"cd\") === \"abcd\""), "true");
|
||||
assert_eq!(eval_str("(\"ab\" + \"cd\") == \"abcd\""), "true");
|
||||
assert_eq!(eval_str("(\"ab\" + \"cd\") === (\"a\" + \"bcd\")"), "true");
|
||||
assert_eq!(eval_str("(\"ab\" + \"cd\") === \"abce\""), "false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_compound_loop_builds_correctly() {
|
||||
// string_build 主模式:`s += j` 循环 50 次(数字叶子 + Cons 链接),
|
||||
// 末次 `.length` 扁平化结果正确(0..49 拼接:10 个单字符 + 40 个双字符 = 90)。
|
||||
assert_eq!(
|
||||
eval_str(
|
||||
"var s = ''; for (var j = 0; j < 50; j++) { s += j } \
|
||||
s + '|' + s.length"
|
||||
),
|
||||
"012345678910111213141516171819202122232425262728293031323334353637383940414243444546474849|90"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_mixed_operand_leaf_conversion() {
|
||||
// 非字符串操作数先转叶子再链接:数字/布尔/null/undefined/对象与字符串混拼。
|
||||
assert_eq!(eval_str("var s = 'a'; s += 1; s += 2.5; s += true; s"), "a12.5true");
|
||||
assert_eq!(eval_str("'x' + null + undefined"), "xnullundefined");
|
||||
assert_eq!(eval_str("var s = 'a'; s += { toString: function () { return 'obj' } }; s"), "aobj");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_concat_n_rhs_becomes_leaf() {
|
||||
// `s += a+b+c`:rhs 摊平为 CONCAT_N(急切 Flat 产物),外层 COMPOUND_ADD 把
|
||||
// Flat 产物链接成 Cons 子节点——两层机制叠加内容正确。
|
||||
assert_eq!(eval_str("var s = ''; s += 'a' + 'b' + 'c'; s + s"), "abcabc");
|
||||
assert_eq!(eval_str("var s = 'z'; for (var i = 0; i < 5; i++) { s += 'x' + 'y' } s"), "zxyxyxyxyxy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_as_property_key() {
|
||||
// rope 作属性键:ToPropertyKey → flat 桥接 intern,键内容一致。
|
||||
assert_eq!(eval_str("var s = 'a' + 'b'; var o = {}; o[s] = 42; o['ab']"), "42");
|
||||
assert_eq!(eval_str("var s = '5'; var o = {}; o[s] = 7; o[5]"), "7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_for_of_iterates_chars() {
|
||||
// for-of 字符串迭代 rope:首次访问扁平化后游标推进,逐字符正确。
|
||||
assert_eq!(eval_str("var s = 'ab' + 'cd'; var out = ''; for (var c of s) { out += c } out"), "abcd");
|
||||
assert_eq!(eval_str("var s = 'x'; s += 'y'; s += 'z'; [...s].join('-')"), "x-y-z");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_consumed_by_string_methods() {
|
||||
// string 方法族(this_string 借用 + 只读扫描)消费 rope 时惰性扁平化。
|
||||
assert_eq!(eval_str("('hello' + 'world').indexOf('wo')"), "5");
|
||||
assert_eq!(eval_str("('hello' + 'world').substring(2, 7)"), "llowo");
|
||||
assert_eq!(eval_str("('ab' + 'cd').repeat(2)"), "abcdabcd");
|
||||
assert_eq!(eval_str("('aB' + 'cD').toUpperCase()"), "ABCD");
|
||||
assert_eq!(eval_str("('a,b' + ',c').split(',').length"), "3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_in_template_and_json() {
|
||||
// 模板串与 JSON.stringify 消费 rope 结果一致。
|
||||
assert_eq!(eval_str("var s = 'a' + 'b'; `${s}-x`"), "ab-x");
|
||||
assert_eq!(eval_str("var s = 'x' + 'y'; JSON.stringify([s])"), "[\"xy\"]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_numeric_coercion_of_content() {
|
||||
// rope 文本参与数值转换:ToNumber 走 string_data 扁平化文本。
|
||||
assert_eq!(eval_str("('1' + '0') * 2"), "20");
|
||||
assert_eq!(eval_str("Number('4' + '2')"), "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_survives_runtime_string_gc() {
|
||||
// 低阈值下指令边界检查几乎每指令触发:循环拼接全程(链接结果写寄存器先于
|
||||
// 下次检查)不被误回收;大量中间垃圾触发多轮回收后 rope 仍正确消费。
|
||||
let mut vm = vm_with_tiny_threshold();
|
||||
let val = eval(
|
||||
&mut vm,
|
||||
"var s = 'a'; for (var i = 0; i < 300; i++) { s += 'b' } \
|
||||
var garbage = ''; for (var g = 0; g < 500; g++) { garbage += 'x' } \
|
||||
s.length + '|' + s.charAt(150)",
|
||||
)
|
||||
.expect("eval ok");
|
||||
assert_eq!(fmt(&vm, val), "301|b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_in_object_holder_survives_gc() {
|
||||
// rope 挂在全局对象属性上:mark 经对象字符串边传播 rope 闭包,子节点与
|
||||
// 产物随父存活(跨多轮低阈值回收)。
|
||||
let mut vm = vm_with_tiny_threshold();
|
||||
let val = eval(
|
||||
&mut vm,
|
||||
"var o = {}; o.s = 'x'; for (var i = 0; i < 200; i++) { o.s += 'y' } \
|
||||
var t = ''; for (var g = 0; g < 300; g++) { t += 'z' } \
|
||||
o.s.length + '|' + o.s.charAt(200) + '|' + t.length",
|
||||
)
|
||||
.expect("eval ok");
|
||||
assert_eq!(fmt(&vm, val), "201|y|300");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rope_dead_after_scope_released() {
|
||||
// rope 离开作用域成为垃圾:低阈值下被后续分配回收,不影响新值。
|
||||
let mut vm = vm_with_tiny_threshold();
|
||||
eval(&mut vm, "var t = ''; for (var i = 0; i < 200; i++) { t += 'z' }").expect("eval ok");
|
||||
let val = eval(&mut vm, "'fresh' + 'value'").expect("eval ok");
|
||||
assert_eq!(fmt(&vm, val), "freshvalue");
|
||||
}
|
||||
Reference in New Issue
Block a user