From a39236a775eff13a5d2e9b53855477f8b00c0349 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 15 Aug 2026 13:50:57 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=89=A7=E8=A1=8C=E6=9C=9F=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2=20GC=20=E8=A7=A6=E5=8F=91=E5=8A=A0=20native?= =?UTF-8?q?=20=E6=B7=B1=E5=BA=A6=E5=AE=88=E5=8D=AB=E9=98=B2=E5=B5=8C?= =?UTF-8?q?=E5=A5=97=20dispatch=20=E5=9B=9E=E8=B0=83=E6=82=AC=E5=9E=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/vm.rs | 12 ++-- crates/oxide_vm/tests/gc_runtime_tests.rs | 78 +++++++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index e48cf797..668be65f 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -1320,10 +1320,14 @@ 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 { + // 执行期字符串 GC 安全点:仅在顶层 dispatch(native_call_depth == 0) + // 的指令边界触发——嵌套 dispatch(builtin 经 call_function_sync 重入 + // 执行 JS 回调、generator/async 恢复)期间,调用方寄存器窗口副本存于 + // inline 状态(非 GC 根),此时回收会把调用方 regs 中的活串当死串释放。 + // 返回顶层后检查恢复,存活串此时已回拷为执行根。账目未超水位时仅 + // 3 次字段比较。 + if self.native_call_depth == 0 && self.gc_state.session_bytes_allocated >= self.gc_state.string_gc_watermark + { self.maybe_collect_session_strings(); } if let Some(max_steps) = max_steps { diff --git a/crates/oxide_vm/tests/gc_runtime_tests.rs b/crates/oxide_vm/tests/gc_runtime_tests.rs index caf3bce7..9c03e718 100644 --- a/crates/oxide_vm/tests/gc_runtime_tests.rs +++ b/crates/oxide_vm/tests/gc_runtime_tests.rs @@ -132,6 +132,84 @@ fn replace_replacer_args_survive_runtime_gc() { assert!(vm.session_gc_stats().total_collections > 0, "执行期应触发字符串 GC"); } +/// 嵌套 dispatch(map 回调经 call_function_sync → 内联字节码执行)内分配超水位时, +/// 调用方寄存器窗口(`save_inline_state` 存入 inline 状态,非 GC 根)持的 session +/// 串不得被执行期回收:回调期间触发收集会把它当死串释放,回拷后成悬垂。 +/// `held` 经 middle 参数位于低号寄存器,恰落在回调写寄存器区(0..n_registers), +/// 修复前回调内触发收集即把它释放并复用(结果变垃圾串),修复后回调期间不回收。 +#[test] +fn map_callback_caller_reg_strings_survive_nested_dispatch() { + 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); } \ + function middle(held) { \ + var r = [1, 2, 3].map(function(x) { var t; for (var j = 0; j < 300; j++) { t = 'z' + j; } return x * 2; }); \ + return held; } \ + var out = middle('mid' + 'str'); out", + ); + let result = vm.run(&module).expect("run"); + let text = vm.lookup_str(result).expect("held 应为字符串").to_string(); + assert_eq!(text, "midstr"); + assert!(vm.session_gc_stats().total_collections > 0, "执行期应触发字符串 GC"); +} + +/// 函数 replacer 回调同样重入嵌套 dispatch:调用方 regs 持串跨回调存活, +/// 回调体内分配不回收调用方窗口中的串。 +#[test] +fn replace_replacer_caller_reg_strings_survive_nested_dispatch() { + 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); } \ + function middle(held) { \ + var out = 'a-b-c'.replace(/-/g, function(m, pos, str) { var t; for (var j = 0; j < 300; j++) { t = 'y' + j; } return '[' + m + ']'; }); \ + return held + '|' + out; } \ + var out = middle('repl' + 'acer'); out", + ); + let result = vm.run(&module).expect("run"); + let text = vm.lookup_str(result).expect("结果应为字符串").to_string(); + assert_eq!(text, "replacer|a[-]b[-]c"); + assert!(vm.session_gc_stats().total_collections > 0, "执行期应触发字符串 GC"); +} + +/// generator.next() 经生成器恢复重入嵌套 dispatch:调用方 regs(参数串与累加器) +/// 在生成器体执行期间不得被回收。修复前该场景直接段错误(悬垂指针解引用)。 +#[test] +fn generator_next_loop_caller_reg_strings_survive_nested_dispatch() { + 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); } \ + function middle(held) { \ + function* gen() { for (var i = 0; i < 5; i++) { var t; for (var j = 0; j < 200; j++) { t = 'g' + j; } yield i; } } \ + var it = gen(); var acc = 0; for (var i = 0; i < 5; i++) { acc += it.next().value; } \ + return held + '|' + acc; } \ + var out = middle('gen' + 'str'); out", + ); + let result = vm.run(&module).expect("run"); + let text = vm.lookup_str(result).expect("结果应为字符串").to_string(); + assert_eq!(text, "genstr|10"); + assert!(vm.session_gc_stats().total_collections > 0, "执行期应触发字符串 GC"); +} + +/// 嵌套回调组合(map 回调内再调 map):两层内联 dispatch 叠加,任一层指令边界 +/// 触发收集都不回收调用方窗口中的串。 +#[test] +fn nested_inline_callbacks_caller_reg_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); } \ + function middle(held) { \ + var r = [1, 2].map(function(x) { \ + var inner = [10, 20].map(function(y) { var t; for (var j = 0; j < 200; j++) { t = 'n' + j; } return y; }); \ + return inner[0] + inner[1] + x; }); \ + return held + '|' + r.join(','); } \ + var out = middle('nest' + 'ed'); out", + ); + let result = vm.run(&module).expect("run"); + let text = vm.lookup_str(result).expect("结果应为字符串").to_string(); + assert_eq!(text, "nested|31,32"); + assert!(vm.session_gc_stats().total_collections > 0, "执行期应触发字符串 GC"); +} + /// full_reset 清空 session 内存:执行期回收释放过的死串不与完全重置的 /// 整体释放路径重复释放(无双重释放崩溃),重置后引擎可继续运行。 #[test]