mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
perf: 对象统一入口 Box 化合并对象表退役晋升档
This commit is contained in:
@@ -530,19 +530,20 @@ fn promote_then_reset_keeps_entries_alive() {
|
||||
assert_eq!(text, "true|false");
|
||||
}
|
||||
|
||||
/// drop 记账:无引用的 async 栈对象被收集时释放状态盒(freed 字节增长)。
|
||||
/// drop 记账:无引用的 async 栈对象被收集时释放状态盒并计入释放字节。
|
||||
/// 低阈值使执行期收集在循环内触发(默认 32MiB 阈值下本负载不触发收集)。
|
||||
#[test]
|
||||
fn drop_accounts_capability_bytes() {
|
||||
let mut vm = Vm::new();
|
||||
let mut config = KernelConfig::minimal();
|
||||
config.set_session_gc_threshold(1);
|
||||
let mut vm = Vm::with_kernel_core(KernelCore::new(config));
|
||||
let allocator = Allocator::default();
|
||||
let program = oxide_parser::parse(&allocator, "for (var i = 0; i < 500; i++) { new AsyncDisposableStack(); } 0")
|
||||
.expect("parse");
|
||||
let module = Compiler::new().compile(&program).expect("compile");
|
||||
vm.run(&Arc::new(module)).expect("run");
|
||||
let before = vm.session_gc_stats().total_bytes_freed;
|
||||
vm.reset();
|
||||
let after = vm.session_gc_stats().total_bytes_freed;
|
||||
assert!(after > before, "reset 应回收栈对象状态盒,before={before} after={after}");
|
||||
let freed = vm.session_gc_stats().total_bytes_freed;
|
||||
assert!(freed > 0, "收集应释放死栈对象状态盒并记账,freed={freed}");
|
||||
}
|
||||
|
||||
/// full_reset(dirty 重建):global 与 object 家族重建后 AsyncDisposableStack 仍
|
||||
|
||||
@@ -424,15 +424,16 @@ fn promote_then_reset_keeps_entries_alive() {
|
||||
assert_eq!(text, "true|false");
|
||||
}
|
||||
|
||||
/// drop 记账:无引用的栈对象被收集时释放状态盒(freed 字节增长)。
|
||||
/// drop 记账:无引用的栈对象被收集时释放状态盒并计入释放字节。
|
||||
/// 低阈值使执行期收集在循环内触发(默认 32MiB 阈值下本负载不触发收集)。
|
||||
#[test]
|
||||
fn drop_accounts_capability_bytes() {
|
||||
let mut vm = make_vm();
|
||||
let mut config = KernelConfig::minimal();
|
||||
config.set_session_gc_threshold(1);
|
||||
let mut vm = Vm::with_kernel_core(KernelCore::new(config));
|
||||
eval_in(&mut vm, "for (var i = 0; i < 500; i++) { new DisposableStack(); } 0").expect("run");
|
||||
let before = vm.session_gc_stats().total_bytes_freed;
|
||||
vm.reset();
|
||||
let after = vm.session_gc_stats().total_bytes_freed;
|
||||
assert!(after > before, "reset 应回收栈对象状态盒,before={before} after={after}");
|
||||
let freed = vm.session_gc_stats().total_bytes_freed;
|
||||
assert!(freed > 0, "收集应释放死栈对象状态盒并记账,freed={freed}");
|
||||
}
|
||||
|
||||
/// full_reset(dirty 重建):global 与 object 家族重建后 DisposableStack 仍完整可用
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_kernel::kernel::{KernelConfig, KernelCore};
|
||||
use oxide_types::value::JsValue;
|
||||
use oxide_vm::vm::Vm;
|
||||
use regress::Regex;
|
||||
@@ -23,35 +24,36 @@ fn to_str(vm: &Vm, val: JsValue) -> String {
|
||||
|
||||
const ROUNDS: u64 = 256;
|
||||
|
||||
/// 字符串模式路径:N 个死载体(包装器 + 载体)经完整收集走 sweep 死分支,
|
||||
/// 字符串模式路径:N 个死载体(包装器 + 载体)经收集走 sweep 死分支,
|
||||
/// 释放字节必须含 N 份编译正则——守卫未命中时该部分恒不计入。
|
||||
/// 低阈值使执行期收集在循环内触发(默认 32MiB 阈值下本负载不触发收集)。
|
||||
#[test]
|
||||
fn string_pattern_match_all_boxes_freed_by_sweep() {
|
||||
let mut vm = Vm::new();
|
||||
let mut config = KernelConfig::minimal();
|
||||
config.set_session_gc_threshold(1);
|
||||
let mut vm = Vm::with_kernel_core(KernelCore::new(config));
|
||||
eval(&mut vm, "for (let i = 0; i < 256; i++) { 'aaaa'.matchAll('a'); }").unwrap();
|
||||
let freed_before = vm.session_gc_stats().total_bytes_freed;
|
||||
vm.reset();
|
||||
let freed_delta = vm.session_gc_stats().total_bytes_freed - freed_before;
|
||||
let freed = vm.session_gc_stats().total_bytes_freed;
|
||||
let min_regex_bytes = ROUNDS * std::mem::size_of::<Regex>() as u64;
|
||||
assert!(
|
||||
freed_delta >= min_regex_bytes,
|
||||
"sweep 释放 {freed_delta} 字节,应含 {ROUNDS} 份编译正则(下界 {min_regex_bytes})"
|
||||
freed >= min_regex_bytes,
|
||||
"sweep 释放 {freed} 字节,应含 {ROUNDS} 份编译正则(下界 {min_regex_bytes})"
|
||||
);
|
||||
}
|
||||
|
||||
/// 非 global 正则路径:`RegExp.prototype.matchAll` 复制补 g 建载体,同口径断言。
|
||||
#[test]
|
||||
fn non_global_regexp_match_all_boxes_freed_by_sweep() {
|
||||
let mut vm = Vm::new();
|
||||
let mut config = KernelConfig::minimal();
|
||||
config.set_session_gc_threshold(1);
|
||||
let mut vm = Vm::with_kernel_core(KernelCore::new(config));
|
||||
// 真则常驻(存活根),循环内只产死载体:释放下界唯一来源是载体 Box。
|
||||
eval(&mut vm, "var r = /ab/; for (let i = 0; i < 256; i++) { r[Symbol.matchAll]('abab'); }").unwrap();
|
||||
let freed_before = vm.session_gc_stats().total_bytes_freed;
|
||||
vm.reset();
|
||||
let freed_delta = vm.session_gc_stats().total_bytes_freed - freed_before;
|
||||
let freed = vm.session_gc_stats().total_bytes_freed;
|
||||
let min_regex_bytes = ROUNDS * std::mem::size_of::<Regex>() as u64;
|
||||
assert!(
|
||||
freed_delta >= min_regex_bytes,
|
||||
"sweep 释放 {freed_delta} 字节,应含 {ROUNDS} 份编译正则(下界 {min_regex_bytes})"
|
||||
freed >= min_regex_bytes,
|
||||
"sweep 释放 {freed} 字节,应含 {ROUNDS} 份编译正则(下界 {min_regex_bytes})"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
//! 确定性账目。
|
||||
//!
|
||||
//! 15 层满二叉树(32767 节点)在「写路径直通(不引克隆)+ 根 promote 深克隆」
|
||||
//! 晋升语义下的三个确定性计数。run 末 session 表只含直 session 分配对象,
|
||||
//! 整树节点字面量驻留 epoch;promote_rooted 把顶层 var 寄存器根的 epoch 原件
|
||||
//! 深克隆为 session 内唯一副本。晋升语义或根处理改动后,若以下任一数值变化,
|
||||
//! 说明 session 留存账目已变——须对照 tests/stress/mem_object_graph.js 的基线
|
||||
//! (benchmark_baseline.json)复核后再更新本锚。
|
||||
//! 晋升语义下的确定性计数。对象分配统一入口 Box 化后全部对象入统一表,
|
||||
//! run 末统一表含整树节点字面量 + 每次 JS 调用无条件创建的 arguments 对象 +
|
||||
//! 直 session 分配(函数对象与 prototype 子对象);promote_rooted 对 session
|
||||
//! 对象全空转(统一表后无 epoch 原件可晋升)。晋升语义或根处理改动后,若以下
|
||||
//! 任一数值变化,说明 session 留存账目已变——须对照 tests/stress/mem_object_graph.js
|
||||
//! 的基线(benchmark_baseline.json)复核后再更新本锚。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -42,19 +43,16 @@ fn run_module(source: &str) -> Vm {
|
||||
vm
|
||||
}
|
||||
|
||||
/// 留存锚:逃逸写直通后 run 末 session 表只含直 session 分配的 2 枚对象
|
||||
/// (makeNode 函数对象与其 prototype 子对象),整树节点字面量驻留 epoch,
|
||||
/// 顶层 var 寄存器槽仍持有 epoch 原件(SET 路径不回写寄存器);测量点
|
||||
/// promote_rooted 把寄存器原件深克隆进 session(转发表去重,整树 32767),
|
||||
/// full GC 后逻辑存活集(整树 + 函数 2)在 session 内恰一份、无双计。
|
||||
/// epoch 侧 65534 的 2× 与节点无关:其中 32767 是每次 JS 调用无条件创建的
|
||||
/// arguments 对象(CREATE_ARGUMENTS),另 32767 才是节点字面量。
|
||||
/// 留存锚:对象分配统一入口 Box 化后 run 末统一表含全部对象——整树节点
|
||||
/// 字面量 32767 + 每次 JS 调用无条件创建的 arguments 对象 32767 + 直 session
|
||||
/// 分配 2(makeNode 函数对象与其 prototype 子对象)= 65536。promote_rooted
|
||||
/// 对 session 对象全空转(统一表后无 epoch 原件可晋升);full GC 后逻辑存活集
|
||||
/// (整树 + 函数 2)在 session 内恰一份、无双计。
|
||||
#[test]
|
||||
fn object_graph_retention_anchor() {
|
||||
let mut vm = run_module(OBJECT_GRAPH);
|
||||
|
||||
assert_eq!(vm.epoch_object_count(), 65534, "run 末 epoch 计数");
|
||||
assert_eq!(vm.session_object_count(), 2, "run 末 session 直分配计数(函数对象 + prototype 子对象)");
|
||||
assert_eq!(vm.session_object_count(), 65536, "run 末统一表计数(节点 + arguments + 直分配)");
|
||||
|
||||
vm.promote_rooted_epoch_objects();
|
||||
vm.promote_session_epoch_refs();
|
||||
|
||||
@@ -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 后追踪表应清空");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user