Files

222 lines
8.4 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
fn run_source(vm: &mut Vm, source: &str) -> JsValue {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
let module = Compiler::new().compile(&program).expect("compile failed");
vm.run(&Arc::new(module)).expect("vm run failed")
}
fn global_prop(vm: &Vm, name: &str) -> JsValue {
let si = vm.kernel_core().perm_interner().intern(name).0;
let session = vm.session();
let global = session.global_object();
let pos = vm
.kernel_core()
.shape_forge()
.lookup_position(global.shape_id(), si)
.expect("global property slot");
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();
run_source(&mut vm, "globalThis.x = {}; globalThis.x.y = 1");
vm.reset();
let result = run_source(&mut vm, "globalThis.x.y");
assert_eq!(result, JsValue::int(1));
}
#[test]
fn test_array_escape() {
let mut vm = Vm::new();
run_source(&mut vm, "globalThis.a = []; globalThis.a.push({ v: 2 })");
vm.reset();
let result = run_source(
&mut vm,
"var i = 0; while (i < 1000) { var tmp = { v: i }; i = i + 1; } globalThis.a[0].v",
);
assert_eq!(result, JsValue::int(2));
}
#[test]
fn test_closure_captured_this_escape() {
let mut vm = Vm::new();
run_source(&mut vm, "globalThis.marker = { v: 9 }; globalThis.f = () => this.marker.v");
vm.reset();
let function = run_source(&mut vm, "globalThis.f");
let result = run_source(&mut vm, "globalThis.marker.v");
let function_obj = unsafe { &*function.as_js_object_ptr() };
assert!(function.is_object());
assert!(function_obj.is_session_epoch());
assert_eq!(global_prop(&vm, "f"), function);
assert_eq!(result, JsValue::int(9));
}
#[test]
fn test_transitive_escape() {
let mut vm = Vm::new();
run_source(&mut vm, "globalThis.root = {}");
vm.reset();
run_source(&mut vm, "globalThis.root.child = { v: 3 }");
vm.reset();
let result = run_source(&mut vm, "globalThis.root.child.v");
assert_eq!(result, JsValue::int(3));
}
#[test]
fn test_full_reset_clears_session_state() {
let mut vm = Vm::new();
run_source(&mut vm, "globalThis.x = {}; globalThis.x.y = 1");
vm.reset();
assert_eq!(run_source(&mut vm, "globalThis.x.y"), JsValue::int(1));
vm.full_reset();
let result = run_source(&mut vm, "globalThis.x");
assert!(result.is_undefined());
}
/// NEW_OBJECT 字面量必须登记进统一追踪表:`{}` 写入超内联容量的属性会分配
/// hash_props 堆 Box,收集路径经追踪表统一释放;不登记则每个字面量泄漏一个 Box。
/// 对象变量置于函数作用域:顶层 var 在声明写即逃逸 promote,后续属性写落
/// session 克隆(其堆数据不属源对象,reset 不释放)。
#[test]
fn new_object_literal_tracked_and_freed_on_reset() {
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.session_object_count() > 0, "NEW_OBJECT 应登记统一追踪表");
vm.reset();
assert_eq!(vm.session_object_count(), 0, "reset 后追踪表应清空");
}
/// full_reset 同样释放 NEW_OBJECT 字面量:带属性写的源对象经逃逸写直通
/// 留存 global 槽(不克隆),完全重置随 global 重建丢弃槽位并释放源对象堆
/// 数据,不得悬垂。
#[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.session_object_count() > 0, "字面量源对象直通留存 global 槽,登记于统一追踪表");
let keep = global_prop(&vm, "keep");
// SAFETY: 逃逸值是本 VM 自有的 session 对象,本 session 内指针有效。
assert!(
unsafe { (&*keep.as_js_object_ptr()).is_session_epoch() },
"逃逸值应直通留存 global 槽,不克隆进 session"
);
vm.full_reset();
assert_eq!(vm.session_object_count(), 0, "full_reset 后统一追踪表应清空");
assert!(run_source(&mut vm, "globalThis.keep").is_undefined(), "full_reset 后逃逸对象不可再访问");
}
/// 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 = 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.session_object_count() > 0, "exec 结果数组应登记统一追踪表");
vm.reset();
// r 是顶层 var(global 根),reset 后存活;20 个临时 exec 结果数组已释放。
assert_eq!(vm.session_object_count(), 1, "reset 后仅 global 根 r 存活");
}
/// split/match 结果数组必须登记统一追踪表:元素区 Box 随收集路径统一释放;
/// 未登记则每次 split/match 泄漏一个 Box。
#[test]
fn string_split_match_result_array_tracked_and_freed_on_reset() {
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.session_object_count() > 0, "split/match 结果数组应登记统一追踪表");
vm.reset();
assert_eq!(vm.session_object_count(), 0, "reset 后追踪表应清空");
}
/// 错误对象必须登记统一追踪表: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 = low_threshold_vm();
let result = run_source(
&mut vm,
"for (var i = 0; i < 20; i++) { \
try { throw new TypeError('boom'); } catch (e) {} \
try { Error.prototype.toString.call(1); } catch (e) {} \
} \
try { throw new TypeError('boom'); } catch (e) { e.message }",
);
assert_eq!(vm.lookup_str(result).unwrap_or_default(), "boom", "错误对象 message 应正常");
assert!(vm.session_object_count() > 0, "错误对象应登记统一追踪表");
vm.reset();
assert_eq!(vm.session_object_count(), 0, "reset 后追踪表应清空");
}
/// gOPD/Reflect 描述符对象必须登记统一追踪表:4 个描述符属性 push 分配
/// hash_props Box,随收集路径统一释放;未登记则每次 getOwnPropertyDescriptor 泄漏。
#[test]
fn gopd_descriptor_tracked_and_freed_on_reset() {
let mut vm = low_threshold_vm();
let result = run_source(
&mut vm,
"for (var i = 0; i < 20; i++) { \
Object.getOwnPropertyDescriptor({x:1,y:2,z:3,w:4}, 'x'); \
} \
Object.getOwnPropertyDescriptor({x:5}, 'x').value",
);
assert_eq!(format!("{}", result), "5", "gOPD 描述符值应正常");
assert!(vm.session_object_count() > 0, "描述符对象应登记统一追踪表");
vm.reset();
assert_eq!(vm.session_object_count(), 0, "reset 后追踪表应清空");
}