fix: 执行期字符串 GC 触发点移到指令边界,消除 builtin 局部活串悬垂

分配点触发会把仅存于 builtin 函数栈上局部 JsValue 与构造中未登记对象的
存活串判死释放(temporal 槽、regexp exec 构造中数组、replace 回调参数
Vec),长会话超阈值即悬垂。触发改在 dispatch 指令边界,此时所有局部值已
落地为执行根;分配点只留账目。水位采用上次收集存活字节加阈值增量,活串
超阈值时不会每指令重复白跑;reset 完整收集仍按阈值直接比较。debug 构建下
收集后断言存活对象字符串边已登记。补低阈值回归:regexp exec 旧行为直接
SIGABRT,temporal 与 replace 结果断言守护。
This commit is contained in:
2026-08-15 13:20:04 +08:00
parent f4c58d5dd9
commit 81f54f280d
5 changed files with 136 additions and 17 deletions
+13 -7
View File
@@ -25,6 +25,8 @@ impl Vm {
// 非 0 的稳定 id(保持枚举层"id 0 哨兵"的既有约定,见 walk_own_keys)。
let length_si = core.perm_interner().intern("length").0;
let obj_proto = P::clone(&session.builtin_world().object_proto);
// 提前缓存执行期字符串 GC 初始水位(构造后 config 不再变化)。
let gc_threshold = core.config().session_gc_threshold;
let mut vm = Self {
regs: [JsValue::undefined(); 256],
pc: 0,
@@ -86,6 +88,7 @@ impl Vm {
session_string_ptrs: Vec::new(),
session_bigint_ptrs: std::cell::RefCell::new(Vec::new()),
session_bytes_allocated: 0,
string_gc_watermark: gc_threshold,
forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher),
},
symbols: SymbolState {
@@ -123,6 +126,8 @@ impl Vm {
// 非 0 的稳定 id(保持枚举层"id 0 哨兵"的既有约定,见 walk_own_keys)。
let length_si = core.perm_interner().intern("length").0;
let obj_proto = P::clone(&session.builtin_world().object_proto);
// 提前缓存执行期字符串 GC 初始水位(构造后 config 不再变化)。
let gc_threshold = core.config().session_gc_threshold;
let mut vm = Self {
regs: [JsValue::undefined(); 256],
pc: 0,
@@ -184,6 +189,7 @@ impl Vm {
session_string_ptrs: Vec::new(),
session_bigint_ptrs: std::cell::RefCell::new(Vec::new()),
session_bytes_allocated: 0,
string_gc_watermark: gc_threshold,
forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher),
},
symbols: SymbolState {
@@ -285,6 +291,7 @@ impl Vm {
self.gc_state.session_epoch.reset();
self.gc_state.session_object_ptrs.clear();
self.gc_state.session_bytes_allocated = 0;
self.gc_state.string_gc_watermark = self.kernel_core.config().session_gc_threshold;
self.gc_state.session_gc = crate::session_gc::SessionGc::new();
self.free_session_string_heap_data();
self.free_session_bigint_heap_data();
@@ -365,17 +372,16 @@ impl Vm {
/// 同 `new_string`,但以 move 接收 `String`,避免一次克隆。
///
/// # 副作用
/// - 累计 session 字符串字节超阈值时,触发一次仅字符串的 GC(对象不搬移)。
/// - 累计 session 字符串字节账目;回收不在分配点触发(见注意事项)。
///
/// # 注意事项
/// - 触发检查在登记之前:返回值尚未写入任何执行根,若登记后回收会被本次 GC
/// 判死并释放,返回即悬垂;此时 `s` 仍在栈上、未被登记,GC 不会触碰它。
/// - 热路径仅 1 次 usize 比较 + 阈值读取,超限才进回收路径。
/// - 分配点不触发 GC:builtin 函数栈上的局部 `JsValue` 与构造中对象不在
/// mark 根清单内,分配前触发会释放"仅存于局部/构造中"的活串(悬垂)。
/// 回收统一在 dispatch 指令边界检查水位触发——此时 builtin 局部值已落地
/// 为执行根,任何分配点的局部持有跨分配点均安全。
/// - 热路径仅 1 次字节账目累加,无阈值读取与分支。
pub fn new_string_owned(&mut self, s: String) -> JsValue {
let len = s.len();
if self.gc_state.session_bytes_allocated >= self.kernel_core.config().session_gc_threshold {
self.maybe_collect_session_strings();
}
let ptr = Box::into_raw(Box::new(JsString::new(s)));
self.gc_state.session_string_ptrs.push(ptr);
self.gc_state.session_bytes_allocated += std::mem::size_of::<JsString>() + len;