From 355b0233f05ba812bcde7cfebeea079bcd752755 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 16 Aug 2026 02:16:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20NEW=5FOBJECT=20=E8=B5=B0=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=88=86=E9=85=8D=E5=85=A5=E5=8F=A3=E7=99=BB=E8=AE=B0?= =?UTF-8?q?=20epoch=20=E5=AF=B9=E8=B1=A1=E6=B6=88=E9=99=A4=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E5=90=91=E9=87=8F=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/dispatch/misc.rs | 4 +- crates/oxide_vm/tests/session_arena_tests.rs | 45 ++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/crates/oxide_vm/src/dispatch/misc.rs b/crates/oxide_vm/src/dispatch/misc.rs index 46e106b..f103af8 100644 --- a/crates/oxide_vm/src/dispatch/misc.rs +++ b/crates/oxide_vm/src/dispatch/misc.rs @@ -227,9 +227,7 @@ impl Vm { shape_id = self.kernel_core.shape_forge().make_shape(shape_id, si); } } - let obj = self - .epoch - .alloc(JsObject::new_empty(shape_id, JsValue::from_js_object(proto_ptr))); + let obj = self.alloc_object(JsObject::new_empty(shape_id, JsValue::from_js_object(proto_ptr))); // 预分配数据槽并同步 generation:批量一次性 +nprops,与逐属性路径的终值一致。 let obj_ref = unsafe { &mut *obj }; for _ in 0..nprops { diff --git a/crates/oxide_vm/tests/session_arena_tests.rs b/crates/oxide_vm/tests/session_arena_tests.rs index 0bd440e..7fc903b 100644 --- a/crates/oxide_vm/tests/session_arena_tests.rs +++ b/crates/oxide_vm/tests/session_arena_tests.rs @@ -87,3 +87,48 @@ fn test_full_reset_clears_session_state() { let result = run_source(&mut vm, "globalThis.x"); assert!(result.is_undefined()); } + +/// NEW_OBJECT 字面量必须登记进 epoch 追踪表:`{}` 写入超内联容量的属性会分配 +/// hash_props 堆 Box,reset 时经追踪表统一释放;不登记则每个字面量泄漏一个 Box。 +#[test] +fn new_object_literal_tracked_and_freed_on_reset() { + let mut vm = Vm::new(); + let result = run_source( + &mut vm, + "var o = {}; for (var i = 0; i < 40; i++) { o['k' + i] = i; } \ + delete o.k7; o.k8", + ); + assert_eq!(format!("{}", result), "8", "属性写入/删除后读取应正常"); + assert!(vm.epoch_object_count() > 0, "NEW_OBJECT 应登记 epoch 追踪表"); + + let freed_before = vm.session_gc_stats().total_bytes_freed; + vm.reset(); + assert!( + vm.session_gc_stats().total_bytes_freed > freed_before, + "reset 应释放字面量对象的属性向量堆数据" + ); + assert_eq!(vm.epoch_object_count(), 0, "reset 后追踪表应清空"); +} + +/// full_reset 同样释放 NEW_OBJECT 字面量:带属性写的 epoch 源对象经 promote 深拷贝 +/// 到 session 后,完全重置同时释放源对象与克隆的堆数据,不得双重释放。 +#[test] +fn new_object_literal_freed_by_full_reset() { + let mut vm = Vm::new(); + let result = run_source( + &mut vm, + "var o = {}; for (var i = 0; i < 40; i++) { o['k' + i] = i; } \ + globalThis.keep = o; o.k7", + ); + assert_eq!(format!("{}", result), "7", "逃逸后属性读取应正常"); + assert!(vm.epoch_object_count() > 0, "字面量源对象在 promote 后仍登记于 epoch 追踪表"); + assert!(vm.session_object_count() > 0, "逃逸克隆应登记于 session 追踪表"); + + vm.full_reset(); + assert_eq!(vm.epoch_object_count(), 0, "full_reset 后 epoch 追踪表应清空"); + assert_eq!(vm.session_object_count(), 0, "full_reset 后 session 追踪表应清空"); + assert!( + run_source(&mut vm, "globalThis.keep").is_undefined(), + "full_reset 后逃逸对象不可再访问" + ); +}