mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-26 19:51:11 +08:00
fix: full_reset/run 边界换新 Bump 替代原地 reset——旧 epoch/session arena 全量归还系统分配器、容量不跨 reset 保留,配 arena 保留锚校准用例
This commit is contained in:
@@ -374,6 +374,68 @@ pub fn run_mem_closure_dead_leak(kernel: &Arc<KernelCore>) -> ExitCode {
|
|||||||
report_series("closure_dead_leak", "iter", &series)
|
report_series("closure_dead_leak", "iter", &series)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 校准用例:重源单次 run 后 `full_reset()`,读双 arena 保留锚
|
||||||
|
/// (`arena_retained_bytes`,reset 边界换新 Bump 后恰 0),再 50 轮轻源
|
||||||
|
/// (每轮 `full_reset()`)采 VmRSS,度量 full_reset 归还路径的跨轮内存
|
||||||
|
/// 增量(斜率 + 窗口总增量),期望走平。
|
||||||
|
///
|
||||||
|
/// # 注意事项
|
||||||
|
/// 不走 VM 池:drop clean 路径与池内归还走同一 `full_reset`,池锁/condvar
|
||||||
|
/// 噪音与本测面无关。重源瞬时对象全 epoch 分配、不逃逸 global;锚 == 0 为
|
||||||
|
/// 确定性主门槛(in-engine,免分配器噪音),RSS 为次锚(进程级,仅判
|
||||||
|
/// 峰后无新增量——归还 chunk 可能滞留分配器自由链不回 OS,不要求回落
|
||||||
|
/// 峰前)。
|
||||||
|
pub fn run_mem_pool_high_water(kernel: &Arc<KernelCore>) -> ExitCode {
|
||||||
|
const LIGHT_ROUNDS: usize = 50;
|
||||||
|
|
||||||
|
let heavy_js = "var t = 0; for (var i = 0; i < 200000; i++) { var o = { s: 'abcdefghij' + i, a: [i, i + 1, i + 2] }; t += o.s.length + o.a.length; } t";
|
||||||
|
let light_js = "1 + 1";
|
||||||
|
let compile_one = |js: &str| {
|
||||||
|
let allocator = Allocator::default();
|
||||||
|
let program = oxide_parser::parse(&allocator, js).expect("parse failed");
|
||||||
|
let hash = compiled_module_hash(&program);
|
||||||
|
kernel
|
||||||
|
.code_forge()
|
||||||
|
.get_or_insert_with(hash, || Compiler::new().compile(&program))
|
||||||
|
.expect("compile failed")
|
||||||
|
};
|
||||||
|
let heavy = compile_one(heavy_js);
|
||||||
|
let light = compile_one(light_js);
|
||||||
|
|
||||||
|
let mut vm = Vm::with_kernel_core(Arc::clone(kernel));
|
||||||
|
if let Err(e) = vm.run(&heavy) {
|
||||||
|
eprintln!("[pool_high_water] heavy run failed: {e}");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
vm.full_reset();
|
||||||
|
let anchor = vm.arena_retained_bytes();
|
||||||
|
eprintln!("[pool_high_water] anchor after full_reset: {anchor} bytes (expect 0)");
|
||||||
|
if anchor != 0 {
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sampler = LeakSampler::new(20);
|
||||||
|
let mut series: Vec<(usize, f64)> = Vec::new();
|
||||||
|
for i in 0..LIGHT_ROUNDS {
|
||||||
|
if let Err(e) = vm.run(&light) {
|
||||||
|
eprintln!("[pool_high_water] round {i} run failed: {e}");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
vm.full_reset();
|
||||||
|
if let Some(kb) = read_vmrss_kb() {
|
||||||
|
let v = kb as f64;
|
||||||
|
series.push((i, v));
|
||||||
|
if let Some(verdict) = sampler.add_sample(i, v) {
|
||||||
|
eprintln!(
|
||||||
|
"[LEAK] pool_high_water rss_kb: slope={:.6} R²={:.4} over {} samples",
|
||||||
|
verdict.slope, verdict.r2, 20
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report_series("pool_high_water", "round", &series)
|
||||||
|
}
|
||||||
|
|
||||||
/// 全序列回归报告:打印首末 RSS、全序列斜率/R² 与窗口(末 20 样本)总
|
/// 全序列回归报告:打印首末 RSS、全序列斜率/R² 与窗口(末 20 样本)总
|
||||||
/// 增量;斜率显著为正(R² > 0.9)返回失败,表示泄漏签名。
|
/// 增量;斜率显著为正(R² > 0.9)返回失败,表示泄漏签名。
|
||||||
fn report_series(case: &str, unit: &str, series: &[(usize, f64)]) -> ExitCode {
|
fn report_series(case: &str, unit: &str, series: &[(usize, f64)]) -> ExitCode {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ pub fn run_benchmarks(config: BenchConfig, kernel: Arc<KernelCore>, pool: Arc<Vm
|
|||||||
Some("vm_creation") => leak_detect::run_mem_vm_creation_leak(&kernel),
|
Some("vm_creation") => leak_detect::run_mem_vm_creation_leak(&kernel),
|
||||||
Some("dirty_rebuild") => leak_detect::run_mem_dirty_rebuild_leak(&kernel),
|
Some("dirty_rebuild") => leak_detect::run_mem_dirty_rebuild_leak(&kernel),
|
||||||
Some("closure_dead_leak") => leak_detect::run_mem_closure_dead_leak(&kernel),
|
Some("closure_dead_leak") => leak_detect::run_mem_closure_dead_leak(&kernel),
|
||||||
|
Some("pool_high_water") => leak_detect::run_mem_pool_high_water(&kernel),
|
||||||
_ => leak_detect::run_leak_detect(&config, &kernel, &pool),
|
_ => leak_detect::run_leak_detect(&config, &kernel, &pool),
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
//! 内存管理抽象:跨 epoch 持久存储与每次调用(agent call)内的 arena 分配。
|
//! 内存管理抽象:跨 epoch 持久存储与每次调用(agent call)内的 arena 分配。
|
||||||
//!
|
//!
|
||||||
//! `P<T>` 是 `Arc` 的透明包装,持有者跨 `Epoch::reset()` 存活;
|
//! `P<T>` 是 `Arc` 的透明包装,持有者跨 `Epoch::reset()` 存活;
|
||||||
//! `Epoch` 则是对 `bumpalo::Bump` 的封装,用于每次调用内的高频分配与
|
//! `Epoch` 则是对 `bumpalo::Bump` 的封装,用于每次调用内的高频分配,
|
||||||
//! O(1) 整体回收,并通过 epoch ID 辅助悬挂指针检测。
|
//! `reset()` 换新 arena 整体回收(旧 arena 全量归还系统分配器,地址空间
|
||||||
|
//! 不复用),并通过 epoch ID 辅助悬挂指针检测。
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
@@ -67,7 +68,8 @@ impl<T: fmt::Display> fmt::Display for P<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 包装 `bumpalo::Bump` 并携带 epoch ID 计数器用于悬挂指针检测。
|
/// 包装 `bumpalo::Bump` 并携带 epoch ID 计数器用于悬挂指针检测。
|
||||||
/// 所有 Agent 调用级对象分配于此;`reset()` 在每次调用结束时 O(1) 清空。
|
/// 所有 Agent 调用级对象分配于此;`reset()` 在每次调用结束时换新 arena,
|
||||||
|
/// 旧 arena 内存全量归还系统分配器。
|
||||||
pub struct Epoch {
|
pub struct Epoch {
|
||||||
bump: bumpalo::Bump,
|
bump: bumpalo::Bump,
|
||||||
epoch_id: u64,
|
epoch_id: u64,
|
||||||
@@ -128,10 +130,15 @@ impl Epoch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// O(1) 批量释放。此前所有分配立即失效。
|
/// 换新空 `Bump` 整体回收:旧 arena 的全部 chunk 归还系统分配器,
|
||||||
|
/// 地址空间不复用,此前所有分配立即失效。
|
||||||
/// 递增 epoch ID 以使过期指针失效(debug_assert 守卫)。
|
/// 递增 epoch ID 以使过期指针失效(debug_assert 守卫)。
|
||||||
|
///
|
||||||
|
/// # 注意事项
|
||||||
|
/// 调用点须保证该边界无存活旧 arena 指针(登记表已清、执行态已空)。
|
||||||
|
/// 换 Bump 使漏晋升的陈旧指针由静默别名新对象转为显式 UAF,属护栏加强。
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.bump.reset();
|
self.bump = bumpalo::Bump::new();
|
||||||
self.epoch_id += 1;
|
self.epoch_id += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -992,6 +992,16 @@ impl Vm {
|
|||||||
self.gc_state.session_bytes_peak
|
self.gc_state.session_bytes_peak
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 双 arena(epoch + session 对象)当前 chunk 已分配字节之和。
|
||||||
|
///
|
||||||
|
/// # 注意事项
|
||||||
|
/// reset 边界读 = 换新 Bump 后空 arena 恰 0(空 chunk 哨兵计 0);
|
||||||
|
/// run 中途读 = 当前 chunk 用量(非全 arena 口径——run 计量走
|
||||||
|
/// [`Self::run_alloc_bytes`],勿据此断言)。
|
||||||
|
pub fn arena_retained_bytes(&self) -> usize {
|
||||||
|
self.epoch.bump().allocated_bytes() + self.gc_state.session_epoch.allocated_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
/// 本 run 累计分配字节:epoch arena + session 对象 arena + session 手工堆
|
/// 本 run 累计分配字节:epoch arena + session 对象 arena + session 手工堆
|
||||||
/// 账目(session 串 + session 对象及其属性向量 + GC 后补回的 BigInt)。
|
/// 账目(session 串 + session 对象及其属性向量 + GC 后补回的 BigInt)。
|
||||||
/// 单次 run 内单调不减(执行期对象不回收、
|
/// 单次 run 内单调不减(执行期对象不回收、
|
||||||
|
|||||||
@@ -343,7 +343,9 @@ impl Vm {
|
|||||||
self.teardown_session_heap_data();
|
self.teardown_session_heap_data();
|
||||||
self.epoch.reset();
|
self.epoch.reset();
|
||||||
self.gc_state.epoch_object_ptrs.clear();
|
self.gc_state.epoch_object_ptrs.clear();
|
||||||
self.gc_state.session_epoch.reset();
|
// 换新 Bump:旧 session arena 全量归还系统分配器(与 sweep 路径同构),
|
||||||
|
// 容量不跨 full_reset 保留。
|
||||||
|
self.gc_state.session_epoch = bumpalo::Bump::new();
|
||||||
self.gc_state.session_bytes_allocated = 0;
|
self.gc_state.session_bytes_allocated = 0;
|
||||||
self.gc_state.session_bytes_peak = 0;
|
self.gc_state.session_bytes_peak = 0;
|
||||||
self.gc_state.string_gc_watermark = self.kernel_core.config().session_gc_threshold;
|
self.gc_state.string_gc_watermark = self.kernel_core.config().session_gc_threshold;
|
||||||
@@ -1201,6 +1203,23 @@ mod tests {
|
|||||||
assert!(unsafe { *session_ptr } == 123);
|
assert!(unsafe { *session_ptr } == 123);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// run 边界换新 Bump 后双 arena 保留锚恰 0:重源 run 冲高水位,
|
||||||
|
/// full_reset 后 epoch/session 两 arena 均空(容量不跨 reset 保留)。
|
||||||
|
#[test]
|
||||||
|
fn full_reset_zeroes_arena_retained() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
run_source(
|
||||||
|
&mut vm,
|
||||||
|
"var t = 0; for (var i = 0; i < 20000; i++) { var o = { s: 'ab' + i, a: [i] }; t += o.s.length + o.a.length; } t",
|
||||||
|
);
|
||||||
|
assert!(vm.epoch.bump().allocated_bytes() > 0, "重源 run 应冲高 epoch arena 水位");
|
||||||
|
|
||||||
|
vm.full_reset();
|
||||||
|
|
||||||
|
assert_eq!(vm.epoch.bump().allocated_bytes(), 0);
|
||||||
|
assert_eq!(vm.gc_state.session_epoch.allocated_bytes(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn immutables_cache_filled_once_per_module() {
|
fn immutables_cache_filled_once_per_module() {
|
||||||
let mut vm = Vm::new();
|
let mut vm = Vm::new();
|
||||||
@@ -1318,12 +1337,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn session_epoch_reset_is_only_in_full_reset_state_clear() {
|
fn session_epoch_replacement_is_only_in_full_reset_state_clear() {
|
||||||
let src = include_str!("vm_support.rs");
|
let src = include_str!("vm_support.rs");
|
||||||
let production = src.split("#[cfg(test)]").next().expect("production source");
|
let production = src.split("#[cfg(test)]").next().expect("production source");
|
||||||
assert_eq!(production.matches("self.gc_state.session_epoch.reset()").count(), 1);
|
assert_eq!(production.matches("self.gc_state.session_epoch = bumpalo::Bump::new()").count(), 1);
|
||||||
assert!(production.contains("fn clear_full_reset_state(&mut self)"));
|
assert!(production.contains("fn clear_full_reset_state(&mut self)"));
|
||||||
assert!(production.contains("self.gc_state.session_epoch.reset();"));
|
assert!(production.contains("self.gc_state.session_epoch = bumpalo::Bump::new();"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -27,6 +27,17 @@ fn epoch_reset_increments_id() {
|
|||||||
assert_eq!(epoch.current_id(), 2);
|
assert_eq!(epoch.current_id(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 换新 Bump 后旧地址空间不复用:reset 边界双 arena 保留锚恰 0
|
||||||
|
/// (空 chunk 哨兵计 0),容量不跨 reset 保留。
|
||||||
|
#[test]
|
||||||
|
fn epoch_reset_frees_arena() {
|
||||||
|
let mut epoch = Epoch::new();
|
||||||
|
let _ptr = epoch.alloc(vec![0u8; 1 << 20]);
|
||||||
|
assert!(epoch.bump().allocated_bytes() > 0);
|
||||||
|
epoch.reset();
|
||||||
|
assert_eq!(epoch.bump().allocated_bytes(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn persistent_new_and_deref() {
|
fn persistent_new_and_deref() {
|
||||||
let p = P::new(42i32);
|
let p = P::new(42i32);
|
||||||
|
|||||||
Reference in New Issue
Block a user