mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
1921 lines
82 KiB
Rust
1921 lines
82 KiB
Rust
use std::collections::{HashMap, HashSet};
|
||
use std::mem::size_of;
|
||
use std::time::Instant;
|
||
|
||
use crate::{vm_debug, vm_info};
|
||
use oxide_types::object::{JsObject, JsString, PropMetaEntry};
|
||
use oxide_types::value::JsValue;
|
||
use rustc_hash::FxBuildHasher;
|
||
|
||
use crate::vm::Vm;
|
||
use oxide_builtins::{array_buffer, data_view, disposable_stack, map, regexp, set, typed_array};
|
||
|
||
/// session 级 mark-sweep GC 的状态与统计。
|
||
///
|
||
/// 回收 session arena 中不再可达的对象与 `Vm::new_string` 分配的 session 字符串:
|
||
/// `mark` 从 VM roots 标记存活对象,`sweep` 将存活对象复制进新 arena(移动式)、
|
||
/// 更新所有引用,并释放死对象;`sweep_session_strings` 按存活标记回收字符串。
|
||
/// 所有统计字段供外部观测 GC 行为。
|
||
pub struct SessionGc {
|
||
pub total_collections: u64,
|
||
pub total_bytes_freed: u64,
|
||
pub total_objects_scanned: u64,
|
||
pub total_objects_live: u64,
|
||
pub total_objects_dead: u64,
|
||
pub last_collection_objects_scanned: u64,
|
||
pub last_collection_objects_live: u64,
|
||
pub last_collection_objects_dead: u64,
|
||
pub last_collection_bytes_freed: u64,
|
||
pub last_collection_duration_us: u64,
|
||
pub max_collection_duration_us: u64,
|
||
pub min_collection_duration_us: u64,
|
||
pub(crate) mark_stack: Vec<*mut JsObject>,
|
||
pub(crate) live_strings: HashSet<*mut JsString, FxBuildHasher>,
|
||
pub(crate) live_bigints: HashSet<*mut num_bigint::BigInt, FxBuildHasher>,
|
||
}
|
||
|
||
impl SessionGc {
|
||
/// 创建全零统计、空标记栈的空 GC 实例。
|
||
pub fn new() -> Self {
|
||
Self::default()
|
||
}
|
||
}
|
||
|
||
impl SessionGc {
|
||
pub(crate) fn clear_all_marks(&mut self, vm: &mut Vm) {
|
||
vm_debug!("[GC] clear_all_marks: {} session objects", vm.gc_state.session_object_ptrs.len());
|
||
for &ptr in &vm.gc_state.session_object_ptrs {
|
||
if ptr.is_null() {
|
||
continue;
|
||
}
|
||
// SAFETY: session_object_ptrs 中的指针只来自 promote_object_inner 的
|
||
// session_epoch.alloc,在 arena 存活期间有效。
|
||
unsafe { (*ptr).set_gc_mark(false) };
|
||
}
|
||
}
|
||
|
||
/// 只读核算 session 对象的堆数据字节(属性/元素/meta Vec capacity + native 状态盒)。
|
||
/// 与 `drop_object_heap_data` 释放口径一致(capacity),不释放、不置空任何指针。
|
||
pub(crate) fn object_heap_data_bytes(obj: &JsObject) -> u64 {
|
||
let mut bytes = 0u64;
|
||
|
||
let elems_ptr = obj.array_elements_raw() as *const Vec<JsValue>;
|
||
if !elems_ptr.is_null() {
|
||
unsafe {
|
||
bytes += size_of::<Vec<JsValue>>() as u64
|
||
+ ((*elems_ptr).capacity() * size_of::<JsValue>()) as u64;
|
||
}
|
||
}
|
||
|
||
let elems_meta_ptr = obj.array_elements_meta_raw() as *const Vec<Option<PropMetaEntry>>;
|
||
if !elems_meta_ptr.is_null() {
|
||
unsafe {
|
||
bytes += size_of::<Vec<Option<PropMetaEntry>>>() as u64
|
||
+ ((*elems_meta_ptr).capacity() * size_of::<Option<PropMetaEntry>>()) as u64;
|
||
}
|
||
}
|
||
|
||
let hash_ptr = obj.hash_props_raw() as *const Vec<JsValue>;
|
||
if !hash_ptr.is_null() {
|
||
unsafe {
|
||
bytes += size_of::<Vec<JsValue>>() as u64
|
||
+ ((*hash_ptr).capacity() * size_of::<JsValue>()) as u64;
|
||
}
|
||
}
|
||
|
||
let meta_ptr = obj.prop_meta_raw() as *const Vec<Option<PropMetaEntry>>;
|
||
if !meta_ptr.is_null() {
|
||
unsafe {
|
||
bytes += size_of::<Vec<Option<PropMetaEntry>>>() as u64
|
||
+ ((*meta_ptr).capacity() * size_of::<Option<PropMetaEntry>>()) as u64;
|
||
}
|
||
}
|
||
|
||
bytes += map::map_native_size(obj);
|
||
bytes += set::set_native_size(obj);
|
||
bytes += disposable_stack::disposable_stack_native_size(obj);
|
||
bytes += array_buffer::array_buffer_native_size(obj);
|
||
bytes += regexp::regexp_native_size(obj);
|
||
bytes += typed_array::typed_array_native_size(obj);
|
||
bytes += data_view::data_view_native_size(obj);
|
||
bytes += crate::generator::generator_native_size(obj);
|
||
bytes += crate::promise::promise_native_size(obj);
|
||
bytes += crate::async_func::async_native_size(obj);
|
||
bytes += crate::async_generator::async_generator_native_size(obj);
|
||
|
||
bytes
|
||
}
|
||
|
||
/// 扫描 `obj` 全部引用边,session 对象子节点推入 `stack`,字符串边标记存活,
|
||
/// BigInt 边标记存活。
|
||
///
|
||
/// 单趟替代原 `object_edges` + `record_object_string_edges` 双遍模式:消除每对象
|
||
/// Vec 分配与重复字段遍历。
|
||
fn scan_edges_for_mark(
|
||
obj: &JsObject,
|
||
vm: &Vm,
|
||
stack: &mut Vec<*mut JsObject>,
|
||
live_strings: &mut HashSet<*mut JsString, FxBuildHasher>,
|
||
live_bigints: &mut HashSet<*mut num_bigint::BigInt, FxBuildHasher>,
|
||
) {
|
||
if let Some(elements) = obj.array_elements_vec() {
|
||
for &value in elements.iter() {
|
||
Self::process_edge(value, vm, stack, live_strings, live_bigints);
|
||
}
|
||
}
|
||
if let Some(meta) = obj.array_elements_meta_vec() {
|
||
for entry in meta.iter().flatten() {
|
||
Self::process_edge(entry.get, vm, stack, live_strings, live_bigints);
|
||
Self::process_edge(entry.set, vm, stack, live_strings, live_bigints);
|
||
}
|
||
}
|
||
if let Some(props) = obj.hash_props_vec() {
|
||
for &value in props.iter() {
|
||
Self::process_edge(value, vm, stack, live_strings, live_bigints);
|
||
}
|
||
}
|
||
if let Some(meta) = obj.prop_meta_vec() {
|
||
for entry in meta.iter().flatten() {
|
||
Self::process_edge(entry.get, vm, stack, live_strings, live_bigints);
|
||
Self::process_edge(entry.set, vm, stack, live_strings, live_bigints);
|
||
}
|
||
}
|
||
Self::process_edge(obj.proto(), vm, stack, live_strings, live_bigints);
|
||
Self::process_edge(obj.captured_this(), vm, stack, live_strings, live_bigints);
|
||
Self::process_edge(obj.home_object(), vm, stack, live_strings, live_bigints);
|
||
if obj.is_map() {
|
||
for value in map::map_native_edges(obj) {
|
||
Self::process_edge(value, vm, stack, live_strings, live_bigints);
|
||
}
|
||
}
|
||
if obj.is_set() {
|
||
for value in set::set_native_edges(obj) {
|
||
Self::process_edge(value, vm, stack, live_strings, live_bigints);
|
||
}
|
||
}
|
||
if obj.is_disposable_stack_obj() || obj.is_async_disposable_stack_obj() {
|
||
for value in disposable_stack::dispose_edges(obj) {
|
||
Self::process_edge(value, vm, stack, live_strings, live_bigints);
|
||
}
|
||
}
|
||
if obj.is_typed_array_obj() {
|
||
for value in typed_array::typed_array_native_edges(obj) {
|
||
Self::process_edge(value, vm, stack, live_strings, live_bigints);
|
||
}
|
||
}
|
||
if obj.is_data_view_obj() {
|
||
for value in data_view::data_view_native_edges(obj) {
|
||
Self::process_edge(value, vm, stack, live_strings, live_bigints);
|
||
}
|
||
}
|
||
if obj.is_generator_obj() {
|
||
for value in crate::generator::generator_native_edges(obj) {
|
||
Self::process_edge(value, vm, stack, live_strings, live_bigints);
|
||
}
|
||
for ptr in crate::generator::generator_native_string_edges(obj) {
|
||
Self::mark_string_live(live_strings, ptr);
|
||
}
|
||
}
|
||
if obj.is_promise_obj() {
|
||
for value in crate::promise::promise_native_edges(obj) {
|
||
Self::process_edge(value, vm, stack, live_strings, live_bigints);
|
||
}
|
||
}
|
||
if obj.is_async_obj() {
|
||
for value in crate::async_func::async_native_edges(obj) {
|
||
Self::process_edge(value, vm, stack, live_strings, live_bigints);
|
||
}
|
||
for ptr in crate::async_func::async_native_string_edges(obj) {
|
||
Self::mark_string_live(live_strings, ptr);
|
||
}
|
||
}
|
||
if obj.is_async_generator_obj() {
|
||
for value in crate::async_generator::async_generator_native_edges(obj) {
|
||
Self::process_edge(value, vm, stack, live_strings, live_bigints);
|
||
}
|
||
for ptr in crate::async_generator::async_generator_native_string_edges(obj) {
|
||
Self::mark_string_live(live_strings, ptr);
|
||
}
|
||
}
|
||
// 遍历 upvalue cell 中的引用。
|
||
for cell_ptr in obj.upvalues_slice() {
|
||
if cell_ptr.is_null() {
|
||
continue;
|
||
}
|
||
let cell = unsafe { &**cell_ptr };
|
||
Self::process_edge(cell.value, vm, stack, live_strings, live_bigints);
|
||
}
|
||
}
|
||
|
||
#[inline]
|
||
fn process_edge(
|
||
value: JsValue,
|
||
vm: &Vm,
|
||
stack: &mut Vec<*mut JsObject>,
|
||
live_strings: &mut HashSet<*mut JsString, FxBuildHasher>,
|
||
live_bigints: &mut HashSet<*mut num_bigint::BigInt, FxBuildHasher>,
|
||
) {
|
||
if value.is_object() {
|
||
let ptr = value.as_js_object_ptr();
|
||
if vm.is_session_ptr(ptr) {
|
||
stack.push(ptr);
|
||
}
|
||
} else if value.is_string() {
|
||
Self::mark_string_live(live_strings, value.as_string_ptr_mut());
|
||
} else if value.is_bigint() {
|
||
live_bigints.insert(value.as_bigint_ptr() as *mut num_bigint::BigInt);
|
||
}
|
||
}
|
||
|
||
/// 把字符串指针标记为存活并传播 rope 闭包:Cons 迭代传播左右子节点与
|
||
/// 扁平化产物(显式栈防深链爆栈)。perm 指针进表无害——sweep 只遍历
|
||
/// `session_string_ptrs`。遗漏子节点 = 子节点被 sweep 释放 → 悬垂 UB,
|
||
/// 全部字符串 insert 点必须收口此入口。
|
||
fn mark_string_live(live: &mut HashSet<*mut JsString, FxBuildHasher>, root: *mut JsString) {
|
||
if root.is_null() {
|
||
return;
|
||
}
|
||
let mut stack = vec![root];
|
||
while let Some(ptr) = stack.pop() {
|
||
if ptr.is_null() || !live.insert(ptr) {
|
||
continue;
|
||
}
|
||
// SAFETY: ptr 是合法 JsString 指针(session 或 perm),mark 期存活。
|
||
let node = unsafe { &*ptr };
|
||
if node.is_cons() {
|
||
for child in node.cons_children() {
|
||
// 子节点与父同属一个 JsString 堆对象族,const 转 mut 仅用于
|
||
// 集合统一,不引入写操作。
|
||
stack.push(child as *mut JsString);
|
||
}
|
||
let flat = node.flat_cache_ptr();
|
||
if !flat.is_null() {
|
||
stack.push(flat as *mut JsString);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 释放一个 session `JsString`(`Box::into_raw` 分配),连带释放 rope
|
||
/// 扁平化产物。**不递归子节点**——子节点独立登记主表、各自判定存活,
|
||
/// 递归即 double-free。
|
||
///
|
||
/// # Safety
|
||
/// `ptr` 必须是 `new_string`/`new_cons_string` 中 `Box::into_raw` 产生的非空
|
||
/// 指针,仍登记在 session 字符串表(或由 full_reset 统一清表),且恰好
|
||
/// 释放一次。
|
||
pub(crate) unsafe fn drop_session_string_box(ptr: *mut JsString) {
|
||
if ptr.is_null() {
|
||
return;
|
||
}
|
||
// 连带释放 rope 载荷(ConsNode 与其扁平化产物);Flat 串无载荷。
|
||
// SAFETY: cons_node_ptr 由 new_cons 产生,本指针恰好释放一次。
|
||
JsString::drop_cons_node((*ptr).cons_node_ptr());
|
||
drop(Box::from_raw(ptr));
|
||
}
|
||
|
||
/// 把 `obj` 直接持有的 session 字符串值记入 `live`。JsString 不持有 GC 引用,
|
||
/// 因此"到达"一个字符串就等于标记它——不存在字符串 DFS 栈。永久字符串也会被
|
||
/// 无害地记录;sweep 只遍历 `session_string_ptrs`,`live` 中的非 session 指针
|
||
/// 永远不会被查询。
|
||
fn record_object_string_edges(live: &mut HashSet<*mut JsString, FxBuildHasher>, obj: &JsObject) {
|
||
if let Some(elements) = obj.array_elements_vec() {
|
||
for value in elements.iter() {
|
||
if value.is_string() {
|
||
Self::mark_string_live(live, value.as_string_ptr_mut());
|
||
}
|
||
}
|
||
}
|
||
if let Some(props) = obj.hash_props_vec() {
|
||
for value in props.iter() {
|
||
if value.is_string() {
|
||
Self::mark_string_live(live, value.as_string_ptr_mut());
|
||
}
|
||
}
|
||
}
|
||
if obj.captured_this().is_string() {
|
||
Self::mark_string_live(live, obj.captured_this().as_string_ptr_mut());
|
||
}
|
||
if obj.home_object().is_string() {
|
||
Self::mark_string_live(live, obj.home_object().as_string_ptr_mut());
|
||
}
|
||
// 扫描 upvalue cell 中的字符串引用。
|
||
for cell_ptr in obj.upvalues_slice() {
|
||
if cell_ptr.is_null() {
|
||
continue;
|
||
}
|
||
let cell = unsafe { &**cell_ptr };
|
||
if cell.value.is_string() {
|
||
Self::mark_string_live(live, cell.value.as_string_ptr_mut());
|
||
}
|
||
}
|
||
if obj.is_map() {
|
||
for value in map::map_native_edges(obj) {
|
||
if value.is_string() {
|
||
Self::mark_string_live(live, value.as_string_ptr_mut());
|
||
}
|
||
}
|
||
}
|
||
if obj.is_set() {
|
||
for value in set::set_native_edges(obj) {
|
||
if value.is_string() {
|
||
Self::mark_string_live(live, value.as_string_ptr_mut());
|
||
}
|
||
}
|
||
}
|
||
if obj.is_disposable_stack_obj() || obj.is_async_disposable_stack_obj() {
|
||
for value in disposable_stack::dispose_edges(obj) {
|
||
if value.is_string() {
|
||
Self::mark_string_live(live, value.as_string_ptr_mut());
|
||
}
|
||
}
|
||
}
|
||
if obj.is_generator_obj() {
|
||
for ptr in crate::generator::generator_native_string_edges(obj) {
|
||
Self::mark_string_live(live, ptr);
|
||
}
|
||
}
|
||
if obj.is_promise_obj() {
|
||
for value in crate::promise::promise_native_edges(obj) {
|
||
if value.is_string() {
|
||
Self::mark_string_live(live, value.as_string_ptr_mut());
|
||
}
|
||
}
|
||
}
|
||
if obj.is_async_obj() {
|
||
for ptr in crate::async_func::async_native_string_edges(obj) {
|
||
Self::mark_string_live(live, ptr);
|
||
}
|
||
}
|
||
if obj.is_async_generator_obj() {
|
||
for ptr in crate::async_generator::async_generator_native_string_edges(obj) {
|
||
Self::mark_string_live(live, ptr);
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(crate) fn mark(&mut self, vm: &Vm) {
|
||
vm_debug!("[GC] mark phase: {} roots", vm.gc_state.session_object_ptrs.len());
|
||
let mut seeds = Vec::new();
|
||
let mut string_seeds: Vec<*mut JsString> = Vec::new();
|
||
let mut bigint_seeds: Vec<*mut num_bigint::BigInt> = Vec::new();
|
||
vm.for_each_root(|root| {
|
||
if root.is_object() {
|
||
seeds.push(root.as_js_object_ptr());
|
||
} else if root.is_string() {
|
||
string_seeds.push(root.as_string_ptr_mut());
|
||
} else if root.is_bigint() {
|
||
bigint_seeds.push(root.as_bigint_ptr() as *mut num_bigint::BigInt);
|
||
}
|
||
});
|
||
|
||
let Self {
|
||
mark_stack: stack,
|
||
live_strings,
|
||
live_bigints,
|
||
..
|
||
} = self;
|
||
stack.clear();
|
||
live_strings.clear();
|
||
live_bigints.clear();
|
||
for ptr in string_seeds {
|
||
Self::mark_string_live(live_strings, ptr);
|
||
}
|
||
for ptr in bigint_seeds {
|
||
live_bigints.insert(ptr);
|
||
}
|
||
|
||
for ptr in seeds {
|
||
if ptr.is_null() {
|
||
continue;
|
||
}
|
||
if vm.is_session_ptr(ptr) {
|
||
stack.push(ptr);
|
||
continue;
|
||
}
|
||
// SAFETY: 对象根由 VM 自有的字段与 builtin 对象产生。
|
||
unsafe {
|
||
let obj = &*ptr;
|
||
Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints);
|
||
}
|
||
}
|
||
|
||
while let Some(ptr) = stack.pop() {
|
||
if ptr.is_null() {
|
||
continue;
|
||
}
|
||
// SAFETY: ptr 由根/session 边发现,session 根检查保证它是合法 session 对象指针。
|
||
unsafe {
|
||
let obj = &mut *ptr;
|
||
if obj.is_gc_marked() {
|
||
continue;
|
||
}
|
||
obj.set_gc_mark(true);
|
||
Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints);
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(crate) fn drop_object_heap_data(obj_ptr: *mut JsObject, require_session: bool) -> u64 {
|
||
if obj_ptr.is_null() {
|
||
return 0;
|
||
}
|
||
// SAFETY: `obj_ptr` 在调用本辅助函数前已校验,指向 VM session arena 拥有的
|
||
// session 对象。只重建 JsObject::ensure_hash_props/ensure_prop_meta 分配的 Box,
|
||
// 且只在这里释放一次。
|
||
unsafe {
|
||
let obj = &mut *obj_ptr;
|
||
if require_session {
|
||
debug_assert!(obj.is_session_epoch());
|
||
}
|
||
let mut freed_bytes = 0u64;
|
||
|
||
let elems_ptr = obj.array_elements_raw() as *mut Vec<JsValue>;
|
||
if !elems_ptr.is_null() {
|
||
let vec = Box::from_raw(elems_ptr);
|
||
freed_bytes += size_of::<Vec<JsValue>>() as u64 + (vec.capacity() * size_of::<JsValue>()) as u64;
|
||
std::mem::drop(vec);
|
||
}
|
||
|
||
let elems_meta_ptr = obj.array_elements_meta_raw() as *mut Vec<Option<PropMetaEntry>>;
|
||
if !elems_meta_ptr.is_null() {
|
||
let vec = Box::from_raw(elems_meta_ptr);
|
||
freed_bytes += size_of::<Vec<Option<PropMetaEntry>>>() as u64
|
||
+ (vec.capacity() * size_of::<Option<PropMetaEntry>>()) as u64;
|
||
std::mem::drop(vec);
|
||
}
|
||
|
||
let hash_ptr = obj.hash_props_raw() as *mut Vec<JsValue>;
|
||
if !hash_ptr.is_null() {
|
||
let vec = Box::from_raw(hash_ptr);
|
||
freed_bytes += size_of::<Vec<JsValue>>() as u64 + (vec.capacity() * size_of::<JsValue>()) as u64;
|
||
std::mem::drop(vec);
|
||
}
|
||
|
||
let meta_ptr = obj.prop_meta_raw() as *mut Vec<Option<PropMetaEntry>>;
|
||
if !meta_ptr.is_null() {
|
||
let vec = Box::from_raw(meta_ptr);
|
||
freed_bytes += size_of::<Vec<Option<PropMetaEntry>>>() as u64
|
||
+ (vec.capacity() * size_of::<Option<PropMetaEntry>>()) as u64;
|
||
std::mem::drop(vec);
|
||
}
|
||
|
||
freed_bytes += map::drop_map_native(obj);
|
||
freed_bytes += set::drop_set_native(obj);
|
||
freed_bytes += disposable_stack::drop_dispose_native(obj);
|
||
freed_bytes += array_buffer::drop_array_buffer_native(obj);
|
||
freed_bytes += regexp::drop_regexp_native(obj);
|
||
freed_bytes += typed_array::drop_typed_array_native(obj);
|
||
freed_bytes += data_view::drop_data_view_native(obj);
|
||
freed_bytes += crate::generator::drop_generator_native(obj);
|
||
freed_bytes += crate::promise::drop_promise_native(obj);
|
||
freed_bytes += crate::async_func::drop_async_native(obj);
|
||
freed_bytes += crate::async_generator::drop_async_generator_native(obj);
|
||
|
||
freed_bytes
|
||
}
|
||
}
|
||
|
||
fn drop_session_object_heap_data(obj_ptr: *mut JsObject) -> u64 {
|
||
Self::drop_object_heap_data(obj_ptr, true)
|
||
}
|
||
|
||
fn drop_dead_session_object(obj_ptr: *mut JsObject) -> u64 {
|
||
Self::drop_session_object_heap_data(obj_ptr) + size_of::<JsObject>() as u64
|
||
}
|
||
|
||
/// 释放一个已死 session `JsString`(由 `Vm::new_string`/`new_cons_string`
|
||
/// 经 `Box::into_raw` 分配),返回释放的字节数(含 rope 扁平化产物)。
|
||
/// 与 `Vm::free_session_string_heap_data` 的释放一致,但只选择性作用于
|
||
/// 单个已死指针。
|
||
///
|
||
/// # Safety
|
||
/// `ptr` 必须是 `Box::into_raw(Box::new(JsString))` 产生的非空指针,仍存在
|
||
/// 于 `session_string_ptrs`,且恰好释放一次。
|
||
unsafe fn drop_dead_session_string(ptr: *mut JsString) -> u64 {
|
||
let bytes = (size_of::<JsString>() + (*ptr).len()) as u64;
|
||
Self::drop_session_string_box(ptr);
|
||
bytes
|
||
}
|
||
|
||
pub(crate) fn sweep(&mut self, vm: &mut Vm) -> u64 {
|
||
let old_ptrs = std::mem::take(&mut vm.gc_state.session_object_ptrs);
|
||
let mut forwarding = std::mem::take(&mut vm.gc_state.forwarding);
|
||
let new_arena = bumpalo::Bump::new();
|
||
let mut survivors = 0u64;
|
||
let mut dead = 0u64;
|
||
let mut freed_bytes = 0u64;
|
||
|
||
for old_ptr in old_ptrs {
|
||
if old_ptr.is_null() {
|
||
continue;
|
||
}
|
||
// SAFETY: old_ptr 来自 session_arena 的晋升,sweep 运行期间仍指向旧 session arena。
|
||
let is_live = unsafe { (*old_ptr).is_gc_marked() };
|
||
if is_live {
|
||
survivors += 1;
|
||
let old_ref = unsafe { &*old_ptr };
|
||
let clone = old_ref.clone_for_session_epoch();
|
||
let new_ptr = new_arena.alloc(clone) as *mut JsObject;
|
||
let new_ref = unsafe { &mut *new_ptr };
|
||
if old_ref.is_map() {
|
||
map::clone_map_native_with_rewrite(old_ref, new_ref, |value| value);
|
||
} else if old_ref.is_set() {
|
||
set::clone_set_native_with_rewrite(old_ref, new_ref, |value| value);
|
||
} else if old_ref.is_disposable_stack_obj() || old_ref.is_async_disposable_stack_obj() {
|
||
disposable_stack::clone_dispose_native_with_rewrite(old_ref, new_ref, |value| value);
|
||
} else if old_ref.is_array_buffer_obj() {
|
||
array_buffer::clone_array_buffer_native(old_ref, new_ref);
|
||
} else if old_ref.is_typed_array_obj() {
|
||
typed_array::clone_typed_array_native_with_rewrite(old_ref, new_ref, |value| value);
|
||
} else if old_ref.is_data_view_obj() {
|
||
data_view::clone_data_view_native_with_rewrite(old_ref, new_ref, |value| value);
|
||
} else if old_ref.is_promise_obj() {
|
||
crate::promise::clone_promise_native_with_rewrite(old_ref, new_ref, |value| value);
|
||
} else if old_ref.is_async_obj() {
|
||
crate::async_func::clone_async_native_with_rewrite(old_ref, new_ref, |value| value);
|
||
} else if old_ref.is_async_generator_obj() {
|
||
crate::async_generator::clone_async_generator_native_with_rewrite(old_ref, new_ref, |value| value);
|
||
} else if old_ref.is_generator_obj() {
|
||
crate::generator::clone_generator_native_with_rewrite(old_ref, new_ref, |value| value);
|
||
} else if old_ref.is_regexp_obj() {
|
||
// 已编译正则是 Box 深拷贝到新对象:源 Box 由 drop 释放,互不共享。
|
||
regexp::clone_regexp_native(old_ref, new_ref);
|
||
}
|
||
forwarding.insert(old_ptr, new_ptr);
|
||
freed_bytes += Self::drop_session_object_heap_data(old_ptr);
|
||
} else {
|
||
dead += 1;
|
||
freed_bytes += Self::drop_dead_session_object(old_ptr);
|
||
}
|
||
}
|
||
|
||
for &dst in forwarding.values() {
|
||
// SAFETY: forwarding 中的指针全是新分配且已初始化的对象。
|
||
let obj = unsafe { &mut *dst };
|
||
obj.rewrite_object_values(|value| {
|
||
if value.is_object() {
|
||
let ptr = value.as_js_object_ptr();
|
||
if let Some(&fwd) = forwarding.get(&ptr) {
|
||
return JsValue::from_js_object(fwd);
|
||
}
|
||
}
|
||
value
|
||
});
|
||
if obj.is_map() {
|
||
map::rewrite_map_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
|
||
} else if obj.is_set() {
|
||
set::rewrite_set_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
|
||
} else if obj.is_disposable_stack_obj() || obj.is_async_disposable_stack_obj() {
|
||
disposable_stack::rewrite_dispose_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
|
||
} else if obj.is_typed_array_obj() {
|
||
typed_array::rewrite_typed_array_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
|
||
} else if obj.is_data_view_obj() {
|
||
data_view::rewrite_data_view_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
|
||
} else if obj.is_generator_obj() {
|
||
crate::generator::rewrite_generator_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
|
||
} else if obj.is_promise_obj() {
|
||
crate::promise::rewrite_promise_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
|
||
} else if obj.is_async_obj() {
|
||
crate::async_func::rewrite_async_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
|
||
} else if obj.is_async_generator_obj() {
|
||
crate::async_generator::rewrite_async_generator_native(obj, |value| {
|
||
rewrite_forwarded_value(value, &forwarding)
|
||
});
|
||
}
|
||
}
|
||
|
||
rewrite_vm_roots(vm, &forwarding);
|
||
|
||
vm.gc_state.session_object_ptrs = forwarding.values().copied().collect();
|
||
forwarding.clear();
|
||
vm.gc_state.forwarding = forwarding;
|
||
vm.gc_state.session_epoch = new_arena;
|
||
vm.gc_state.session_bytes_allocated = vm.gc_state
|
||
.session_object_ptrs
|
||
.iter()
|
||
.filter(|&&ptr| !ptr.is_null())
|
||
.map(|&ptr| {
|
||
let obj = unsafe { &*ptr };
|
||
size_of::<JsObject>() as u64 + Self::object_heap_data_bytes(obj)
|
||
})
|
||
.sum::<u64>() as usize;
|
||
|
||
self.clear_all_marks(vm);
|
||
|
||
let total_ptrs = survivors + dead;
|
||
if total_ptrs > 0 {
|
||
if dead == 0 {
|
||
vm_debug!("[GC] sweep phase -> no objects collected ({} live, {} dead)", survivors, dead);
|
||
} else {
|
||
vm_debug!(
|
||
"[GC] sweep phase: {} scanned, {} live, {} dead, {} bytes",
|
||
total_ptrs,
|
||
survivors,
|
||
dead,
|
||
freed_bytes
|
||
);
|
||
}
|
||
}
|
||
|
||
self.total_bytes_freed += freed_bytes;
|
||
self.total_objects_scanned += total_ptrs;
|
||
self.total_objects_live += survivors;
|
||
self.total_objects_dead += dead;
|
||
self.last_collection_objects_scanned = total_ptrs;
|
||
self.last_collection_objects_live = survivors;
|
||
self.last_collection_objects_dead = dead;
|
||
self.last_collection_bytes_freed = freed_bytes;
|
||
freed_bytes
|
||
}
|
||
|
||
/// 清扫 session `JsString`:保留 `mark()` 阶段记为存活的部分,其余经
|
||
/// `Box::from_raw` 释放。存活字符串不被搬移——Box 地址稳定——因此无需
|
||
/// forwarding 表与根指针重写。在对象清扫之后运行(对象清扫会把
|
||
/// `session_bytes_allocated` 重置为仅对象),再补回存活字符串字节。
|
||
/// 返回释放的字节数。
|
||
pub(crate) fn sweep_session_strings(&mut self, vm: &mut Vm) -> u64 {
|
||
vm_debug!("[GC] sweep strings: {} string ptrs", vm.gc_state.session_string_ptrs.len());
|
||
let old = std::mem::take(&mut vm.gc_state.session_string_ptrs);
|
||
let mut freed = 0u64;
|
||
let mut live_bytes = 0usize;
|
||
let mut live = Vec::with_capacity(old.len());
|
||
for ptr in old {
|
||
if ptr.is_null() {
|
||
continue;
|
||
}
|
||
if self.live_strings.contains(&ptr) {
|
||
// 存活——地址不变,无需重写。
|
||
// SAFETY: ptr 是仍归 VM 所有的存活 session 字符串 box。
|
||
live_bytes += unsafe { size_of::<JsString>() + (*ptr).len() };
|
||
live.push(ptr);
|
||
} else {
|
||
// SAFETY: ptr 在 session_string_ptrs 中但不可达,恰好释放一次。
|
||
freed += unsafe { Self::drop_dead_session_string(ptr) };
|
||
}
|
||
}
|
||
vm.gc_state.session_string_ptrs = live;
|
||
vm.gc_state.session_bytes_allocated = vm.gc_state.session_bytes_allocated.saturating_add(live_bytes);
|
||
|
||
self.total_bytes_freed = self.total_bytes_freed.saturating_add(freed);
|
||
self.last_collection_bytes_freed = self.last_collection_bytes_freed.saturating_add(freed);
|
||
freed
|
||
}
|
||
|
||
/// 清扫 session `BigInt`:保留 `mark()` 阶段记为存活的部分,其余经
|
||
/// `Box::from_raw` 释放。存活 BigInt 不被搬移——Box 地址稳定——因此无需
|
||
/// forwarding 表与根指针重写。在字符串清扫之后运行。
|
||
/// 返回释放的字节数。
|
||
pub(crate) fn sweep_session_bigints(&mut self, vm: &mut Vm) -> u64 {
|
||
let old = vm.gc_state.session_bigint_ptrs.borrow_mut().drain(..).collect::<Vec<_>>();
|
||
let mut freed = 0u64;
|
||
let mut live_bytes = 0usize;
|
||
let mut live = Vec::with_capacity(old.len());
|
||
for ptr in old {
|
||
if ptr.is_null() {
|
||
continue;
|
||
}
|
||
if self.live_bigints.contains(&ptr) {
|
||
// 存活——地址不变,无需重写。
|
||
live_bytes += size_of::<num_bigint::BigInt>();
|
||
live.push(ptr);
|
||
} else {
|
||
// SAFETY: ptr 在 session_bigint_ptrs 中但不可达,恰好释放一次。
|
||
freed += size_of::<num_bigint::BigInt>() as u64;
|
||
unsafe {
|
||
drop(Box::from_raw(ptr));
|
||
}
|
||
}
|
||
}
|
||
*vm.gc_state.session_bigint_ptrs.borrow_mut() = live;
|
||
vm.gc_state.session_bytes_allocated = vm.gc_state.session_bytes_allocated.saturating_add(live_bytes);
|
||
|
||
self.total_bytes_freed = self.total_bytes_freed.saturating_add(freed);
|
||
self.last_collection_bytes_freed = self.last_collection_bytes_freed.saturating_add(freed);
|
||
freed
|
||
}
|
||
|
||
pub(crate) fn should_collect(&self, vm: &Vm) -> bool {
|
||
(!vm.gc_state.session_object_ptrs.is_empty()
|
||
|| !vm.gc_state.session_string_ptrs.is_empty()
|
||
|| !vm.gc_state.session_bigint_ptrs.borrow().is_empty())
|
||
&& vm.gc_state.session_bytes_allocated >= vm.kernel_core().config().session_gc_threshold
|
||
}
|
||
|
||
/// 执行期完整 GC 的触发判断:账目须超过水位(上次收集后的存活字节 +
|
||
/// 阈值增量)。与 `should_collect`(reset 路径用)不同,本方法走增量水位,
|
||
/// 防止存活对象超阈值时每指令重复触发无死对象可回收的白跑。
|
||
/// 预留给 17.3b(对象侧执行期触发)安全点审计后使用。
|
||
#[allow(dead_code)]
|
||
pub(crate) fn should_collect_gc(&self, vm: &Vm) -> bool {
|
||
(!vm.gc_state.session_object_ptrs.is_empty()
|
||
|| !vm.gc_state.session_string_ptrs.is_empty()
|
||
|| !vm.gc_state.session_bigint_ptrs.borrow().is_empty())
|
||
&& vm.gc_state.session_bytes_allocated >= vm.gc_state.gc_watermark
|
||
}
|
||
|
||
/// 执行期字符串回收的触发判断:账目须超过水位(上次收集后的存活字节 +
|
||
/// 阈值增量)。完整收集(reset)仍用 [`Self::should_collect`] 的阈值直接比较,
|
||
/// 保证死对象超阈值即被 reset 回收;执行期走增量水位,活串超阈值时不每指令
|
||
/// 重复触发无死串可回收的白跑。
|
||
pub(crate) fn should_collect_strings(&self, vm: &Vm) -> bool {
|
||
(!vm.gc_state.session_object_ptrs.is_empty()
|
||
|| !vm.gc_state.session_string_ptrs.is_empty()
|
||
|| !vm.gc_state.session_bigint_ptrs.borrow().is_empty())
|
||
&& vm.gc_state.session_bytes_allocated >= vm.gc_state.string_gc_watermark
|
||
}
|
||
|
||
/// debug 兜底:mark 之后断言所有存活对象持有的字符串边都已登记进
|
||
/// `live_strings`——捕获"对象边漏登记 → 存活串被误释放"的静默悬垂。
|
||
#[cfg(debug_assertions)]
|
||
fn debug_assert_marked_object_strings_live(&self, vm: &Vm) {
|
||
let mut live = HashSet::with_hasher(FxBuildHasher);
|
||
for &ptr in &vm.gc_state.session_object_ptrs {
|
||
if ptr.is_null() {
|
||
continue;
|
||
}
|
||
// SAFETY: session_object_ptrs 归 VM 所有,指针在 arena 存活期有效。
|
||
let obj = unsafe { &*ptr };
|
||
if !obj.is_gc_marked() {
|
||
continue;
|
||
}
|
||
live.clear();
|
||
Self::record_object_string_edges(&mut live, obj);
|
||
for s_ptr in live.drain() {
|
||
assert!(self.live_strings.contains(&s_ptr), "存活对象持有未登记的 session 字符串边");
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(crate) fn collect(&mut self, vm: &mut Vm) {
|
||
let start = Instant::now();
|
||
|
||
vm_info!("[GC] cycle #{} start", self.total_collections + 1);
|
||
|
||
self.mark(vm);
|
||
let mut freed_bytes = self.sweep(vm);
|
||
freed_bytes += self.sweep_session_strings(vm);
|
||
freed_bytes += self.sweep_session_bigints(vm);
|
||
|
||
let elapsed = start.elapsed();
|
||
self.total_collections += 1;
|
||
self.last_collection_duration_us = elapsed.as_micros() as u64;
|
||
self.max_collection_duration_us = self.max_collection_duration_us.max(self.last_collection_duration_us);
|
||
self.min_collection_duration_us = self.min_collection_duration_us.min(self.last_collection_duration_us);
|
||
|
||
vm_info!(
|
||
"[GC] cycle #{} end: {} scanned, {} live, {} dead, {:.1}ms, {} bytes freed",
|
||
self.total_collections,
|
||
self.last_collection_objects_scanned,
|
||
self.last_collection_objects_live,
|
||
self.last_collection_objects_dead,
|
||
elapsed.as_secs_f64() * 1000.0,
|
||
freed_bytes,
|
||
);
|
||
|
||
if freed_bytes > 0 || self.total_collections % 100 == 0 {
|
||
vm_debug!("{}", self.stats_summary());
|
||
}
|
||
}
|
||
|
||
/// 仅回收 session 字符串:完整 mark(对象只置位不搬移)→ 按存活集清扫字符串。
|
||
///
|
||
/// 刻意跳过对象 sweep 搬移——builtin/dispatch 层持有跨分配点的 session 对象
|
||
/// 裸指针,移动式 sweep 会使其悬垂;对象执行期回收仍只在 reset 统一进行。
|
||
/// 字符串每串独立 Box、地址稳定,清扫无需 forwarding 与根重写。
|
||
///
|
||
/// # 副作用
|
||
/// - 清空全部对象 mark 位;按存活集释放死串;累计 `total_collections` 与时长统计;
|
||
/// 抬高 `string_gc_watermark`(存活字节 + 阈值增量)供下次执行期触发。
|
||
///
|
||
/// # 注意事项
|
||
/// - mark 的 DFS 以 `is_gc_marked` 短路,残留 true 会导致后续完整收集漏标,
|
||
/// 本路径不跑对象 sweep(sweep 内清位不会执行),故开头与结尾都清位——
|
||
/// 维持"mark 之后必由清位收尾"的不变量,strings-only 结束不留残留。
|
||
/// - 不搬移对象,`session_bytes_allocated` 保持对象账目,手动扣掉字符串账目后
|
||
/// 由 `sweep_session_strings` 补回存活串字节,与完整收集的最终账目一致。
|
||
pub(crate) fn collect_strings_only(&mut self, vm: &mut Vm) {
|
||
let start = Instant::now();
|
||
|
||
vm_info!("[GC] strings-only cycle #{} start", self.total_collections + 1);
|
||
|
||
// 清残留 mark 位:防止本次字符串 mark 因历史 true 短路而漏标。
|
||
self.clear_all_marks(vm);
|
||
|
||
// 完整 mark:复用现有实现,产出 live_strings;对象仅置位、不搬移。
|
||
self.mark(vm);
|
||
|
||
// debug 兜底:存活对象的字符串边必须已登记,否则串清扫将释放活串。
|
||
// cfg 守卫与定义一致:release 下断言体剥离,调用点同步剥离。
|
||
#[cfg(debug_assertions)]
|
||
self.debug_assert_marked_object_strings_live(vm);
|
||
|
||
// 扣减字符串账目(保留对象账目),再补回存活串字节。
|
||
let object_bytes: usize = vm.gc_state
|
||
.session_object_ptrs
|
||
.iter()
|
||
.filter(|&&ptr| !ptr.is_null())
|
||
.map(|&ptr| {
|
||
let obj = unsafe { &*ptr };
|
||
size_of::<JsObject>() as u64 + Self::object_heap_data_bytes(obj)
|
||
})
|
||
.sum::<u64>() as usize;
|
||
vm.gc_state.session_bytes_allocated = object_bytes;
|
||
let mut freed_bytes = self.sweep_session_strings(vm);
|
||
freed_bytes += self.sweep_session_bigints(vm);
|
||
|
||
// 恢复 mark 位不变量:mark 之后必由清位收尾(与 sweep 末尾同点清位)。
|
||
// 残留 marked 对象会让下一次完整收集(reset 路径)的 mark DFS 短路漏标,
|
||
// 其字符串边不进 live_strings → 存活串被误释放 → 存活对象持悬垂指针。
|
||
self.clear_all_marks(vm);
|
||
|
||
// 抬高下次触发水位:本次存活字节 + 阈值增量,避免活串超阈值时每指令重复
|
||
// 触发无死串可回收的完整 mark + 串清扫。
|
||
let threshold = vm.kernel_core().config().session_gc_threshold;
|
||
vm.gc_state.string_gc_watermark = vm.gc_state.session_bytes_allocated.saturating_add(threshold);
|
||
|
||
let elapsed = start.elapsed();
|
||
self.total_collections += 1;
|
||
self.last_collection_duration_us = elapsed.as_micros() as u64;
|
||
self.max_collection_duration_us = self.max_collection_duration_us.max(self.last_collection_duration_us);
|
||
self.min_collection_duration_us = self.min_collection_duration_us.min(self.last_collection_duration_us);
|
||
|
||
vm_info!(
|
||
"[GC] strings-only cycle #{} end: {} bytes freed, {:.1}ms",
|
||
self.total_collections,
|
||
freed_bytes,
|
||
elapsed.as_secs_f64() * 1000.0,
|
||
);
|
||
}
|
||
|
||
pub(crate) fn maybe_collect_strings_only(&mut self, vm: &mut Vm) {
|
||
if self.should_collect_strings(vm) {
|
||
self.collect_strings_only(vm);
|
||
}
|
||
}
|
||
|
||
pub(crate) fn maybe_collect(&mut self, vm: &mut Vm) {
|
||
if self.should_collect(vm) {
|
||
self.collect(vm);
|
||
}
|
||
}
|
||
|
||
/// 执行期完整 GC 入口:按水位判定,触发后抬高水位。
|
||
/// 预留给 17.3b(对象侧执行期触发)安全点审计后使用。
|
||
#[allow(dead_code)]
|
||
pub(crate) fn maybe_collect_gc(&mut self, vm: &mut Vm) {
|
||
if self.should_collect_gc(vm) {
|
||
self.collect(vm);
|
||
// 抬高下次触发水位:存活字节 + 阈值增量,避免活对象超阈值时每指令重复触发。
|
||
let threshold = vm.gc_state.gc_threshold_cached;
|
||
vm.gc_state.gc_watermark = vm.gc_state.session_bytes_allocated.saturating_add(threshold);
|
||
}
|
||
}
|
||
|
||
pub(crate) fn stats_summary(&self) -> String {
|
||
format!(
|
||
"[GC] collection #{}: {} scanned, {} live, {} dead, {} freed, {}μs",
|
||
self.total_collections,
|
||
self.last_collection_objects_scanned,
|
||
self.last_collection_objects_live,
|
||
self.last_collection_objects_dead,
|
||
self.last_collection_bytes_freed,
|
||
self.last_collection_duration_us
|
||
)
|
||
}
|
||
}
|
||
|
||
impl Default for SessionGc {
|
||
fn default() -> Self {
|
||
Self {
|
||
total_collections: 0,
|
||
total_bytes_freed: 0,
|
||
total_objects_scanned: 0,
|
||
total_objects_live: 0,
|
||
total_objects_dead: 0,
|
||
last_collection_objects_scanned: 0,
|
||
last_collection_objects_live: 0,
|
||
last_collection_objects_dead: 0,
|
||
last_collection_bytes_freed: 0,
|
||
last_collection_duration_us: 0,
|
||
max_collection_duration_us: 0,
|
||
min_collection_duration_us: u64::MAX,
|
||
mark_stack: Vec::new(),
|
||
live_strings: HashSet::default(),
|
||
live_bigints: HashSet::default(),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn rewrite_forwarded_value(
|
||
value: JsValue, forwarding: &HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>,
|
||
) -> JsValue {
|
||
if !value.is_object() {
|
||
return value;
|
||
}
|
||
forwarding
|
||
.get(&value.as_js_object_ptr())
|
||
.map(|&ptr| JsValue::from_js_object(ptr))
|
||
.unwrap_or(value)
|
||
}
|
||
|
||
fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>) {
|
||
vm_debug!("[GC] rewrite_vm_roots: {} forwarded objects", forwarding.len());
|
||
// 统一遍历:与 for_each_value 共用同一字段清单。
|
||
vm.rewrite_values(|value| rewrite_forwarded_value(value, forwarding));
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use oxide_compiler::compiler::Compiler;
|
||
use oxide_kernel::kernel::{KernelConfig, KernelCore};
|
||
use oxide_parser::Allocator;
|
||
use oxide_types::object::JsObject;
|
||
use oxide_types::value::JsValue;
|
||
|
||
use super::*;
|
||
use crate::vm::{CallFrame, FrameContinuation};
|
||
use oxide_builtins::{array_buffer, data_view, map, set, typed_array};
|
||
use oxide_runtime_api::NativeResult;
|
||
|
||
fn plain_object(vm: &mut Vm) -> *mut JsObject {
|
||
let proto_ptr = vm.session.builtin_world().object_proto.as_ptr() as *mut JsObject;
|
||
let ptr = vm.epoch.alloc(JsObject::new_empty(
|
||
oxide_kernel::shape_forge::EMPTY_SHAPE_ID,
|
||
JsValue::from_js_object(proto_ptr),
|
||
));
|
||
// 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。
|
||
unsafe { (*ptr).set_is_epoch(true) };
|
||
ptr
|
||
}
|
||
|
||
fn has_ptr(roots: &[JsValue], ptr: *mut JsObject) -> bool {
|
||
roots
|
||
.iter()
|
||
.any(|value| value.is_object() && std::ptr::eq(value.as_js_object_ptr(), ptr))
|
||
}
|
||
|
||
#[test]
|
||
fn uncaught_value_is_gc_root() {
|
||
let mut vm = Vm::new();
|
||
let obj = plain_object(&mut vm);
|
||
let session = vm.promote_object(obj);
|
||
vm.last_uncaught_value = Some(JsValue::from_js_object(session));
|
||
let mut roots = Vec::new();
|
||
vm.for_each_root(|v| roots.push(v));
|
||
assert!(has_ptr(&roots, session));
|
||
}
|
||
|
||
#[test]
|
||
fn suspended_signal_fields_are_roots() {
|
||
let mut vm = Vm::new();
|
||
let a = plain_object(&mut vm);
|
||
let b = plain_object(&mut vm);
|
||
let c = plain_object(&mut vm);
|
||
let a_s = vm.promote_object(a);
|
||
let b_s = vm.promote_object(b);
|
||
let c_s = vm.promote_object(c);
|
||
vm.generator_suspended = Some(JsValue::from_js_object(a_s));
|
||
vm.async_context = Some(JsValue::from_js_object(b_s));
|
||
vm.async_gen_context = Some(JsValue::from_js_object(c_s));
|
||
let mut roots = Vec::new();
|
||
vm.for_each_root(|v| roots.push(v));
|
||
assert!(has_ptr(&roots, a_s));
|
||
assert!(has_ptr(&roots, b_s));
|
||
assert!(has_ptr(&roots, c_s));
|
||
}
|
||
|
||
#[test]
|
||
fn gc_roots_contains_registers_frames_and_root_roots() {
|
||
let mut vm = Vm::new();
|
||
let root = plain_object(&mut vm);
|
||
let frame_obj = plain_object(&mut vm);
|
||
let saved_this = plain_object(&mut vm);
|
||
let child = plain_object(&mut vm);
|
||
|
||
unsafe {
|
||
(*root).set_prop_at(0, JsValue::from_js_object(child));
|
||
}
|
||
|
||
let root_session = vm.promote_object(root);
|
||
let frame_session = vm.promote_object(frame_obj);
|
||
let this_session = vm.promote_object(saved_this);
|
||
let child_session = vm.promote_object(child);
|
||
vm.regs[0] = JsValue::from_js_object(root_session);
|
||
|
||
vm.frames.push(CallFrame {
|
||
return_addr: 0,
|
||
function_name: 0,
|
||
caller_reg_limit: 1,
|
||
caller_active_reg_limit: 1,
|
||
saved_reg_offset: 0,
|
||
spill_offset: 0,
|
||
arguments_base: 0,
|
||
arguments_count: 0,
|
||
saved_this: JsValue::from_js_object(this_session),
|
||
saved_new_target: JsValue::from_js_object(child_session),
|
||
callee: JsValue::from_js_object(child_session),
|
||
construct_result_reg: None,
|
||
constructed_this: Some(JsValue::from_js_object(child_session)),
|
||
is_derived_constructor: false,
|
||
super_called: false,
|
||
continuation: FrameContinuation::None,
|
||
});
|
||
vm.save_stack.push(JsValue::from_js_object(frame_session));
|
||
|
||
vm.regs[1] = JsValue::from_js_object(child_session);
|
||
vm.exception_value = Some(JsValue::from_js_object(root_session));
|
||
vm.pending_exception = Some(JsValue::from_js_object(child_session));
|
||
vm.iters.for_of_iters.push(crate::vm_state::ForOfEntry {
|
||
iterator: JsValue::from_js_object(child_session),
|
||
last_result: JsValue::from_js_object(root_session),
|
||
is_async: false,
|
||
});
|
||
|
||
let mut roots = Vec::new();
|
||
vm.for_each_root(|v| roots.push(v));
|
||
assert!(has_ptr(&roots, root_session));
|
||
assert!(has_ptr(&roots, frame_session));
|
||
assert!(has_ptr(&roots, this_session));
|
||
assert!(has_ptr(&roots, child_session));
|
||
assert!(has_ptr(&roots, vm.session.global_object().as_ptr() as *mut JsObject));
|
||
assert!(!roots.is_empty());
|
||
assert!(roots.contains(&JsValue::from_js_object(root_session)));
|
||
assert_eq!(vm.exception_value, Some(JsValue::from_js_object(root_session)));
|
||
}
|
||
|
||
#[test]
|
||
fn rewrite_matches_roots_coverage() {
|
||
// 指针重写必须覆盖根收集访问的同一组执行核心字段:session→marker 映射后,
|
||
// 每个持有 session 的字段都应被改写为 marker(与 Step 1 的 for_each 覆盖测试
|
||
// 互补,共同构成孪生清单验证)。
|
||
let mut vm = Vm::new();
|
||
let obj = plain_object(&mut vm);
|
||
let session = vm.promote_object(obj);
|
||
let marker = plain_object(&mut vm);
|
||
let marker_session = vm.promote_object(marker);
|
||
vm.regs[0] = JsValue::from_js_object(session);
|
||
vm.last_uncaught_value = Some(JsValue::from_js_object(session));
|
||
vm.generator_suspended = Some(JsValue::from_js_object(session));
|
||
vm.delegated_iterator = Some(JsValue::from_js_object(session));
|
||
vm.async_context = Some(JsValue::from_js_object(session));
|
||
vm.async_gen_context = Some(JsValue::from_js_object(session));
|
||
vm.inline_callee = Some(JsValue::from_js_object(session));
|
||
vm.pending_completion = Some(crate::vm::Completion::Return {
|
||
value: JsValue::from_js_object(session),
|
||
remaining_finally: 0,
|
||
for_of_count: 0,
|
||
for_in_count: 0,
|
||
});
|
||
|
||
let mut forwarding = std::collections::HashMap::new();
|
||
forwarding.insert(session, marker_session);
|
||
vm.rewrite_values(|v| {
|
||
if v.is_object() {
|
||
if let Some(&m) = forwarding.get(&v.as_js_object_ptr()) {
|
||
return JsValue::from_js_object(m);
|
||
}
|
||
}
|
||
v
|
||
});
|
||
assert_eq!(vm.regs[0].as_js_object_ptr(), marker_session);
|
||
assert_eq!(vm.last_uncaught_value.unwrap().as_js_object_ptr(), marker_session);
|
||
assert_eq!(vm.generator_suspended.unwrap().as_js_object_ptr(), marker_session);
|
||
assert_eq!(vm.delegated_iterator.unwrap().as_js_object_ptr(), marker_session);
|
||
assert_eq!(vm.async_context.unwrap().as_js_object_ptr(), marker_session);
|
||
assert_eq!(vm.async_gen_context.unwrap().as_js_object_ptr(), marker_session);
|
||
assert_eq!(vm.inline_callee.unwrap().as_js_object_ptr(), marker_session);
|
||
match vm.pending_completion.unwrap() {
|
||
crate::vm::Completion::Return { value, .. } => assert_eq!(value.as_js_object_ptr(), marker_session),
|
||
_ => panic!("expected Return completion"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn mark_phase_reaches_cycles_and_unreachable_are_unmarked() {
|
||
let mut vm = Vm::new();
|
||
let root = plain_object(&mut vm);
|
||
let reachable = plain_object(&mut vm);
|
||
let unreachable = plain_object(&mut vm);
|
||
unsafe {
|
||
(*root).set_prop_at(0, JsValue::from_js_object(reachable));
|
||
(*reachable).set_prop_at(0, JsValue::from_js_object(root));
|
||
}
|
||
|
||
let root_session = vm.promote_object(root);
|
||
let reachable_session = unsafe { (*root_session).get_prop_at(0).as_js_object_ptr() };
|
||
let unreachable_session = vm.promote_object(unreachable);
|
||
|
||
vm.regs[0] = JsValue::from_js_object(root_session);
|
||
let mut gc = std::mem::take(&mut vm.gc_state.session_gc);
|
||
gc.mark(&vm);
|
||
vm.gc_state.session_gc = gc;
|
||
|
||
assert!(unsafe { (*root_session).is_gc_marked() });
|
||
assert!(unsafe { (*reachable_session).is_gc_marked() });
|
||
assert!(!unsafe { (*unreachable_session).is_gc_marked() });
|
||
assert_ne!(unreachable_session, root_session);
|
||
}
|
||
|
||
#[test]
|
||
fn sweep_preserves_cycle_and_collects_unreachable() {
|
||
let mut vm = Vm::new();
|
||
let root = plain_object(&mut vm);
|
||
let child = plain_object(&mut vm);
|
||
let dead = plain_object(&mut vm);
|
||
unsafe {
|
||
(*root).set_prop_at(0, JsValue::from_js_object(child));
|
||
(*child).set_prop_at(0, JsValue::from_js_object(root));
|
||
}
|
||
|
||
let root_session = vm.promote_object(root);
|
||
vm.regs[0] = JsValue::from_js_object(root_session);
|
||
let dead_session = vm.promote_object(dead);
|
||
let mut gc = std::mem::take(&mut vm.gc_state.session_gc);
|
||
gc.mark(&vm);
|
||
let _ = gc.sweep(&mut vm);
|
||
vm.gc_state.session_gc = gc;
|
||
|
||
assert_eq!(vm.gc_state.session_object_ptrs.len(), 2);
|
||
assert!(!vm.gc_state.session_object_ptrs.contains(&root_session));
|
||
assert!(!vm.gc_state.session_object_ptrs.contains(&dead_session));
|
||
assert!(!vm
|
||
.gc_state
|
||
.session_object_ptrs
|
||
.iter()
|
||
.any(|ptr| unsafe { (*(*ptr)).is_gc_marked() }));
|
||
}
|
||
|
||
#[test]
|
||
fn sweep_preserves_array_elements_and_collects_dead_element_object() {
|
||
let mut vm = Vm::new();
|
||
let array_proto = vm.session.builtin_world().array_proto.as_ptr() as *mut JsObject;
|
||
let arr = vm.epoch.alloc(JsObject::new_array(
|
||
oxide_kernel::shape_forge::EMPTY_SHAPE_ID,
|
||
JsValue::from_js_object(array_proto),
|
||
2,
|
||
vm.epoch.bump(),
|
||
));
|
||
// 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。
|
||
unsafe { (*arr).set_is_epoch(true) };
|
||
let live_elem = plain_object(&mut vm);
|
||
let dead_elem = plain_object(&mut vm);
|
||
unsafe {
|
||
(*arr).set_prop_at(0, JsValue::from_js_object(live_elem));
|
||
(*arr).set_prop_at(1, JsValue::from_js_object(dead_elem));
|
||
}
|
||
// 晋升数组会把元素对象一并带入 session;随后断开元素 1 的引用,使其成为
|
||
// mark 不可达的死对象,供 sweep 回收。
|
||
let arr_session = vm.promote_object(arr);
|
||
unsafe {
|
||
(*arr_session).set_prop_at(1, JsValue::undefined());
|
||
}
|
||
vm.regs[0] = JsValue::from_js_object(arr_session);
|
||
|
||
let mut gc = std::mem::take(&mut vm.gc_state.session_gc);
|
||
gc.mark(&vm);
|
||
let _ = gc.sweep(&mut vm);
|
||
vm.gc_state.session_gc = gc;
|
||
|
||
// sweep 复制存活对象并把 VM 根改写为新 arena 指针:regs[0] 是数组的新地址,
|
||
// 旧指针已随旧 arena 释放,不可再用。
|
||
let arr_new = vm.regs[0].as_js_object_ptr();
|
||
let live_session = unsafe { (*arr_new).get_prop_at(0).as_js_object_ptr() };
|
||
assert_eq!(unsafe { (*live_session).prop_count() }, 0);
|
||
assert_eq!(unsafe { (*arr_new).prop_count() }, 2);
|
||
assert_eq!(unsafe { (*arr_new).get_prop_at(0).as_js_object_ptr() }, live_session);
|
||
// 元素 1 引用已断开:死元素对象被回收,存活集合只剩数组与元素 0 的对象。
|
||
assert_eq!(unsafe { (*arr_new).get_prop_at(1) }, JsValue::undefined());
|
||
assert_eq!(vm.gc_state.session_object_ptrs.len(), 2);
|
||
}
|
||
|
||
#[test]
|
||
fn forwarding_is_cleared_after_sweep() {
|
||
let mut vm = Vm::new();
|
||
let root = plain_object(&mut vm);
|
||
let child = plain_object(&mut vm);
|
||
unsafe {
|
||
(*root).set_prop_at(0, JsValue::from_js_object(child));
|
||
}
|
||
let root_session = vm.promote_object(root);
|
||
vm.regs[0] = JsValue::from_js_object(root_session);
|
||
let _ = vm.promote_object(child);
|
||
let mut gc = std::mem::take(&mut vm.gc_state.session_gc);
|
||
gc.mark(&vm);
|
||
let _ = gc.sweep(&mut vm);
|
||
vm.gc_state.session_gc = gc;
|
||
|
||
// 复用的 forwarding 表必须在 sweep 后清空,否则 promote 会观察到指向已释放
|
||
// arena 的过期 old->new 条目。
|
||
assert!(vm.gc_state.forwarding.is_empty());
|
||
}
|
||
|
||
fn vm_with_low_threshold() -> Vm {
|
||
let mut cfg = KernelConfig::minimal();
|
||
cfg.set_session_gc_threshold(1);
|
||
let core = KernelCore::new(cfg);
|
||
Vm::with_kernel_core(core)
|
||
}
|
||
|
||
fn native_ok(result: NativeResult) -> JsValue {
|
||
match result {
|
||
NativeResult::Ok(value) => value,
|
||
NativeResult::Err(err) => panic!("native error: {err}"),
|
||
NativeResult::TailCall { .. } => panic!("unexpected native bytecode call"),
|
||
}
|
||
}
|
||
|
||
/// 分配一个原型指向 Map.prototype 的占位对象并写入寄存器,作为构造器调用的 `this`。
|
||
fn map_this(vm: &mut Vm, reg: u8) -> JsValue {
|
||
let proto = vm.session.builtin_world().map_proto.as_ptr() as *mut JsObject;
|
||
let obj = vm.epoch.alloc(JsObject::new_empty(
|
||
oxide_kernel::shape_forge::EMPTY_SHAPE_ID,
|
||
JsValue::from_js_object(proto),
|
||
));
|
||
// 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。
|
||
unsafe { (*obj).set_is_epoch(true) };
|
||
let val = JsValue::from_js_object(obj);
|
||
vm.regs[reg as usize] = val;
|
||
val
|
||
}
|
||
|
||
/// 分配一个原型指向 Set.prototype 的占位对象并写入寄存器,作为构造器调用的 `this`。
|
||
fn set_this(vm: &mut Vm, reg: u8) -> JsValue {
|
||
let proto = vm.session.builtin_world().set_proto.as_ptr() as *mut JsObject;
|
||
let obj = vm.epoch.alloc(JsObject::new_empty(
|
||
oxide_kernel::shape_forge::EMPTY_SHAPE_ID,
|
||
JsValue::from_js_object(proto),
|
||
));
|
||
// 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。
|
||
unsafe { (*obj).set_is_epoch(true) };
|
||
let val = JsValue::from_js_object(obj);
|
||
vm.regs[reg as usize] = val;
|
||
val
|
||
}
|
||
|
||
#[test]
|
||
fn reset_maybe_collect_collects_after_threshold() {
|
||
let mut vm = vm_with_low_threshold();
|
||
let obj = plain_object(&mut vm);
|
||
vm.promote_object(obj);
|
||
assert!(!vm.gc_state.session_object_ptrs.is_empty());
|
||
let tracked_before = vm.gc_state.session_object_ptrs.len();
|
||
|
||
vm.regs[0] = JsValue::undefined();
|
||
vm.regs[1] = JsValue::undefined();
|
||
vm.reset();
|
||
|
||
assert!(vm.gc_state.session_object_ptrs.len() <= tracked_before);
|
||
assert_eq!(vm.gc_state.session_object_ptrs.len(), 0);
|
||
assert_eq!(vm.gc_state.session_bytes_allocated, 0);
|
||
}
|
||
|
||
#[test]
|
||
fn gc_stats_summary_includes_collection() {
|
||
let mut vm = vm_with_low_threshold();
|
||
let obj = plain_object(&mut vm);
|
||
vm.promote_object(obj);
|
||
vm.regs[0] = JsValue::undefined();
|
||
vm.maybe_collect_session_gc();
|
||
let summary = vm.gc_state.session_gc.stats_summary();
|
||
assert!(summary.contains("[GC] collection"));
|
||
}
|
||
|
||
#[test]
|
||
fn moving_sweep_rewrites_global_root_edges() {
|
||
let mut vm = vm_with_low_threshold();
|
||
let obj = plain_object(&mut vm);
|
||
unsafe {
|
||
(*obj).set_prop_at(0, JsValue::int(42));
|
||
}
|
||
let old_ptr = vm.promote_object(obj);
|
||
let key = vm.kernel_core.perm_interner().intern("gcRoot").0;
|
||
let global_ptr = vm.session.global_object().as_ptr() as *mut JsObject;
|
||
unsafe {
|
||
let global = &mut *global_ptr;
|
||
vm.set_or_create_prop_value(global, key, JsValue::from_js_object(old_ptr));
|
||
}
|
||
|
||
vm.maybe_collect_session_gc();
|
||
|
||
let global = vm.session.global_object();
|
||
let pos = vm
|
||
.kernel_core
|
||
.shape_forge()
|
||
.lookup_position(global.shape_id(), key)
|
||
.expect("global slot");
|
||
let new_value = global.get_prop_at(pos);
|
||
assert!(new_value.is_object());
|
||
assert!(!std::ptr::eq(new_value.as_js_object_ptr(), old_ptr));
|
||
assert_eq!(unsafe { (*new_value.as_js_object_ptr()).get_prop_at(0) }, JsValue::int(42));
|
||
}
|
||
|
||
#[test]
|
||
fn map_native_storage_is_not_a_normal_object_edge() {
|
||
let mut vm = Vm::new();
|
||
map_this(&mut vm, 1);
|
||
let map_value = native_ok(map::map_constructor(&mut vm, &[1]));
|
||
let map_obj = unsafe { &*map_value.as_js_object_ptr() };
|
||
let native_ptr = map_obj.native_data() as *mut JsObject;
|
||
|
||
assert!(map_obj.hash_props_vec().is_none());
|
||
assert!(!map_obj.native_data().is_null());
|
||
// Verify native storage pointer is not traced as a normal object edge.
|
||
let mut stack = Vec::new();
|
||
let mut live = HashSet::with_hasher(FxBuildHasher);
|
||
let mut live_bigints = HashSet::with_hasher(FxBuildHasher);
|
||
SessionGc::scan_edges_for_mark(map_obj, &vm, &mut stack, &mut live, &mut live_bigints);
|
||
assert!(!stack.iter().any(|&ptr| std::ptr::eq(ptr, native_ptr)));
|
||
}
|
||
|
||
#[test]
|
||
fn session_gc_traces_map_object_key_and_value() {
|
||
let mut vm = vm_with_low_threshold();
|
||
map_this(&mut vm, 3);
|
||
let map_value = native_ok(map::map_constructor(&mut vm, &[3]));
|
||
let key = JsValue::from_js_object(plain_object(&mut vm));
|
||
let value = JsValue::from_js_object(plain_object(&mut vm));
|
||
vm.regs[0] = map_value;
|
||
vm.regs[1] = key;
|
||
vm.regs[2] = value;
|
||
native_ok(map::map_set(&mut vm, &[0, 1, 2]));
|
||
|
||
let map_session = vm.promote_object(map_value.as_js_object_ptr());
|
||
vm.regs.fill(JsValue::undefined());
|
||
vm.regs[0] = JsValue::from_js_object(map_session);
|
||
let mut gc = std::mem::take(&mut vm.gc_state.session_gc);
|
||
gc.collect(&mut vm);
|
||
vm.gc_state.session_gc = gc;
|
||
|
||
let live_map = unsafe { &*vm.regs[0].as_js_object_ptr() };
|
||
let edges = map::map_native_edges(live_map);
|
||
assert_eq!(edges.len(), 2);
|
||
assert!(edges.iter().all(|value| vm.is_session_ptr(value.as_js_object_ptr())));
|
||
assert_eq!(vm.gc_state.session_object_ptrs.len(), 3);
|
||
}
|
||
|
||
#[test]
|
||
fn session_gc_traces_set_object_key() {
|
||
let mut vm = vm_with_low_threshold();
|
||
set_this(&mut vm, 2);
|
||
let set_value = native_ok(set::set_constructor(&mut vm, &[2]));
|
||
let key = JsValue::from_js_object(plain_object(&mut vm));
|
||
vm.regs[0] = set_value;
|
||
vm.regs[1] = key;
|
||
native_ok(set::set_add(&mut vm, &[0, 1]));
|
||
|
||
let set_session = vm.promote_object(set_value.as_js_object_ptr());
|
||
vm.regs.fill(JsValue::undefined());
|
||
vm.regs[0] = JsValue::from_js_object(set_session);
|
||
let mut gc = std::mem::take(&mut vm.gc_state.session_gc);
|
||
gc.collect(&mut vm);
|
||
vm.gc_state.session_gc = gc;
|
||
|
||
let live_set = unsafe { &*vm.regs[0].as_js_object_ptr() };
|
||
let edges = set::set_native_edges(live_set);
|
||
assert_eq!(edges.len(), 1);
|
||
assert!(vm.is_session_ptr(edges[0].as_js_object_ptr()));
|
||
assert_eq!(vm.gc_state.session_object_ptrs.len(), 2);
|
||
}
|
||
|
||
#[test]
|
||
fn session_gc_keeps_shared_array_buffer_alive_through_view_native_edges() {
|
||
let mut vm = vm_with_low_threshold();
|
||
let root = plain_object(&mut vm);
|
||
|
||
vm.regs[1] = JsValue::int(8);
|
||
let buffer = native_ok(array_buffer::array_buffer_constructor(&mut vm, &[0, 1]));
|
||
|
||
vm.regs[1] = buffer;
|
||
let typed = native_ok(typed_array::int32array_constructor(&mut vm, &[0, 1]));
|
||
|
||
vm.regs[1] = buffer;
|
||
let view = native_ok(data_view::data_view_constructor(&mut vm, &[0, 1]));
|
||
|
||
vm.regs[0] = view;
|
||
vm.regs[1] = JsValue::int(0);
|
||
vm.regs[2] = JsValue::int(42);
|
||
vm.regs[3] = JsValue::bool(true);
|
||
native_ok(data_view::data_view_set_int32(&mut vm, &[0, 1, 2, 3]));
|
||
|
||
unsafe {
|
||
(*root).set_prop_at(0, typed);
|
||
(*root).set_prop_at(1, view);
|
||
}
|
||
|
||
let root_session = vm.promote_object(root);
|
||
vm.regs.fill(JsValue::undefined());
|
||
vm.regs[0] = JsValue::from_js_object(root_session);
|
||
let mut gc = std::mem::take(&mut vm.gc_state.session_gc);
|
||
gc.collect(&mut vm);
|
||
vm.gc_state.session_gc = gc;
|
||
|
||
let live_root = unsafe { &*vm.regs[0].as_js_object_ptr() };
|
||
let live_typed = live_root.get_prop_at(0);
|
||
let live_view = live_root.get_prop_at(1);
|
||
let typed_obj = unsafe { &*live_typed.as_js_object_ptr() };
|
||
let view_obj = unsafe { &*live_view.as_js_object_ptr() };
|
||
let typed_edges = typed_array::typed_array_native_edges(typed_obj);
|
||
let view_edges = data_view::data_view_native_edges(view_obj);
|
||
|
||
assert_eq!(typed_edges.len(), 1);
|
||
assert_eq!(view_edges.len(), 1);
|
||
assert!(vm.is_session_ptr(typed_edges[0].as_js_object_ptr()));
|
||
assert!(vm.is_session_ptr(view_edges[0].as_js_object_ptr()));
|
||
assert!(std::ptr::eq(typed_edges[0].as_js_object_ptr(), view_edges[0].as_js_object_ptr()));
|
||
|
||
vm.regs[0] = live_typed;
|
||
vm.regs[1] = JsValue::int(0);
|
||
assert_eq!(native_ok(typed_array::typed_array_at(&mut vm, &[0, 1])), JsValue::int(42));
|
||
|
||
vm.regs[0] = live_view;
|
||
vm.regs[1] = JsValue::int(4);
|
||
vm.regs[2] = JsValue::int(7);
|
||
vm.regs[3] = JsValue::bool(true);
|
||
native_ok(data_view::data_view_set_int32(&mut vm, &[0, 1, 2, 3]));
|
||
|
||
vm.regs[0] = live_typed;
|
||
vm.regs[1] = JsValue::int(1);
|
||
assert_eq!(native_ok(typed_array::typed_array_at(&mut vm, &[0, 1])), JsValue::int(7));
|
||
}
|
||
|
||
#[test]
|
||
fn session_gc_rewrites_buffer_retained_only_by_data_view_native_edge() {
|
||
let mut vm = vm_with_low_threshold();
|
||
vm.regs[1] = JsValue::int(8);
|
||
let buffer = native_ok(array_buffer::array_buffer_constructor(&mut vm, &[0, 1]));
|
||
vm.regs[1] = buffer;
|
||
let view = native_ok(data_view::data_view_constructor(&mut vm, &[0, 1]));
|
||
|
||
vm.regs[0] = view;
|
||
vm.regs[1] = JsValue::int(0);
|
||
vm.regs[2] = JsValue::int(9);
|
||
native_ok(data_view::data_view_set_int32(&mut vm, &[0, 1, 2]));
|
||
|
||
let view_session = vm.promote_object(view.as_js_object_ptr());
|
||
vm.regs.fill(JsValue::undefined());
|
||
vm.regs[0] = JsValue::from_js_object(view_session);
|
||
let mut gc = std::mem::take(&mut vm.gc_state.session_gc);
|
||
gc.collect(&mut vm);
|
||
vm.gc_state.session_gc = gc;
|
||
|
||
let live_view = vm.regs[0];
|
||
let live_view_obj = unsafe { &*live_view.as_js_object_ptr() };
|
||
let edges = data_view::data_view_native_edges(live_view_obj);
|
||
assert_eq!(edges.len(), 1);
|
||
assert!(vm.is_session_ptr(edges[0].as_js_object_ptr()));
|
||
assert_eq!(vm.gc_state.session_object_ptrs.len(), 2);
|
||
|
||
vm.regs[0] = live_view;
|
||
vm.regs[1] = JsValue::int(0);
|
||
assert_eq!(native_ok(data_view::data_view_get_int32(&mut vm, &[0, 1])), JsValue::int(9));
|
||
}
|
||
|
||
fn collect(vm: &mut Vm) {
|
||
let mut gc = std::mem::take(&mut vm.gc_state.session_gc);
|
||
gc.collect(vm);
|
||
vm.gc_state.session_gc = gc;
|
||
}
|
||
|
||
#[test]
|
||
fn session_string_collected_when_dead() {
|
||
let mut vm = Vm::new();
|
||
let dead = vm.new_string("dead-string");
|
||
let dead_ptr = dead.as_string_ptr_mut();
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&dead_ptr));
|
||
|
||
// 没有根引用 `dead`(它只作为值包装存在于 Rust 栈上)。
|
||
collect(&mut vm);
|
||
|
||
assert!(!vm.gc_state.session_string_ptrs.contains(&dead_ptr));
|
||
}
|
||
|
||
#[test]
|
||
fn session_string_survives_when_in_register() {
|
||
let mut vm = Vm::new();
|
||
let live = vm.new_string("live-in-reg");
|
||
let live_ptr = live.as_string_ptr_mut();
|
||
vm.regs[0] = live;
|
||
|
||
collect(&mut vm);
|
||
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&live_ptr));
|
||
// 存活字符串永不搬移——寄存器仍指向同一 box。
|
||
assert_eq!(vm.regs[0].as_string_ptr_mut(), live_ptr);
|
||
assert_eq!(unsafe { (*live_ptr).as_str() }, "live-in-reg");
|
||
}
|
||
|
||
#[test]
|
||
fn session_string_survives_via_object_property() {
|
||
let mut vm = Vm::new();
|
||
let obj = plain_object(&mut vm);
|
||
let s = vm.new_string("prop-string");
|
||
let s_ptr = s.as_string_ptr_mut();
|
||
unsafe {
|
||
(*obj).set_prop_at(0, s);
|
||
}
|
||
let obj_session = vm.promote_object(obj);
|
||
|
||
// 该字符串仅通过存活对象的属性可达,不经过任何寄存器。
|
||
vm.regs.fill(JsValue::undefined());
|
||
vm.regs[0] = JsValue::from_js_object(obj_session);
|
||
|
||
collect(&mut vm);
|
||
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&s_ptr));
|
||
assert_eq!(unsafe { (*s_ptr).as_str() }, "prop-string");
|
||
}
|
||
|
||
#[test]
|
||
fn permanent_string_untouched_by_sweep() {
|
||
let mut vm = Vm::new();
|
||
let perm = vm.perm_string("perm");
|
||
let perm_ptr = perm.as_string_ptr_mut();
|
||
// 永久字符串位于 PermInterner,绝不在 session 集合中。
|
||
assert!(!vm.gc_state.session_string_ptrs.contains(&perm_ptr));
|
||
vm.regs[0] = perm;
|
||
|
||
collect(&mut vm);
|
||
|
||
// 绝不被 session 清扫释放(仍可读,仍不在 session 集合中)。
|
||
assert!(!vm.gc_state.session_string_ptrs.contains(&perm_ptr));
|
||
assert_eq!(unsafe { (*perm_ptr).as_str() }, "perm");
|
||
}
|
||
|
||
#[test]
|
||
fn string_sweep_byte_accounting() {
|
||
let mut vm = Vm::new();
|
||
let dead = vm.new_string("0123456789");
|
||
let _ = dead.as_string_ptr_mut();
|
||
let expected = (size_of::<JsString>() + "0123456789".len()) as u64;
|
||
|
||
let mut gc = std::mem::take(&mut vm.gc_state.session_gc);
|
||
let before = gc.total_bytes_freed;
|
||
gc.collect(&mut vm);
|
||
let after = gc.total_bytes_freed;
|
||
vm.gc_state.session_gc = gc;
|
||
|
||
assert!(after >= before + expected);
|
||
}
|
||
|
||
fn vm_with_threshold(bytes: usize) -> Vm {
|
||
let mut cfg = KernelConfig::minimal();
|
||
cfg.set_session_gc_threshold(bytes);
|
||
let core = KernelCore::new(cfg);
|
||
Vm::with_kernel_core(core)
|
||
}
|
||
|
||
// ── upvalue cell 独立堆分配:跨对象 sweep 存活 ─────────────────────────
|
||
|
||
fn compile(source: &str) -> oxide_bytecode::module::CompiledModule {
|
||
let allocator = Allocator::default();
|
||
let program = oxide_parser::parse(&allocator, source).expect("parse");
|
||
Compiler::new().compile(&program).expect("compile")
|
||
}
|
||
|
||
fn global_prop_opt(vm: &Vm, name: &str) -> Option<JsValue> {
|
||
let global = vm.session.global_object();
|
||
let si = vm.kernel_core().perm_interner().intern(name).0;
|
||
vm.resolve_property(global, si)
|
||
}
|
||
|
||
/// 闭包捕获变量跨对象 sweep 存活:reset 触发完整收集(session_epoch 替换),
|
||
/// 修复前 cell 分配于旧 arena、随 Bump drop 悬垂;修复后独立堆分配、
|
||
/// 地址稳定,sweep 只重写 cell.value 中的对象引用,值跨搬移保留。
|
||
#[test]
|
||
fn closure_cell_survives_object_sweep() {
|
||
let mut vm = vm_with_threshold(1);
|
||
vm.run(&compile("var counter = 0; function inc() { return ++counter; } globalThis.inc = inc; 0"))
|
||
.expect("run1");
|
||
assert!(!vm.gc_state.session_cell_ptrs.borrow().is_empty(), "run1 应分配 upvalue cell");
|
||
|
||
// reset 触发对象 sweep:存活对象搬到新 arena,旧 arena 释放。
|
||
vm.reset();
|
||
assert!(vm.session_gc_stats().total_collections > 0, "reset 应触发对象收集");
|
||
|
||
// 跨搬移后从 global 重新取 inc 函数对象:upvalue cell 指针稳定、值保留。
|
||
let inc_val = global_prop_opt(&vm, "inc").expect("inc 应挂在 global 上");
|
||
let obj = unsafe { &*inc_val.as_js_object_ptr() };
|
||
let cells = obj.upvalues_slice();
|
||
assert_eq!(cells.len(), 1, "inc 应捕获 counter 一个 cell");
|
||
let cell = unsafe { &*cells[0] };
|
||
assert_eq!(cell.value, JsValue::int(0), "sweep 后 cell 值应保留为 counter 初值");
|
||
assert!(cell.is_initialized(), "sweep 后 cell 初始化位应保留");
|
||
}
|
||
|
||
/// 私有字段类的 brand cell 跨对象 sweep 存活:`@@class_brand` upvalue cell
|
||
/// 存类 brand 对象,sweep 后其值经 forwarding 重写为搬移后的新对象地址,
|
||
/// cell 指针仍稳定(修复前 cell 悬垂、值被复用覆盖)。
|
||
#[test]
|
||
fn private_brand_cell_survives_object_sweep() {
|
||
let mut vm = vm_with_threshold(1);
|
||
vm.run(&compile(
|
||
"class C { #x = 0; set(v){ this.#x = v; } get(){ return this.#x; } } \
|
||
globalThis.c = new C(); globalThis.c.set(4); 0",
|
||
))
|
||
.expect("run1");
|
||
|
||
vm.reset();
|
||
assert!(vm.session_gc_stats().total_collections > 0, "reset 应触发对象收集");
|
||
|
||
// 实例 c 的方法 get(类原型上)捕获 @@class_brand cell:值须仍为对象。
|
||
let c_val = global_prop_opt(&vm, "c").expect("c 应挂在 global 上");
|
||
let c_obj = unsafe { &*c_val.as_js_object_ptr() };
|
||
let proto_ptr = c_obj.proto().as_js_object_ptr();
|
||
let get_val = vm
|
||
.resolve_property(unsafe { &*proto_ptr }, vm.kernel_core().perm_interner().intern("get").0)
|
||
.expect("类原型应有 get 方法");
|
||
let get_obj = unsafe { &*get_val.as_js_object_ptr() };
|
||
let cells = get_obj.upvalues_slice();
|
||
assert!(!cells.is_empty(), "get 应捕获 @@class_brand cell");
|
||
let brand = unsafe { &*cells[0] }.value;
|
||
assert!(brand.is_object(), "sweep 后 brand cell 值应仍为 brand 对象");
|
||
}
|
||
|
||
/// full_reset 统一释放全部 cell box 且可重新分配:追踪表清空(无 double-free),
|
||
/// 重置后新闭包正常建立新 cell。
|
||
#[test]
|
||
fn cells_freed_by_full_reset_and_reallocatable() {
|
||
let mut vm = Vm::new();
|
||
vm.run(&compile("var x = 1; function f() { return x; } globalThis.f = f; f()"))
|
||
.expect("run1");
|
||
assert!(!vm.gc_state.session_cell_ptrs.borrow().is_empty());
|
||
|
||
vm.full_reset();
|
||
assert!(vm.gc_state.session_cell_ptrs.borrow().is_empty(), "full_reset 应释放全部 cell");
|
||
|
||
let result = vm.run(&compile("var y = 2; function g() { return y; } g()")).expect("run2");
|
||
assert_eq!(format!("{result}"), "2", "重置后新 cell 应正常分配与读取");
|
||
}
|
||
|
||
#[test]
|
||
fn allocation_does_not_trigger_gc_before_safe_point() {
|
||
let mut vm = vm_with_threshold(1);
|
||
// 分配点不触发回收:触发已移到 dispatch 指令边界,局部持有跨分配点安全。
|
||
let seed = vm.new_string_owned("seed".repeat(16));
|
||
let seed_ptr = seed.as_string_ptr_mut();
|
||
let returned = vm.new_string_owned("returned".repeat(16));
|
||
|
||
assert_eq!(vm.gc_state.session_gc.total_collections, 0);
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&seed_ptr));
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&returned.as_string_ptr_mut()));
|
||
|
||
// 显式触发后:无根引用的 seed 被回收,入根的 returned 存活(安全点语义)。
|
||
vm.regs[0] = returned;
|
||
vm.maybe_collect_session_strings();
|
||
assert!(vm.gc_state.session_gc.total_collections >= 1);
|
||
assert!(!vm.gc_state.session_string_ptrs.contains(&seed_ptr));
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&returned.as_string_ptr_mut()));
|
||
}
|
||
|
||
#[test]
|
||
fn strings_only_collection_preserves_all_root_kinds() {
|
||
// 阈值 1:每次 new_string_owned 分配前自动触发回收,逐步验证各类根的保护。
|
||
let mut vm = vm_with_threshold(1);
|
||
// 寄存器根:直接持有 session 串。
|
||
let reg_str = vm.new_string_owned("reg-root".repeat(16));
|
||
vm.regs[0] = reg_str;
|
||
// 存活对象属性根:session 对象经 promote 后持有 session 串(分配即触发回收,
|
||
// reg_str 仍在寄存器,prop_str 挂到对象后才被下次回收看到)。
|
||
let obj = plain_object(&mut vm);
|
||
let prop_str = vm.new_string_owned("prop-root".repeat(16));
|
||
unsafe {
|
||
(*obj).set_prop_at(0, prop_str);
|
||
}
|
||
let obj_session = vm.promote_object(obj);
|
||
vm.regs[1] = JsValue::from_js_object(obj_session);
|
||
// 非 session 根对象属性:epoch 根对象(在寄存器)持有 session 串,mark 走
|
||
// 非 session 根扫描路径保护。
|
||
let epoch_obj = plain_object(&mut vm);
|
||
let epoch_str = vm.new_string_owned("epoch-root".repeat(16));
|
||
unsafe {
|
||
(*epoch_obj).set_prop_at(0, epoch_str);
|
||
}
|
||
vm.regs[2] = JsValue::from_js_object(epoch_obj);
|
||
// 死串:无任何根引用,最后手动触发一轮回收它。
|
||
let dead = vm.new_string_owned("dead".repeat(16));
|
||
let dead_ptr = dead.as_string_ptr_mut();
|
||
|
||
vm.maybe_collect_session_strings();
|
||
|
||
assert!(vm.gc_state.session_string_ptrs.contains(®_str.as_string_ptr_mut()));
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&prop_str.as_string_ptr_mut()));
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&epoch_str.as_string_ptr_mut()));
|
||
assert!(!vm.gc_state.session_string_ptrs.contains(&dead_ptr));
|
||
// 存活串内容可读,地址稳定。
|
||
assert_eq!(unsafe { (*reg_str.as_string_ptr_mut()).as_str() }, "reg-root".repeat(16));
|
||
assert_eq!(unsafe { (*prop_str.as_string_ptr_mut()).as_str() }, "prop-root".repeat(16));
|
||
assert_eq!(unsafe { (*epoch_str.as_string_ptr_mut()).as_str() }, "epoch-root".repeat(16));
|
||
}
|
||
|
||
#[test]
|
||
fn strings_only_collection_does_not_move_objects() {
|
||
let mut vm = vm_with_threshold(1);
|
||
let obj = plain_object(&mut vm);
|
||
let obj_session = vm.promote_object(obj);
|
||
vm.regs[0] = JsValue::from_js_object(obj_session);
|
||
let before: Vec<_> = vm.gc_state.session_object_ptrs.clone();
|
||
|
||
let s = vm.new_string_owned("x".repeat(64));
|
||
vm.regs[1] = s;
|
||
// 分配不自动触发(安全点语义),显式触发一轮验证对象不搬移。
|
||
vm.maybe_collect_session_strings();
|
||
|
||
// 对象指针逐一相同:不搬移、不重写根。
|
||
assert_eq!(vm.gc_state.session_object_ptrs, before);
|
||
assert_eq!(vm.regs[0].as_js_object_ptr(), obj_session);
|
||
assert_eq!(vm.session_object_count(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn multiple_strings_only_cycles_keep_objects_alive() {
|
||
let mut vm = vm_with_threshold(1);
|
||
let obj = plain_object(&mut vm);
|
||
let obj_session = vm.promote_object(obj);
|
||
let live = vm.new_string_owned("keep".repeat(32));
|
||
unsafe {
|
||
(*obj_session).set_prop_at(0, live);
|
||
}
|
||
vm.regs[0] = JsValue::from_js_object(obj_session);
|
||
|
||
// 连续多轮触发字符串回收:对象 mark 残留位被显式清理,对象与挂载串持续存活。
|
||
for _ in 0..5 {
|
||
for _ in 0..8 {
|
||
let _ = vm.new_string_owned("dead".repeat(64));
|
||
}
|
||
// 分配不自动触发,每轮显式触发一次回收。
|
||
vm.maybe_collect_session_strings();
|
||
assert!(vm.gc_state.session_object_ptrs.contains(&obj_session));
|
||
assert_eq!(unsafe { (*obj_session).get_prop_at(0) }, live);
|
||
}
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&live.as_string_ptr_mut()));
|
||
}
|
||
|
||
#[test]
|
||
fn strings_only_then_full_collect_keeps_object_strings_live() {
|
||
// strings-only 收集后接完整收集(reset 路径):strings-only 残留的 mark 位
|
||
// 不得使完整收集的 mark DFS 短路漏标,存活对象属性中的串须跨收集存活。
|
||
let mut vm = vm_with_threshold(1);
|
||
let obj = plain_object(&mut vm);
|
||
let s = vm.new_string_owned("kept".repeat(8));
|
||
let s_ptr = s.as_string_ptr_mut();
|
||
unsafe {
|
||
(*obj).set_prop_at(0, s);
|
||
}
|
||
let obj_session = vm.promote_object(obj);
|
||
vm.regs[0] = JsValue::from_js_object(obj_session);
|
||
|
||
// 第一轮 strings-only:对象被 mark 置位,修复前该位残留至完整收集。
|
||
vm.maybe_collect_session_strings();
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&s_ptr));
|
||
|
||
// 完整收集(reset 的 maybe_collect 路径):对象搬移、字符串地址稳定。
|
||
let mut gc = std::mem::take(&mut vm.gc_state.session_gc);
|
||
gc.collect(&mut vm);
|
||
vm.gc_state.session_gc = gc;
|
||
|
||
// 存活对象经重写仍可达其串:串未被误释放,内容可读。
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&s_ptr));
|
||
assert_eq!(unsafe { (*s_ptr).as_str() }, "kept".repeat(8));
|
||
assert_eq!(unsafe { (*vm.regs[0].as_js_object_ptr()).get_prop_at(0) }, s);
|
||
}
|
||
|
||
#[test]
|
||
fn strings_only_collection_byte_accounting_matches_survivors() {
|
||
let mut vm = vm_with_threshold(1);
|
||
let obj = plain_object(&mut vm);
|
||
let obj_session = vm.promote_object(obj);
|
||
vm.regs[0] = JsValue::from_js_object(obj_session);
|
||
let live = vm.new_string_owned("live".repeat(8));
|
||
let live_ptr = live.as_string_ptr_mut();
|
||
vm.regs[1] = live;
|
||
let dead = vm.new_string_owned("dead".repeat(8));
|
||
let _ = dead.as_string_ptr_mut();
|
||
|
||
vm.maybe_collect_session_strings();
|
||
|
||
// 账目 = 对象头 + 存活串(size_of::<JsString>() + len),死串不再计入。
|
||
let expected = size_of::<JsObject>() + (size_of::<JsString>() + unsafe { (*live_ptr).len() });
|
||
assert_eq!(vm.gc_state.session_bytes_allocated, expected);
|
||
assert!(!vm.gc_state.session_string_ptrs.contains(&dead.as_string_ptr_mut()));
|
||
}
|
||
|
||
#[test]
|
||
fn runtime_collection_below_threshold_is_noop() {
|
||
let mut vm = Vm::new();
|
||
let s = vm.new_string_owned("small".repeat(4));
|
||
vm.regs[0] = s;
|
||
|
||
assert_eq!(vm.gc_state.session_gc.total_collections, 0);
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&s.as_string_ptr_mut()));
|
||
}
|
||
|
||
// ── rope(Cons)GC 传播闭包 ──
|
||
|
||
/// 构造 `left + right` 的 Cons 节点(返回 (父, 左, 右) 三指针)。
|
||
fn make_cons_pair(vm: &mut Vm, l: &str, r: &str) -> (JsValue, *mut JsString, *mut JsString) {
|
||
let left = vm.new_string(l);
|
||
let right = vm.new_string(r);
|
||
let parent = vm.new_cons_string(left, right);
|
||
(parent, left.as_string_ptr_mut(), right.as_string_ptr_mut())
|
||
}
|
||
|
||
#[test]
|
||
fn rope_survives_collection_with_children_propagated() {
|
||
let mut vm = Vm::new();
|
||
let (parent, l_ptr, r_ptr) = make_cons_pair(&mut vm, "left-part", "right-part");
|
||
vm.regs[0] = parent;
|
||
|
||
collect(&mut vm);
|
||
|
||
// 父(寄存器根)+ 子节点(经传播闭包)全部存活,内容可读。
|
||
let parent_ptr = parent.as_string_ptr_mut();
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&parent_ptr));
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&l_ptr));
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&r_ptr));
|
||
assert_eq!(unsafe { (*parent_ptr).as_str() }, "left-partright-part");
|
||
}
|
||
|
||
#[test]
|
||
fn rope_children_swept_when_parent_dead() {
|
||
let mut vm = Vm::new();
|
||
let (parent, l_ptr, r_ptr) = make_cons_pair(&mut vm, "left", "right");
|
||
let parent_ptr = parent.as_string_ptr_mut();
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&parent_ptr));
|
||
|
||
// 父与子均无根引用 → 整树回收。
|
||
collect(&mut vm);
|
||
|
||
assert!(!vm.gc_state.session_string_ptrs.contains(&parent_ptr));
|
||
assert!(!vm.gc_state.session_string_ptrs.contains(&l_ptr));
|
||
assert!(!vm.gc_state.session_string_ptrs.contains(&r_ptr));
|
||
}
|
||
|
||
#[test]
|
||
fn rope_product_freed_with_parent() {
|
||
let mut vm = Vm::new();
|
||
let (parent, _, _) = make_cons_pair(&mut vm, "left-part", "right-part");
|
||
let parent_ptr = parent.as_string_ptr_mut();
|
||
// 触发扁平化:产物发布到 flat_cache。
|
||
assert_eq!(unsafe { (*parent_ptr).flat_str() }, "left-partright-part");
|
||
let flat_ptr = unsafe { (*parent_ptr).flat_cache_ptr() };
|
||
assert!(!flat_ptr.is_null());
|
||
|
||
collect(&mut vm);
|
||
|
||
// 父死 → 连带释放产物(不 double-free、不泄漏;产物从不进 session 表)。
|
||
let flat_mut = flat_ptr as *mut JsString;
|
||
assert!(!vm.gc_state.session_string_ptrs.contains(&parent_ptr));
|
||
assert!(!vm.gc_state.session_string_ptrs.contains(&flat_mut));
|
||
}
|
||
|
||
#[test]
|
||
fn rope_deep_chain_mark_iterative() {
|
||
let mut vm = Vm::new();
|
||
// 1024 层左倾链:mark 传播用显式栈,不爆栈、不遗漏子节点。
|
||
let mut chain = vm.new_string("root");
|
||
for _ in 0..1024 {
|
||
let leaf = vm.new_string("x");
|
||
chain = vm.new_cons_string(chain, leaf);
|
||
}
|
||
vm.regs[0] = chain;
|
||
let chain_ptr = chain.as_string_ptr_mut();
|
||
|
||
collect(&mut vm);
|
||
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&chain_ptr));
|
||
assert_eq!(unsafe { (*chain_ptr).as_str() }, format!("root{}", "x".repeat(1024)));
|
||
}
|
||
|
||
#[test]
|
||
fn rope_perm_child_untouched_by_sweep() {
|
||
let mut vm = Vm::new();
|
||
let perm = vm.perm_string("perm-leaf");
|
||
let perm_ptr = perm.as_string_ptr_mut();
|
||
let session = vm.new_string("session-leaf");
|
||
let session_ptr = session.as_string_ptr_mut();
|
||
let parent = vm.new_cons_string(perm, session);
|
||
vm.regs[0] = parent;
|
||
|
||
collect(&mut vm);
|
||
|
||
// perm 子节点永不释放(不在 session 表);session 子节点随父存活。
|
||
assert!(!vm.gc_state.session_string_ptrs.contains(&perm_ptr));
|
||
assert!(vm.gc_state.session_string_ptrs.contains(&session_ptr));
|
||
assert_eq!(unsafe { (*parent.as_string_ptr_mut()).as_str() }, "perm-leafsession-leaf");
|
||
}
|
||
|
||
#[test]
|
||
fn full_reset_frees_rope_and_product() {
|
||
let mut vm = Vm::new();
|
||
let (parent, _, _) = make_cons_pair(&mut vm, "left-part", "right-part");
|
||
// 触发扁平化(产物挂在父上)。
|
||
assert_eq!(unsafe { (*parent.as_string_ptr_mut()).flat_str() }, "left-partright-part");
|
||
vm.regs[0] = parent;
|
||
|
||
// full_reset 清空全部 session 字符串(连带产物):无泄漏、无 double-free。
|
||
vm.full_reset();
|
||
assert!(vm.gc_state.session_string_ptrs.is_empty());
|
||
}
|
||
}
|