perf: 对象统一入口 Box 化合并对象表退役晋升档

This commit is contained in:
2026-10-01 06:39:51 +00:00
parent 0e3992722e
commit 1711ae948b
13 changed files with 262 additions and 523 deletions
+42 -57
View File
@@ -1,6 +1,7 @@
use std::sync::Arc;
use oxide_compiler::compiler::Compiler;
use oxide_kernel::kernel::{KernelConfig, KernelCore};
use oxide_parser::Allocator;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
@@ -23,6 +24,15 @@ fn global_prop(vm: &Vm, name: &str) -> JsValue {
global.get_prop_at(pos)
}
/// 低阈值 VM:`reset()` 的 session 收集受阈值门控(默认 32MiB),小负载须把
/// 阈值压到 1 字节才能让 reset 真正回收,断言「reset 后追踪表清空/字节释放」
/// 的测试用此构造。
fn low_threshold_vm() -> Vm {
let mut config = KernelConfig::minimal();
config.set_session_gc_threshold(1);
Vm::with_kernel_core(KernelCore::new(config))
}
#[test]
fn test_basic_object_escape() {
let mut vm = Vm::new();
@@ -90,31 +100,26 @@ fn test_full_reset_clears_session_state() {
assert!(result.is_undefined());
}
/// NEW_OBJECT 字面量必须登记进 epoch 追踪表:`{}` 写入超内联容量的属性会分配
/// hash_props 堆 Box,reset 时经追踪表统一释放;不登记则每个字面量泄漏一个 Box。
/// NEW_OBJECT 字面量必须登记进统一追踪表:`{}` 写入超内联容量的属性会分配
/// hash_props 堆 Box,收集路径经追踪表统一释放;不登记则每个字面量泄漏一个 Box。
/// 对象变量置于函数作用域:顶层 var 在声明写即逃逸 promote,后续属性写落
/// session 克隆(其堆数据不属 epoch 源对象,reset 不释放)。
/// session 克隆(其堆数据不属源对象,reset 不释放)。
#[test]
fn new_object_literal_tracked_and_freed_on_reset() {
let mut vm = Vm::new();
let mut vm = low_threshold_vm();
let result = run_source(
&mut vm,
"(function(){var o = {}; for (var i = 0; i < 40; i++) { o['k' + i] = i; } \
delete o.k7; return o.k8;})()",
);
assert_eq!(format!("{}", result), "8", "属性写入/删除后读取应正常");
assert!(vm.epoch_object_count() > 0, "NEW_OBJECT 应登记 epoch 追踪表");
assert!(vm.session_object_count() > 0, "NEW_OBJECT 应登记统一追踪表");
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 后追踪表应清空");
assert_eq!(vm.session_object_count(), 0, "reset 后追踪表应清空");
}
/// full_reset 同样释放 NEW_OBJECT 字面量:带属性写的 epoch 源对象经逃逸写直通
/// full_reset 同样释放 NEW_OBJECT 字面量:带属性写的源对象经逃逸写直通
/// 留存 global 槽(不克隆),完全重置随 global 重建丢弃槽位并释放源对象堆
/// 数据,不得悬垂。
#[test]
@@ -126,70 +131,60 @@ fn new_object_literal_freed_by_full_reset() {
globalThis.keep = o; o.k7",
);
assert_eq!(format!("{}", result), "7", "逃逸后属性读取应正常");
assert!(vm.epoch_object_count() > 0, "字面量源对象直通留存 epoch,登记于 epoch 追踪表");
assert!(vm.session_object_count() > 0, "字面量源对象直通留存 global 槽,登记于统一追踪表");
let keep = global_prop(&vm, "keep");
// SAFETY: 逃逸值是本 VM 自有的 epoch 对象,本 session 内指针有效。
// SAFETY: 逃逸值是本 VM 自有的 session 对象,本 session 内指针有效。
assert!(
unsafe { (&*keep.as_js_object_ptr()).is_epoch() },
"逃逸值应直通留存 epoch,不克隆进 session"
unsafe { (&*keep.as_js_object_ptr()).is_session_epoch() },
"逃逸值应直通留存 global 槽,不克隆进 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_eq!(vm.session_object_count(), 0, "full_reset 后统一追踪表应清空");
assert!(run_source(&mut vm, "globalThis.keep").is_undefined(), "full_reset 后逃逸对象不可再访问");
}
/// regexp exec 结果数组必须登记 epoch 追踪表:数组元素区 Box 与 index/input 命名
/// 属性 hash_props Box 随 reset 统一释放;未登记则每次 exec/Symbol.match 泄漏。
/// regexp exec 结果数组必须登记统一追踪表:数组元素区 Box 与 index/input 命名
/// 属性 hash_props Box 随收集路径统一释放;未登记则每次 exec/Symbol.match 泄漏。
#[test]
fn regexp_exec_result_array_tracked_and_freed_on_reset() {
let mut vm = Vm::new();
let mut vm = low_threshold_vm();
let result = run_source(
&mut vm,
"for (var i = 0; i < 20; i++) { /a(b)?/.exec('ab'); } \
var r = /a(b)?/.exec('ab'); r.length",
);
assert_eq!(format!("{}", result), "2", "exec 结果数组长度应正常");
assert!(vm.epoch_object_count() > 0, "exec 结果数组应登记 epoch 追踪表");
assert!(vm.session_object_count() > 0, "exec 结果数组应登记统一追踪表");
let freed_before = vm.session_gc_stats().total_bytes_freed;
vm.reset();
assert!(
vm.session_gc_stats().total_bytes_freed > freed_before,
"reset 应释放 exec 结果数组的元素区与命名属性堆数据"
);
assert_eq!(vm.epoch_object_count(), 0, "reset 后追踪表应清空");
// r 是顶层 var(global 根),reset 后存活;20 个临时 exec 结果数组已释放。
assert_eq!(vm.session_object_count(), 1, "reset 后仅 global 根 r 存活");
}
/// split/match 结果数组必须登记 epoch 追踪表:元素区 Box 随 reset 统一释放;
/// split/match 结果数组必须登记统一追踪表:元素区 Box 随收集路径统一释放;
/// 未登记则每次 split/match 泄漏一个 Box。
#[test]
fn string_split_match_result_array_tracked_and_freed_on_reset() {
let mut vm = Vm::new();
let mut vm = low_threshold_vm();
let result = run_source(
&mut vm,
"for (var i = 0; i < 20; i++) { 'a,b,c'.split(','); 'ab'.match(/a/); } \
'a,b,c'.split(',').length",
);
assert_eq!(format!("{}", result), "3", "split 结果数组长度应正常");
assert!(vm.epoch_object_count() > 0, "split/match 结果数组应登记 epoch 追踪表");
assert!(vm.session_object_count() > 0, "split/match 结果数组应登记统一追踪表");
let freed_before = vm.session_gc_stats().total_bytes_freed;
vm.reset();
assert!(
vm.session_gc_stats().total_bytes_freed > freed_before,
"reset 应释放 split/match 结果数组的元素区堆数据"
);
assert_eq!(vm.epoch_object_count(), 0, "reset 后追踪表应清空");
assert_eq!(vm.session_object_count(), 0, "reset 后追踪表应清空");
}
/// 错误对象必须登记 epoch 追踪表:message 非空时 push_prop 分配 hash_props Box,
/// 随 reset 统一释放;未登记则每次抛错(全引擎最频繁路径)泄漏一个 Box。
/// 错误对象必须登记统一追踪表:message 非空时 push_prop 分配 hash_props Box,
/// 随收集路径统一释放;未登记则每次抛错(全引擎最频繁路径)泄漏一个 Box。
/// 同时覆盖构造器路径(`new TypeError`)与内部抛错路径(builtin 内 create_type_error)。
#[test]
fn error_object_tracked_and_freed_on_reset() {
let mut vm = Vm::new();
let mut vm = low_threshold_vm();
let result = run_source(
&mut vm,
"for (var i = 0; i < 20; i++) { \
@@ -199,22 +194,17 @@ fn error_object_tracked_and_freed_on_reset() {
try { throw new TypeError('boom'); } catch (e) { e.message }",
);
assert_eq!(vm.lookup_str(result).unwrap_or_default(), "boom", "错误对象 message 应正常");
assert!(vm.epoch_object_count() > 0, "错误对象应登记 epoch 追踪表");
assert!(vm.session_object_count() > 0, "错误对象应登记统一追踪表");
let freed_before = vm.session_gc_stats().total_bytes_freed;
vm.reset();
assert!(
vm.session_gc_stats().total_bytes_freed > freed_before,
"reset 应释放错误对象的 message 属性堆数据"
);
assert_eq!(vm.epoch_object_count(), 0, "reset 后追踪表应清空");
assert_eq!(vm.session_object_count(), 0, "reset 后追踪表应清空");
}
/// gOPD/Reflect 描述符对象必须登记 epoch 追踪表:4 个描述符属性 push 分配
/// hash_props Box,随 reset 统一释放;未登记则每次 getOwnPropertyDescriptor 泄漏。
/// gOPD/Reflect 描述符对象必须登记统一追踪表:4 个描述符属性 push 分配
/// hash_props Box,随收集路径统一释放;未登记则每次 getOwnPropertyDescriptor 泄漏。
#[test]
fn gopd_descriptor_tracked_and_freed_on_reset() {
let mut vm = Vm::new();
let mut vm = low_threshold_vm();
let result = run_source(
&mut vm,
"for (var i = 0; i < 20; i++) { \
@@ -223,13 +213,8 @@ fn gopd_descriptor_tracked_and_freed_on_reset() {
Object.getOwnPropertyDescriptor({x:5}, 'x').value",
);
assert_eq!(format!("{}", result), "5", "gOPD 描述符值应正常");
assert!(vm.epoch_object_count() > 0, "描述符对象应登记 epoch 追踪表");
assert!(vm.session_object_count() > 0, "描述符对象应登记统一追踪表");
let freed_before = vm.session_gc_stats().total_bytes_freed;
vm.reset();
assert!(
vm.session_gc_stats().total_bytes_freed > freed_before,
"reset 应释放描述符对象的 hash_props 堆数据"
);
assert_eq!(vm.epoch_object_count(), 0, "reset 后追踪表应清空");
assert_eq!(vm.session_object_count(), 0, "reset 后追踪表应清空");
}