fix: test262 runner 进程 RSS 无界增长——session 收尾统一释放内建裸堆区/upvalue 别名 + 每测试分配上限 256MiB(任务 116)

- 泄漏修复:KernelSession 收尾(teardown_intrinsic_protos/free_session_upvalues)
  释放内建对象 Box::into_raw 裸堆区(release_raw_heap 四区置空幂等)与
  session upvalue 原件/晋升克隆别名去重释放;bindings 原生盒负载边、
  session_gc 边记录、vm_props 属性区同步收尾
- 失控测试封顶:runner kernel max_alloc_bytes=256MiB(dispatch 采样口径
  run_alloc_bytes/run_alloc_bytes_full),超限计 memory-limit 失败而非拖垮
  进程 RSS(arena 高水位 GB 级 × 多 worker = 宿主 OOM)
- 归属:RSS 泄漏 pre-existing(HEAD 无 WIP 同现);identifiers escaped
  unicode 挂死族另立 B132(pre-existing,supervise 超时自愈)
- 验证:build/clippy/四 crate 测试全绿零新增告警;language 前 5000 测试
  单窗峰值 330MB(旧 ~4.5MB/测试 单调增长→88GB OOM 签名消失),
  窗口间峰值 0.17-1.6GB 有界、逐测试释放回基线
This commit is contained in:
2026-09-11 03:55:59 +00:00
parent a5258f2739
commit 227dfe008f
19 changed files with 566 additions and 100 deletions
+38 -4
View File
@@ -16,7 +16,7 @@ use crate::string_forge::PermInterner;
use oxide_log;
use oxide_log::{Level, SUBSYSTEM_COUNT};
/// kernel 运行配置:VM 池规模、步数/调用深度上限、session GC 阈值、
/// kernel 运行配置:VM 池规模、步数/调用深度/单 run 分配上限、session GC 阈值、
/// 日志级别与内置对象预热开关。由三个预设构造器([`KernelConfig::minimal`] /
/// [`KernelConfig::standard`] / [`KernelConfig::full`])或默认值创建。
#[derive(Clone)]
@@ -25,6 +25,10 @@ pub struct KernelConfig {
pub max_pool_size: Option<usize>,
pub max_dead_strings: Option<usize>,
pub max_steps: Option<u64>,
/// 单次 run 的分配上限(epoch + session arena + session 堆账目字节):
/// 超限的 run 以 `VM memory limit exceeded` 失败,防单测试 arena 高水位
/// 拖垮宿主内存。None 为无上限(CLI/嵌入默认;runner 按场景设置)。
pub max_alloc_bytes: Option<usize>,
pub max_call_depth: usize,
pub session_gc_threshold: usize,
pub max_cached_modules: usize,
@@ -42,6 +46,7 @@ impl KernelConfig {
max_pool_size: Some(8),
max_dead_strings: Some(10_000),
max_steps: None,
max_alloc_bytes: None,
max_call_depth: 1024,
session_gc_threshold: 33_554_432,
max_cached_modules: 512,
@@ -59,6 +64,7 @@ impl KernelConfig {
max_pool_size: Some(32),
max_dead_strings: Some(10_000),
max_steps: None,
max_alloc_bytes: None,
max_call_depth: 1024,
session_gc_threshold: 33_554_432,
max_cached_modules: 512,
@@ -76,6 +82,7 @@ impl KernelConfig {
max_pool_size: None,
max_dead_strings: Some(5_000),
max_steps: None,
max_alloc_bytes: None,
max_call_depth: 1024,
session_gc_threshold: 33_554_432,
max_cached_modules: 512,
@@ -698,26 +705,53 @@ impl KernelSession {
self.dirty_since_snapshot().any()
}
/// 收尾释放 session 拥有的手工堆数据:builtin world(方法 wrapper 登记表 +
/// 全部 P 对象属性区)与 global 对象属性区。对象本体随 Arc 引用归零释放。
///
/// # 注意事项
/// 幂等(登记表按值取走、属性区释放后置空),session 生命周期内可安全重入;
/// 仅应在 session 真正终止时调用——选择性重置换出的旧 world/global 不走本路径。
pub fn teardown_builtins(&mut self) {
self.builtin_world.teardown_heap_data();
// SAFETY: global 归本 session 所有,收尾时恰好释放其属性区一次。
let global = unsafe { &mut *(self.global_object.as_ptr() as *mut JsObject) };
global.release_raw_heap();
}
/// 选择性重置:只重建被污染的对象(global 或相应 builtin 家族),并返回脏集合。
///
/// 相比全量重建,可保留未污染的内置对象指针与世代,减少隔离成本。
pub fn selective_reset(&mut self, core: &Arc<KernelCore>) -> BuiltinDirtySet {
let dirty = self.dirty_since_snapshot();
if dirty.global {
// 旧 global 的属性区在替换前释放,避免随旧引用永久泄漏。
let old_global = unsafe { &mut *(self.global_object.as_ptr() as *mut JsObject) };
old_global.release_raw_heap();
self.global_object = Self::new_global_object(core);
}
if dirty.any_builtin_dirty() {
self.builtin_world = Arc::new(BuiltinWorld::rebuild_with_dirty(
&self.builtin_world,
let old_world = self.builtin_world.clone();
let new_world = BuiltinWorld::rebuild_with_dirty(
&old_world,
core.perm_interner.as_ref(),
core.shape_forge.as_ref(),
&dirty,
));
);
// 旧 world 的登记表并入新 world:存活 wrapper(未污染家族别名)仍须在
// session 收尾统一释放;已弃 wrapper 随之恰好释放一次,不二次持有。
new_world.inherit_leaked_objects(&old_world);
self.builtin_world = Arc::new(new_world);
}
dirty
}
}
impl Drop for KernelSession {
fn drop(&mut self) {
self.teardown_builtins();
}
}
#[cfg(test)]
mod tests {
use super::*;