mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
fix: 执行期字符串 GC 触发点移到指令边界,消除 builtin 局部活串悬垂
分配点触发会把仅存于 builtin 函数栈上局部 JsValue 与构造中未登记对象的 存活串判死释放(temporal 槽、regexp exec 构造中数组、replace 回调参数 Vec),长会话超阈值即悬垂。触发改在 dispatch 指令边界,此时所有局部值已 落地为执行根;分配点只留账目。水位采用上次收集存活字节加阈值增量,活串 超阈值时不会每指令重复白跑;reset 完整收集仍按阈值直接比较。debug 构建下 收集后断言存活对象字符串边已登记。补低阈值回归:regexp exec 旧行为直接 SIGABRT,temporal 与 replace 结果断言守护。
This commit is contained in:
@@ -504,6 +504,40 @@ impl SessionGc {
|
||||
&& vm.gc_state.session_bytes_allocated >= vm.kernel_core().config().session_gc_threshold
|
||||
}
|
||||
|
||||
/// 执行期字符串回收的触发判断:账目须超过水位(上次收集后的存活字节 +
|
||||
/// 阈值增量)。完整收集(reset)仍用 [`Self::should_collect`] 的阈值直接比较,
|
||||
/// 保证死对象超阈值即被 reset 回收;执行期走增量水位,活串超阈值时不每指令
|
||||
/// 重复触发无死串可回收的白跑。
|
||||
pub(crate) fn should_collect_strings(&self, vm: &Vm) -> bool {
|
||||
(!vm.gc_state.session_object_ptrs.is_empty() || !vm.gc_state.session_string_ptrs.is_empty())
|
||||
&& vm.gc_state.session_bytes_allocated >= vm.gc_state.string_gc_watermark
|
||||
}
|
||||
|
||||
/// debug 兜底:mark 之后断言所有存活对象持有的字符串边都已登记进
|
||||
/// `live_strings`——捕获"对象边漏登记 → 存活串被误释放"的静默悬垂。
|
||||
#[cfg(debug_assertions)]
|
||||
fn debug_assert_marked_object_strings_live(&self, vm: &Vm) {
|
||||
let mut live = HashSet::with_hasher(FxBuildHasher);
|
||||
for &ptr in &vm.gc_state.session_object_ptrs {
|
||||
if ptr.is_null() {
|
||||
continue;
|
||||
}
|
||||
// SAFETY: session_object_ptrs 归 VM 所有,指针在 arena 存活期有效。
|
||||
let obj = unsafe { &*ptr };
|
||||
if !obj.is_gc_marked() {
|
||||
continue;
|
||||
}
|
||||
live.clear();
|
||||
Self::record_object_string_edges(&mut live, obj);
|
||||
for s_ptr in live.drain() {
|
||||
assert!(
|
||||
self.live_strings.contains(&s_ptr),
|
||||
"存活对象持有未登记的 session 字符串边"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collect(&mut self, vm: &mut Vm) {
|
||||
let start = Instant::now();
|
||||
|
||||
@@ -541,11 +575,13 @@ impl SessionGc {
|
||||
/// 字符串每串独立 Box、地址稳定,清扫无需 forwarding 与根重写。
|
||||
///
|
||||
/// # 副作用
|
||||
/// - 清空全部对象 mark 位;按存活集释放死串;累计 `total_collections` 与时长统计。
|
||||
/// - 清空全部对象 mark 位;按存活集释放死串;累计 `total_collections` 与时长统计;
|
||||
/// 抬高 `string_gc_watermark`(存活字节 + 阈值增量)供下次执行期触发。
|
||||
///
|
||||
/// # 注意事项
|
||||
/// - mark 的 DFS 以 `is_gc_marked` 短路,残留 true 会导致后续完整收集漏标,
|
||||
/// 本路径不跑对象 sweep(sweep 内清位不会执行),必须先 `clear_all_marks`。
|
||||
/// 本路径不跑对象 sweep(sweep 内清位不会执行),故开头与结尾都清位——
|
||||
/// 维持"mark 之后必由清位收尾"的不变量,strings-only 结束不留残留。
|
||||
/// - 不搬移对象,`session_bytes_allocated` 保持对象账目,手动扣掉字符串账目后
|
||||
/// 由 `sweep_session_strings` 补回存活串字节,与完整收集的最终账目一致。
|
||||
pub(crate) fn collect_strings_only(&mut self, vm: &mut Vm) {
|
||||
@@ -559,6 +595,9 @@ impl SessionGc {
|
||||
// 完整 mark:复用现有实现,产出 live_strings;对象仅置位、不搬移。
|
||||
self.mark(vm);
|
||||
|
||||
// debug 兜底:存活对象的字符串边必须已登记,否则串清扫将释放活串。
|
||||
self.debug_assert_marked_object_strings_live(vm);
|
||||
|
||||
// 扣减字符串账目(保留对象账目),再补回存活串字节。
|
||||
let object_bytes = vm.gc_state.session_object_ptrs.len() * size_of::<JsObject>();
|
||||
vm.gc_state.session_bytes_allocated = object_bytes;
|
||||
@@ -569,6 +608,11 @@ impl SessionGc {
|
||||
// 其字符串边不进 live_strings → 存活串被误释放 → 存活对象持悬垂指针。
|
||||
self.clear_all_marks(vm);
|
||||
|
||||
// 抬高下次触发水位:本次存活字节 + 阈值增量,避免活串超阈值时每指令重复
|
||||
// 触发无死串可回收的完整 mark + 串清扫。
|
||||
let threshold = vm.kernel_core().config().session_gc_threshold;
|
||||
vm.gc_state.string_gc_watermark = vm.gc_state.session_bytes_allocated.saturating_add(threshold);
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
self.total_collections += 1;
|
||||
self.last_collection_duration_us = elapsed.as_micros() as u64;
|
||||
@@ -584,7 +628,7 @@ impl SessionGc {
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_collect_strings_only(&mut self, vm: &mut Vm) {
|
||||
if self.should_collect(vm) {
|
||||
if self.should_collect_strings(vm) {
|
||||
self.collect_strings_only(vm);
|
||||
}
|
||||
}
|
||||
@@ -1267,20 +1311,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_threshold_triggers_strings_only_collection() {
|
||||
fn allocation_does_not_trigger_gc_before_safe_point() {
|
||||
let mut vm = vm_with_threshold(1);
|
||||
// 第一次分配不触发(累计 0 < 阈值),登记后超阈值;后续每次分配前先回收。
|
||||
// 分配点不触发回收:触发已移到 dispatch 指令边界,局部持有跨分配点安全。
|
||||
let seed = vm.new_string_owned("seed".repeat(16));
|
||||
let seed_ptr = seed.as_string_ptr_mut();
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&seed_ptr));
|
||||
|
||||
// 分配前触发回收:seed 无根引用 → 判死释放,返回值本身存活(登记前 GC 不触碰它)。
|
||||
let returned = vm.new_string_owned("returned".repeat(16));
|
||||
let returned_ptr = returned.as_string_ptr_mut();
|
||||
|
||||
assert_eq!(vm.gc_state.session_gc.total_collections, 0);
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&seed_ptr));
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&returned.as_string_ptr_mut()));
|
||||
|
||||
// 显式触发后:无根引用的 seed 被回收,入根的 returned 存活(安全点语义)。
|
||||
vm.regs[0] = returned;
|
||||
vm.maybe_collect_session_strings();
|
||||
assert!(vm.gc_state.session_gc.total_collections >= 1);
|
||||
assert!(!vm.gc_state.session_string_ptrs.contains(&seed_ptr));
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&returned_ptr));
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&returned.as_string_ptr_mut()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1333,6 +1380,8 @@ mod tests {
|
||||
|
||||
let s = vm.new_string_owned("x".repeat(64));
|
||||
vm.regs[1] = s;
|
||||
// 分配不自动触发(安全点语义),显式触发一轮验证对象不搬移。
|
||||
vm.maybe_collect_session_strings();
|
||||
|
||||
// 对象指针逐一相同:不搬移、不重写根。
|
||||
assert_eq!(vm.gc_state.session_object_ptrs, before);
|
||||
@@ -1356,6 +1405,8 @@ mod tests {
|
||||
for _ in 0..8 {
|
||||
let _ = vm.new_string_owned("dead".repeat(64));
|
||||
}
|
||||
// 分配不自动触发,每轮显式触发一次回收。
|
||||
vm.maybe_collect_session_strings();
|
||||
assert!(vm.gc_state.session_object_ptrs.contains(&obj_session));
|
||||
assert_eq!(unsafe { (*obj_session).get_prop_at(0) }, live);
|
||||
}
|
||||
|
||||
@@ -1320,6 +1320,12 @@ impl Vm {
|
||||
let mut steps: u64 = 0;
|
||||
loop {
|
||||
steps += 1;
|
||||
// 执行期字符串 GC 安全点:指令边界上无 builtin 局部活值(分配点触发
|
||||
// 会释放仅存于 Rust 局部/构造中未登记对象的串),此处触发保证回收
|
||||
// 时所有存活串都已落地为执行根。账目未超水位时仅 2 次字段比较。
|
||||
if self.gc_state.session_bytes_allocated >= self.gc_state.string_gc_watermark {
|
||||
self.maybe_collect_session_strings();
|
||||
}
|
||||
if let Some(max_steps) = max_steps {
|
||||
if steps > max_steps {
|
||||
vm_warn!("dispatch: step limit {} exceeded at pc={}", max_steps, self.pc);
|
||||
|
||||
@@ -34,6 +34,10 @@ pub(crate) struct GcState {
|
||||
/// mark/sweep 回收(值量少),只在 full_reset 统一释放。
|
||||
pub(crate) session_bigint_ptrs: RefCell<Vec<*mut num_bigint::BigInt>>,
|
||||
pub(crate) session_bytes_allocated: usize,
|
||||
/// 执行期字符串 GC 的触发水位:本次收集后的存活字节 + 阈值增量。
|
||||
/// 仅当账目超过水位才在指令边界触发回收——活串超阈值时不会每指令重复
|
||||
/// 触发无死串可回收的白跑,且保证触发点恒在无 builtin 局部活值的边界。
|
||||
pub(crate) string_gc_watermark: usize,
|
||||
pub(crate) forwarding: HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -80,6 +80,58 @@ fn strings_only_then_reset_keeps_global_subtree_strings() {
|
||||
assert_eq!(text, "hello");
|
||||
}
|
||||
|
||||
/// 低阈值执行期字符串 GC 下 builtin 构造期局部串跨分配点安全(触发点
|
||||
/// 在指令边界,native 调用内不回收):RegExp exec 写入构造中数组的匹配串/
|
||||
/// 捕获组串不被后续分配回收,返回数组内容正确。
|
||||
#[test]
|
||||
fn regexp_exec_strings_survive_runtime_gc() {
|
||||
let mut vm = vm_with_threshold(512);
|
||||
let module = compile(
|
||||
"var text = 'a'.repeat(500) + 'b'.repeat(500); var m = /(a+)(b+)/.exec(text); \
|
||||
m[0] + '|' + m[1] + '|' + m[2]",
|
||||
);
|
||||
let result = vm.run(&module).expect("run");
|
||||
let text = vm.lookup_str(result).expect("exec 结果应为字符串").to_string();
|
||||
let expected = format!(
|
||||
"{}|{}|{}",
|
||||
"a".repeat(500) + &"b".repeat(500),
|
||||
"a".repeat(500),
|
||||
"b".repeat(500)
|
||||
);
|
||||
assert_eq!(text, expected);
|
||||
assert!(vm.session_gc_stats().total_collections > 0, "执行期应触发字符串 GC");
|
||||
}
|
||||
|
||||
/// 低阈值下 Temporal.ZonedDateTime 构造的局部串(timeZone/calendar 槽)跨
|
||||
/// 分配安全:对象槽不因执行期回收而悬垂,getter 返回原内容。
|
||||
#[test]
|
||||
fn temporal_zoned_date_time_strings_survive_runtime_gc() {
|
||||
let mut vm = vm_with_threshold(512);
|
||||
let module = compile(
|
||||
"var g = {}; for (var i = 0; i < 20; i++) { g['k' + i] = 'v'.repeat(64); } \
|
||||
var z = new Temporal.ZonedDateTime(0n, 'UTC'); z.timeZoneId + '|' + z.calendarId",
|
||||
);
|
||||
let result = vm.run(&module).expect("run");
|
||||
let text = vm.lookup_str(result).expect("ZDT 槽应为字符串").to_string();
|
||||
assert_eq!(text, "UTC|iso8601");
|
||||
assert!(vm.session_gc_stats().total_collections > 0, "执行期应触发字符串 GC");
|
||||
}
|
||||
|
||||
/// 低阈值下 replace 函数 replacer 的回调参数 Vec(局部逐项分配)跨分配安全:
|
||||
/// 回调内拼接正确,说明参数串未被执行期回收释放。
|
||||
#[test]
|
||||
fn replace_replacer_args_survive_runtime_gc() {
|
||||
let mut vm = vm_with_threshold(512);
|
||||
let module = compile(
|
||||
"var g = {}; for (var i = 0; i < 20; i++) { g['k' + i] = 'v'.repeat(64); } \
|
||||
'a-b-c'.replace(/(a)-(b)-(c)/g, function(m, p1, p2, p3, pos, s) { return p1 + p2 + p3; })",
|
||||
);
|
||||
let result = vm.run(&module).expect("run");
|
||||
let text = vm.lookup_str(result).expect("replace 结果应为字符串").to_string();
|
||||
assert_eq!(text, "abc");
|
||||
assert!(vm.session_gc_stats().total_collections > 0, "执行期应触发字符串 GC");
|
||||
}
|
||||
|
||||
/// full_reset 清空 session 内存:执行期回收释放过的死串不与完全重置的
|
||||
/// 整体释放路径重复释放(无双重释放崩溃),重置后引擎可继续运行。
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user