perf: 执行期 session 字符串 GC 阈值回收(超 32MiB 触发,跳过对象 sweep)
new_string_owned 分配前做廉价阈值检查(1 次 usize 比较 + 阈值读取), 超 session_gc_threshold 即调 collect_strings_only:clear_all_marks → 完整 mark(现成)→ sweep_session_strings(现成),跳过对象 sweep 搬移, builtin 层对象裸指针跨分配点零悬垂面。触发检查在登记之前——返回值 尚未根化,登记后回收会被本次 GC 判死致悬垂。分配前触发亦使 GC 期间 新串仍在栈上未登记,天然安全。 长循环字符串累积场景(REPL 常驻 / test262 长测试 / kernel_batch 复用) session 内存从无界变为有界(≤ 阈值 + 存活串);bench gc_trigger_count 恢复有效(string_build 由 0 → 2 次触发)。07.2 rope 死节点回收依赖本机制。
This commit is contained in:
@@ -534,6 +534,56 @@ impl SessionGc {
|
||||
}
|
||||
}
|
||||
|
||||
/// 仅回收 session 字符串:完整 mark(对象只置位不搬移)→ 按存活集清扫字符串。
|
||||
///
|
||||
/// 刻意跳过对象 sweep 搬移——builtin/dispatch 层持有跨分配点的 session 对象
|
||||
/// 裸指针,移动式 sweep 会使其悬垂;对象执行期回收仍只在 reset 统一进行。
|
||||
/// 字符串每串独立 Box、地址稳定,清扫无需 forwarding 与根重写。
|
||||
///
|
||||
/// # 副作用
|
||||
/// - 清空全部对象 mark 位;按存活集释放死串;累计 `total_collections` 与时长统计。
|
||||
///
|
||||
/// # 注意事项
|
||||
/// - mark 的 DFS 以 `is_gc_marked` 短路,残留 true 会导致后续完整收集漏标,
|
||||
/// 本路径不跑对象 sweep(sweep 内清位不会执行),必须先 `clear_all_marks`。
|
||||
/// - 不搬移对象,`session_bytes_allocated` 保持对象账目,手动扣掉字符串账目后
|
||||
/// 由 `sweep_session_strings` 补回存活串字节,与完整收集的最终账目一致。
|
||||
pub(crate) fn collect_strings_only(&mut self, vm: &mut Vm) {
|
||||
let start = Instant::now();
|
||||
|
||||
vm_info!("[GC] strings-only cycle #{} start", self.total_collections + 1);
|
||||
|
||||
// 清残留 mark 位:防止本次字符串 mark 因历史 true 短路而漏标。
|
||||
self.clear_all_marks(vm);
|
||||
|
||||
// 完整 mark:复用现有实现,产出 live_strings;对象仅置位、不搬移。
|
||||
self.mark(vm);
|
||||
|
||||
// 扣减字符串账目(保留对象账目),再补回存活串字节。
|
||||
let object_bytes = vm.gc_state.session_object_ptrs.len() * size_of::<JsObject>();
|
||||
vm.gc_state.session_bytes_allocated = object_bytes;
|
||||
let freed_bytes = self.sweep_session_strings(vm);
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
self.total_collections += 1;
|
||||
self.last_collection_duration_us = elapsed.as_micros() as u64;
|
||||
self.max_collection_duration_us = self.max_collection_duration_us.max(self.last_collection_duration_us);
|
||||
self.min_collection_duration_us = self.min_collection_duration_us.min(self.last_collection_duration_us);
|
||||
|
||||
vm_info!(
|
||||
"[GC] strings-only cycle #{} end: {} bytes freed, {:.1}ms",
|
||||
self.total_collections,
|
||||
freed_bytes,
|
||||
elapsed.as_secs_f64() * 1000.0,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_collect_strings_only(&mut self, vm: &mut Vm) {
|
||||
if self.should_collect(vm) {
|
||||
self.collect_strings_only(vm);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_collect(&mut self, vm: &mut Vm) {
|
||||
if self.should_collect(vm) {
|
||||
self.collect(vm);
|
||||
@@ -647,7 +697,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gc_roots_contains_registers_frames_and_root_roots() { let mut vm = Vm::new();
|
||||
fn gc_roots_contains_registers_frames_and_root_roots() {
|
||||
let mut vm = Vm::new();
|
||||
let root = plain_object(&mut vm);
|
||||
let frame_obj = plain_object(&mut vm);
|
||||
let saved_this = plain_object(&mut vm);
|
||||
@@ -746,7 +797,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mark_phase_reaches_cycles_and_unreachable_are_unmarked() { let mut vm = Vm::new();
|
||||
fn mark_phase_reaches_cycles_and_unreachable_are_unmarked() {
|
||||
let mut vm = Vm::new();
|
||||
let root = plain_object(&mut vm);
|
||||
let reachable = plain_object(&mut vm);
|
||||
let unreachable = plain_object(&mut vm);
|
||||
@@ -1201,4 +1253,137 @@ mod tests {
|
||||
|
||||
assert!(after >= before + expected);
|
||||
}
|
||||
|
||||
fn vm_with_threshold(bytes: usize) -> Vm {
|
||||
let mut cfg = KernelConfig::minimal();
|
||||
cfg.set_session_gc_threshold(bytes);
|
||||
let core = KernelCore::new(cfg);
|
||||
Vm::with_kernel_core(core)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_threshold_triggers_strings_only_collection() {
|
||||
let mut vm = vm_with_threshold(1);
|
||||
// 第一次分配不触发(累计 0 < 阈值),登记后超阈值;后续每次分配前先回收。
|
||||
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!(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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strings_only_collection_preserves_all_root_kinds() {
|
||||
// 阈值 1:每次 new_string_owned 分配前自动触发回收,逐步验证各类根的保护。
|
||||
let mut vm = vm_with_threshold(1);
|
||||
// 寄存器根:直接持有 session 串。
|
||||
let reg_str = vm.new_string_owned("reg-root".repeat(16));
|
||||
vm.regs[0] = reg_str;
|
||||
// 存活对象属性根:session 对象经 promote 后持有 session 串(分配即触发回收,
|
||||
// reg_str 仍在寄存器,prop_str 挂到对象后才被下次回收看到)。
|
||||
let obj = plain_object(&mut vm);
|
||||
let prop_str = vm.new_string_owned("prop-root".repeat(16));
|
||||
unsafe {
|
||||
(*obj).set_prop_at(0, prop_str);
|
||||
}
|
||||
let obj_session = vm.promote_object(obj);
|
||||
vm.regs[1] = JsValue::from_js_object(obj_session);
|
||||
// 非 session 根对象属性:epoch 根对象(在寄存器)持有 session 串,mark 走
|
||||
// 非 session 根扫描路径保护。
|
||||
let epoch_obj = plain_object(&mut vm);
|
||||
let epoch_str = vm.new_string_owned("epoch-root".repeat(16));
|
||||
unsafe {
|
||||
(*epoch_obj).set_prop_at(0, epoch_str);
|
||||
}
|
||||
vm.regs[2] = JsValue::from_js_object(epoch_obj);
|
||||
// 死串:无任何根引用,最后手动触发一轮回收它。
|
||||
let dead = vm.new_string_owned("dead".repeat(16));
|
||||
let dead_ptr = dead.as_string_ptr_mut();
|
||||
|
||||
vm.maybe_collect_session_strings();
|
||||
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(®_str.as_string_ptr_mut()));
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&prop_str.as_string_ptr_mut()));
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&epoch_str.as_string_ptr_mut()));
|
||||
assert!(!vm.gc_state.session_string_ptrs.contains(&dead_ptr));
|
||||
// 存活串内容可读,地址稳定。
|
||||
assert_eq!(unsafe { (*reg_str.as_string_ptr_mut()).as_str() }, "reg-root".repeat(16));
|
||||
assert_eq!(unsafe { (*prop_str.as_string_ptr_mut()).as_str() }, "prop-root".repeat(16));
|
||||
assert_eq!(unsafe { (*epoch_str.as_string_ptr_mut()).as_str() }, "epoch-root".repeat(16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strings_only_collection_does_not_move_objects() {
|
||||
let mut vm = vm_with_threshold(1);
|
||||
let obj = plain_object(&mut vm);
|
||||
let obj_session = vm.promote_object(obj);
|
||||
vm.regs[0] = JsValue::from_js_object(obj_session);
|
||||
let before: Vec<_> = vm.gc_state.session_object_ptrs.clone();
|
||||
|
||||
let s = vm.new_string_owned("x".repeat(64));
|
||||
vm.regs[1] = s;
|
||||
|
||||
// 对象指针逐一相同:不搬移、不重写根。
|
||||
assert_eq!(vm.gc_state.session_object_ptrs, before);
|
||||
assert_eq!(vm.regs[0].as_js_object_ptr(), obj_session);
|
||||
assert_eq!(vm.session_object_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_strings_only_cycles_keep_objects_alive() {
|
||||
let mut vm = vm_with_threshold(1);
|
||||
let obj = plain_object(&mut vm);
|
||||
let obj_session = vm.promote_object(obj);
|
||||
let live = vm.new_string_owned("keep".repeat(32));
|
||||
unsafe {
|
||||
(*obj_session).set_prop_at(0, live);
|
||||
}
|
||||
vm.regs[0] = JsValue::from_js_object(obj_session);
|
||||
|
||||
// 连续多轮触发字符串回收:对象 mark 残留位被显式清理,对象与挂载串持续存活。
|
||||
for _ in 0..5 {
|
||||
for _ in 0..8 {
|
||||
let _ = vm.new_string_owned("dead".repeat(64));
|
||||
}
|
||||
assert!(vm.gc_state.session_object_ptrs.contains(&obj_session));
|
||||
assert_eq!(unsafe { (*obj_session).get_prop_at(0) }, live);
|
||||
}
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&live.as_string_ptr_mut()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strings_only_collection_byte_accounting_matches_survivors() {
|
||||
let mut vm = vm_with_threshold(1);
|
||||
let obj = plain_object(&mut vm);
|
||||
let obj_session = vm.promote_object(obj);
|
||||
vm.regs[0] = JsValue::from_js_object(obj_session);
|
||||
let live = vm.new_string_owned("live".repeat(8));
|
||||
let live_ptr = live.as_string_ptr_mut();
|
||||
vm.regs[1] = live;
|
||||
let dead = vm.new_string_owned("dead".repeat(8));
|
||||
let _ = dead.as_string_ptr_mut();
|
||||
|
||||
vm.maybe_collect_session_strings();
|
||||
|
||||
// 账目 = 对象头 + 存活串(size_of::<JsString>() + len),死串不再计入。
|
||||
let expected = size_of::<JsObject>() + (size_of::<JsString>() + unsafe { (*live_ptr).len() });
|
||||
assert_eq!(vm.gc_state.session_bytes_allocated, expected);
|
||||
assert!(!vm.gc_state.session_string_ptrs.contains(&dead.as_string_ptr_mut()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_collection_below_threshold_is_noop() {
|
||||
let mut vm = Vm::new();
|
||||
let s = vm.new_string_owned("small".repeat(4));
|
||||
vm.regs[0] = s;
|
||||
|
||||
assert_eq!(vm.gc_state.session_gc.total_collections, 0);
|
||||
assert!(vm.gc_state.session_string_ptrs.contains(&s.as_string_ptr_mut()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -804,6 +804,14 @@ impl Vm {
|
||||
self.gc_state.session_gc = session_gc;
|
||||
}
|
||||
|
||||
/// 执行期字符串阈值回收:仅回收 session 字符串(跳过对象搬移)。热路径只在
|
||||
/// 超阈值后进入,`mem::take` 不承担每次分配的开销。
|
||||
pub(crate) fn maybe_collect_session_strings(&mut self) {
|
||||
let mut session_gc = std::mem::take(&mut self.gc_state.session_gc);
|
||||
session_gc.maybe_collect_strings_only(self);
|
||||
self.gc_state.session_gc = session_gc;
|
||||
}
|
||||
|
||||
/// 只读访问 session GC 的统计(回收次数、存活/死亡对象数、释放字节等)。
|
||||
pub fn session_gc_stats(&self) -> &SessionGc {
|
||||
&self.gc_state.session_gc
|
||||
|
||||
@@ -363,8 +363,19 @@ impl Vm {
|
||||
}
|
||||
|
||||
/// 同 `new_string`,但以 move 接收 `String`,避免一次克隆。
|
||||
///
|
||||
/// # 副作用
|
||||
/// - 累计 session 字符串字节超阈值时,触发一次仅字符串的 GC(对象不搬移)。
|
||||
///
|
||||
/// # 注意事项
|
||||
/// - 触发检查在登记之前:返回值尚未写入任何执行根,若登记后回收会被本次 GC
|
||||
/// 判死并释放,返回即悬垂;此时 `s` 仍在栈上、未被登记,GC 不会触碰它。
|
||||
/// - 热路径仅 1 次 usize 比较 + 阈值读取,超限才进回收路径。
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_kernel::kernel::{KernelConfig, KernelCore};
|
||||
use oxide_parser::Allocator;
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
fn compile(source: &str) -> oxide_bytecode::module::CompiledModule {
|
||||
let allocator = Allocator::default();
|
||||
let program = oxide_parser::parse(&allocator, source).expect("parse");
|
||||
Compiler::new().compile(&program).expect("compile")
|
||||
}
|
||||
|
||||
fn vm_with_threshold(bytes: usize) -> Vm {
|
||||
let mut config = KernelConfig::minimal();
|
||||
config.set_session_gc_threshold(bytes);
|
||||
Vm::with_kernel_core(KernelCore::new(config))
|
||||
}
|
||||
|
||||
/// 长循环拼接产生海量 session 字符串:执行期自动触发仅字符串 GC,
|
||||
/// 结果正确且 session 字节有界(不触发则累计 ~20 MiB 远超低阈值)。
|
||||
#[test]
|
||||
fn long_string_loop_triggers_runtime_gc_and_bounds_session() {
|
||||
let mut vm = vm_with_threshold(4096);
|
||||
let module = compile("var s; for (var i = 0; i < 500000; i++) { s = 'str' + i; } s.length");
|
||||
let result = vm.run(&module).expect("run");
|
||||
|
||||
assert_eq!(format!("{}", result), "9");
|
||||
assert!(vm.session_gc_stats().total_collections > 0, "执行期应触发字符串 GC");
|
||||
assert!(
|
||||
vm.session_bytes_allocated() < 1_000_000,
|
||||
"session 字节应受阈值约束,实际 {}",
|
||||
vm.session_bytes_allocated()
|
||||
);
|
||||
}
|
||||
|
||||
/// 拼接结果写 global(逃逸根),跨 run 后触发执行期 GC 仍可读——
|
||||
/// 验证非会话存活串不因执行期回收丢失。
|
||||
#[test]
|
||||
fn session_string_survives_across_runs_with_runtime_gc() {
|
||||
let mut vm = vm_with_threshold(4096);
|
||||
let first = compile("globalThis.kept = 'he' + 'llo'; 0");
|
||||
vm.run(&first).expect("run1");
|
||||
|
||||
let second = compile("var s; for (var i = 0; i < 500000; i++) { s = 'y' + i; } globalThis.kept");
|
||||
let result = vm.run(&second).expect("run2");
|
||||
let text = vm.lookup_str(result).expect("kept 应为字符串").to_string();
|
||||
|
||||
assert_eq!(text, "hello");
|
||||
assert!(vm.session_gc_stats().total_collections > 0, "run2 应触发执行期字符串 GC");
|
||||
}
|
||||
|
||||
/// reset(轻量重置)保留 session 字符串:跨 eval 存活语义不被执行期 GC 破坏。
|
||||
#[test]
|
||||
fn reset_preserves_session_strings_after_runtime_gc() {
|
||||
let mut vm = vm_with_threshold(4096);
|
||||
let first = compile("globalThis.kept = 'x' + 'y'; 0");
|
||||
vm.run(&first).expect("run1");
|
||||
vm.reset();
|
||||
|
||||
let second = compile("globalThis.kept");
|
||||
let result = vm.run(&second).expect("run2");
|
||||
let text = vm.lookup_str(result).expect("kept 应为字符串").to_string();
|
||||
assert_eq!(text, "xy");
|
||||
}
|
||||
|
||||
/// full_reset 清空 session 内存:执行期回收释放过的死串不与完全重置的
|
||||
/// 整体释放路径重复释放(无双重释放崩溃),重置后引擎可继续运行。
|
||||
#[test]
|
||||
fn full_reset_clears_session_after_runtime_gc() {
|
||||
let mut vm = vm_with_threshold(4096);
|
||||
let first = compile("var s; for (var i = 0; i < 200000; i++) { s = 'a' + i; } s.length");
|
||||
vm.run(&first).expect("run1");
|
||||
assert!(vm.session_gc_stats().total_collections > 0, "run1 应触发执行期字符串 GC");
|
||||
|
||||
vm.full_reset();
|
||||
|
||||
assert_eq!(vm.session_bytes_allocated(), 0, "full_reset 后 session 字节应为 0");
|
||||
let second = compile("'ok' + '!'");
|
||||
let result = vm.run(&second).expect("run2");
|
||||
let text = vm.lookup_str(result).expect("拼接结果应为字符串").to_string();
|
||||
assert_eq!(text, "ok!");
|
||||
}
|
||||
Reference in New Issue
Block a user