perf: 账目公式切手工口径退役 arena 计数器

This commit is contained in:
2026-10-01 10:35:29 +00:00
parent b4e36fc147
commit ce8bd6682c
7 changed files with 95 additions and 62 deletions
+26 -7
View File
@@ -450,17 +450,17 @@ 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 保留锚 /// 校准用例:重源单次 run 后 `full_reset()`,读 session 账目锚
/// (`arena_retained_bytes`,reset 边界换新 Bump 后恰 0),再 50 轮轻源 /// (`session_bytes_allocated`,全量重置清表归零后恰 0),再 50 轮轻源
/// (每轮 `full_reset()`)采 VmRSS,度量 full_reset 归还路径的跨轮内存 /// (每轮 `full_reset()`)采 VmRSS,度量 full_reset 归还路径的跨轮内存
/// 增量(斜率 + 窗口总增量),期望走平。 /// 增量(斜率 + 窗口总增量),期望走平。
/// ///
/// # 注意事项 /// # 注意事项
/// 不走 VM 池:drop clean 路径与池内归还走同一 `full_reset`,池锁/condvar /// 不走 VM 池:drop clean 路径与池内归还走同一 `full_reset`,池锁/condvar
/// 噪音与本测面无关。重源瞬时对象全 epoch 分配、不逃逸 global;锚 == 0 为 /// 噪音与本测面无关。重源瞬时对象不逃逸 global,全量重置后 session 账目
/// 确定性主门槛(in-engine,免分配器噪音),RSS 序列为次锚(进程级辅助指标, /// 清零;锚 == 0 为确定性主门槛(in-engine,免分配器噪音),RSS 序列为次锚
/// 峰后无新增量——归还 chunk 可能滞留分配器自由链不回 OS,不要求回落 /// (进程级辅助指标,峰后无新增量——归还 chunk 可能滞留分配器自由链不回
/// 峰前)。 /// OS,不要求回落峰前)。
pub fn run_mem_pool_high_water(kernel: &Arc<KernelCore>) -> ExitCode { pub fn run_mem_pool_high_water(kernel: &Arc<KernelCore>) -> ExitCode {
const LIGHT_ROUNDS: usize = 50; const LIGHT_ROUNDS: usize = 50;
@@ -484,7 +484,7 @@ pub fn run_mem_pool_high_water(kernel: &Arc<KernelCore>) -> ExitCode {
return ExitCode::FAILURE; return ExitCode::FAILURE;
} }
vm.full_reset(); vm.full_reset();
let anchor = vm.arena_retained_bytes(); let anchor = vm.session_bytes_allocated();
eprintln!("[pool_high_water] anchor after full_reset: {anchor} bytes (expect 0)"); eprintln!("[pool_high_water] anchor after full_reset: {anchor} bytes (expect 0)");
if anchor != 0 { if anchor != 0 {
return ExitCode::FAILURE; return ExitCode::FAILURE;
@@ -898,6 +898,25 @@ mod tests {
assert_eq!(r2, 1.0); assert_eq!(r2, 1.0);
} }
/// 全量重置后 session 账目锚恰 0:`mem_pool_high_water` 的确定性主门槛
/// (锚 == 0)依赖全量重置清表归零,重源 run 后账目非零作对照。
#[test]
fn session_accounting_zero_after_full_reset() {
let kernel = KernelCore::new(KernelConfig::standard());
let mut vm = Vm::with_kernel_core(Arc::clone(&kernel));
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, "var o = { a: 1 }; o").expect("parse");
let hash = compiled_module_hash(&program);
let module = kernel
.code_forge()
.get_or_insert_with(hash, || Compiler::new().compile(&program))
.expect("compile");
vm.run(&module).expect("run");
assert!(vm.session_bytes_allocated() > 0);
vm.full_reset();
assert_eq!(vm.session_bytes_allocated(), 0);
}
/// 窗口采样器:正斜率序列出判定且斜率还原正确。 /// 窗口采样器:正斜率序列出判定且斜率还原正确。
#[test] #[test]
fn sampler_verdict_on_rising_series() { fn sampler_verdict_on_rising_series() {
+15 -17
View File
@@ -593,7 +593,8 @@ impl SessionGc {
/// ///
/// # 副作用 /// # 副作用
/// - `session_object_ptrs` 仅剩存活对象;`session_bytes_allocated` 重算为 /// - `session_object_ptrs` 仅剩存活对象;`session_bytes_allocated` 重算为
/// 存活对象字节(串/BigInt/cell 分量由各清扫路径自行补回); /// 存活对象字节(串分量由串清扫补回;BigInt/cell 不在手工账目内,
/// 由 `run_alloc_bytes` 公式按表长单列);
/// - `total_*` 与 `last_collection_*` 统计按对象口径更新(绝对赋值, /// - `total_*` 与 `last_collection_*` 统计按对象口径更新(绝对赋值,
/// 串/BigInt/cell 清扫在其上累加)。 /// 串/BigInt/cell 清扫在其上累加)。
/// ///
@@ -729,15 +730,14 @@ impl SessionGc {
pub(crate) fn sweep_session_bigints(&mut self, vm: &mut Vm) -> u64 { 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 old = vm.gc_state.session_bigint_ptrs.borrow_mut().drain(..).collect::<Vec<_>>();
let mut freed = 0u64; let mut freed = 0u64;
let mut live_bytes = 0usize;
let mut live = Vec::with_capacity(old.len()); let mut live = Vec::with_capacity(old.len());
for ptr in old { for ptr in old {
if ptr.is_null() { if ptr.is_null() {
continue; continue;
} }
if self.live_bigints.contains(&ptr) { if self.live_bigints.contains(&ptr) {
// 存活——地址不变,无需重写。 // 存活——地址不变,无需重写。BigInt 不入手工账目,
live_bytes += size_of::<num_bigint::BigInt>(); // 存活字节由 `run_alloc_bytes` 公式按表长单列。
live.push(ptr); live.push(ptr);
} else { } else {
// SAFETY: ptr 在 session_bigint_ptrs 中但不可达,恰好释放一次。 // SAFETY: ptr 在 session_bigint_ptrs 中但不可达,恰好释放一次。
@@ -748,7 +748,6 @@ impl SessionGc {
} }
} }
*vm.gc_state.session_bigint_ptrs.borrow_mut() = live; *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.total_bytes_freed = self.total_bytes_freed.saturating_add(freed);
self.last_collection_bytes_freed = self.last_collection_bytes_freed.saturating_add(freed); self.last_collection_bytes_freed = self.last_collection_bytes_freed.saturating_add(freed);
@@ -759,8 +758,9 @@ impl SessionGc {
/// `Box::from_raw` 释放。存活 cell 不被搬移——Box 地址稳定——因此无需 /// `Box::from_raw` 释放。存活 cell 不被搬移——Box 地址稳定——因此无需
/// forwarding 表与根指针重写。在 BigInt 清扫之后运行。 /// forwarding 表与根指针重写。在 BigInt 清扫之后运行。
/// ///
/// cell 不在 `session_bytes_allocated` 账目口径内(与字符串/BigInt 补回口径 /// cell 不在 `session_bytes_allocated` 手工账目口径内(串分量在账目内,
/// 不同),故只累计释放字节统计、不重算存活账目。 /// BigInt/cell 由 `run_alloc_bytes` 公式按表长单列),故只累计释放字节
/// 统计、不重算存活账目。
/// 返回释放的字节数。 /// 返回释放的字节数。
pub(crate) fn sweep_session_cells(&mut self, vm: &mut Vm) -> u64 { pub(crate) fn sweep_session_cells(&mut self, vm: &mut Vm) -> u64 {
let old = vm.gc_state.session_cell_ptrs.borrow_mut().drain(..).collect::<Vec<_>>(); let old = vm.gc_state.session_cell_ptrs.borrow_mut().drain(..).collect::<Vec<_>>();
@@ -881,7 +881,8 @@ impl SessionGc {
/// 本路径不跑对象 sweep(sweep 内清位不会执行),故开头与结尾都清位—— /// 本路径不跑对象 sweep(sweep 内清位不会执行),故开头与结尾都清位——
/// 维持"mark 之后必由清位收尾"的不变量,strings-only 结束不留残留。 /// 维持"mark 之后必由清位收尾"的不变量,strings-only 结束不留残留。
/// - 不搬移对象,`session_bytes_allocated` 保持对象账目,手动扣掉字符串账目后 /// - 不搬移对象,`session_bytes_allocated` 保持对象账目,手动扣掉字符串账目后
/// 由 `sweep_session_strings` 补回存活串字节,与完整收集的最终账目一致。 /// 由 `sweep_session_strings` 补回存活串字节(BigInt/cell 不在手工账目内),
/// 与完整收集的最终账目一致。
pub(crate) fn collect_strings_only(&mut self, vm: &mut Vm) { pub(crate) fn collect_strings_only(&mut self, vm: &mut Vm) {
let start = Instant::now(); let start = Instant::now();
@@ -967,7 +968,8 @@ impl SessionGc {
/// ///
/// # 副作用 /// # 副作用
/// - `session_object_ptrs` 仅剩存活,`session_bytes_allocated` 按存活整体 /// - `session_object_ptrs` 仅剩存活,`session_bytes_allocated` 按存活整体
/// 重算(对象 + 串 + BigInt;本路径不扫串/BigInt,串 GC 归 strings-only); /// 重算(对象 + 串;本路径不扫串,串表整体计入,串 GC 归 strings-only;
/// BigInt/cell 不在手工账目内,由公式按表长单列);
/// - `gc_watermark` = 当前分配包络 + 阈值增量; /// - `gc_watermark` = 当前分配包络 + 阈值增量;
/// - 累计/时长统计更新;mark 位全清(收集后无残留)。 /// - 累计/时长统计更新;mark 位全清(收集后无残留)。
pub(crate) fn collect_in_run(&mut self, vm: &mut Vm) { pub(crate) fn collect_in_run(&mut self, vm: &mut Vm) {
@@ -987,8 +989,9 @@ impl SessionGc {
// 死 cell 随死对象出表:存活 cell 经 mark 种子 + 对象边入存活集,此处清扫。 // 死 cell 随死对象出表:存活 cell 经 mark 种子 + 对象边入存活集,此处清扫。
self.sweep_session_cells(vm); self.sweep_session_cells(vm);
// 整体口径重算:对象分量已由清扫核心写入,此处补串与 BigInt 分量 // 整体口径重算:对象分量已由清扫核心写入,此处补串分量(本路径不扫
// (本路径不扫串/BigInt,按当前表整体计入,与 strings-only 口径一致)。 // 串,串表整体计入,与 strings-only 口径一致;BigInt/cell 不在手工
// 账目内,由 `run_alloc_bytes` 公式按表长单列)。
let mut string_bytes: usize = 0; let mut string_bytes: usize = 0;
for &ptr in &vm.gc_state.session_string_ptrs { for &ptr in &vm.gc_state.session_string_ptrs {
if ptr.is_null() { if ptr.is_null() {
@@ -997,12 +1000,7 @@ impl SessionGc {
// SAFETY: ptr 在字符串表登记,收尾前有效。 // SAFETY: ptr 在字符串表登记,收尾前有效。
string_bytes += size_of::<JsString>() + unsafe { (*ptr).payload_bytes() }; string_bytes += size_of::<JsString>() + unsafe { (*ptr).payload_bytes() };
} }
let bigint_bytes = vm.gc_state.session_bigint_ptrs.borrow().len() * size_of::<num_bigint::BigInt>(); vm.gc_state.session_bytes_allocated = vm.gc_state.session_bytes_allocated.saturating_add(string_bytes);
vm.gc_state.session_bytes_allocated = vm
.gc_state
.session_bytes_allocated
.saturating_add(string_bytes)
.saturating_add(bigint_bytes);
// 抬高下次触发水位:当前分配包络 + 阈值增量——存活包络超阈值时不每指令 // 抬高下次触发水位:当前分配包络 + 阈值增量——存活包络超阈值时不每指令
// 重复触发无死对象可回收的白跑。 // 重复触发无死对象可回收的白跑。
+32
View File
@@ -1539,6 +1539,38 @@ fn strings_only_collection_byte_accounting_matches_survivors() {
assert!(!vm.gc_state.session_string_ptrs.contains(&dead.as_string_ptr_mut())); assert!(!vm.gc_state.session_string_ptrs.contains(&dead.as_string_ptr_mut()));
} }
#[test]
fn run_alloc_formula_lists_bigint_and_cell_via_tables() {
let mut vm = Vm::new();
let obj = plain_object(&mut vm);
vm.regs[0] = JsValue::from_js_object(obj);
let s = vm.new_string_owned("formula".repeat(4));
vm.regs[1] = s;
let b = vm.new_bigint(num_bigint::BigInt::from(7_i128));
vm.regs[2] = b;
// 未根 cell:收集后不可达,随 sweep 出表。
vm.gc_state.alloc_cell(JsValue::int(9), true);
// 公式 = 手工账目 + BigInt 表长 × 尺寸 + cell 表长 × 尺寸,三分量两两不相交。
assert_eq!(
vm.run_alloc_bytes(),
vm.gc_state.session_bytes_allocated
+ vm.gc_state.session_bigint_ptrs.borrow().len() * size_of::<num_bigint::BigInt>()
+ vm.gc_state.session_cell_ptrs.borrow().len() * size_of::<Cell>()
);
// BigInt 不在手工账目内:账目 = 对象(头 + 堆数据,本例无堆区)+ 串。
let string_bytes = size_of::<JsString>() + unsafe { (*s.as_string_ptr_mut()).payload_bytes() };
assert_eq!(vm.gc_state.session_bytes_allocated, size_of::<JsObject>() + string_bytes);
// 完整收集后:死 cell 出表、存活 BigInt 留表,账目保持对象 + 串口径。
vm.collect_session_gc();
assert_eq!(vm.gc_state.session_cell_ptrs.borrow().len(), 0);
assert_eq!(vm.gc_state.session_bigint_ptrs.borrow().len(), 1);
assert_eq!(vm.gc_state.session_bytes_allocated, size_of::<JsObject>() + string_bytes);
// 深采样全量重算 = 轻层公式 + 逐对象堆数据重算(本例无堆区,两值相等)。
assert_eq!(vm.run_alloc_bytes_full(), vm.run_alloc_bytes() as u64);
}
#[test] #[test]
fn runtime_collection_below_threshold_is_noop() { fn runtime_collection_below_threshold_is_noop() {
let mut vm = Vm::new(); let mut vm = Vm::new();
+1 -1
View File
@@ -134,7 +134,7 @@ impl Vm {
if bytes > self.gc_state.session_bytes_peak { if bytes > self.gc_state.session_bytes_peak {
self.gc_state.session_bytes_peak = bytes; self.gc_state.session_bytes_peak = bytes;
} }
// 单 run 分配包络高水位:O(1) 三计数器读(双 arena + session 账目)。 // 单 run 分配包络高水位:O(1) 读(手工账目 + BigInt/cell 表长)。
let alloc = self.run_alloc_bytes(); let alloc = self.run_alloc_bytes();
if alloc > self.gc_state.run_alloc_peak { if alloc > self.gc_state.run_alloc_peak {
self.gc_state.run_alloc_peak = alloc; self.gc_state.run_alloc_peak = alloc;
+15 -32
View File
@@ -275,45 +275,34 @@ 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 累计分配字节的高水位:`run_alloc_bytes` 的顶层指令边界 /// 本 run 累计分配字节的高水位:`run_alloc_bytes` 的顶层指令边界
/// 采样上界,run 边界(reset/full_reset)重起算。留存内存观测锚。 /// 采样上界,run 边界(reset/full_reset)重起算。留存内存观测锚。
pub fn run_alloc_peak(&self) -> usize { pub fn run_alloc_peak(&self) -> usize {
self.gc_state.run_alloc_peak self.gc_state.run_alloc_peak
} }
/// 本 run 累计分配字节:epoch arena + session 对象 arena + session 手工堆 /// 本 run 累计分配字节:session 手工堆账目(session 对象及其属性向量 +
/// 账目(session 串 + session 对象及其属性向量 + GC 后补回的 BigInt)。 /// session 串)+ BigInt 表长 × `size_of::<BigInt>` + cell 表长 ×
/// 单次 run 内单调不减(执行期对象不回收、 /// `size_of::<Cell>`。三分量两两不相交(BigInt 与 cell 不在手工账目内),
/// 串 GC 只降手工堆账目而 arena 不减);run 边界(reset)后重新起算, /// O(1) 读,供单 run 分配上限判定。收集释放后读数回落存活集量级;
/// 供单 run 分配上限判定。 /// run 边界(reset/full_reset)后账目与表清零重起算。
/// ///
/// 注意:手工堆账目只在 promote/字符串分配/GC 回收点更新,执行期对象 /// 注意:手工堆账目只在对象分配/字符串分配/GC 回收点更新,执行期对象
/// 属性区(元素/属性向量扩容)增长对其不可见——上限判定须配合 /// 属性区(元素/属性向量扩容)增长对其不可见——上限判定须配合
/// [`Self::run_alloc_bytes_full`] 的深采样层。 /// [`Self::run_alloc_bytes_full`] 的深采样层。
pub(crate) fn run_alloc_bytes(&self) -> usize { pub(crate) fn run_alloc_bytes(&self) -> usize {
self.epoch.bump().allocated_bytes() self.gc_state.session_bytes_allocated
+ self.gc_state.session_epoch.allocated_bytes() + self.gc_state.session_bigint_ptrs.borrow().len() * std::mem::size_of::<num_bigint::BigInt>()
+ self.gc_state.session_bytes_allocated + self.gc_state.session_cell_ptrs.borrow().len() * std::mem::size_of::<Cell>()
} }
/// 本 run 累计分配字节的全量重算版:base 只取两个 arena 计数器 /// 本 run 累计分配字节的全量重算版:base 与 [`Self::run_alloc_bytes`] 同
/// (epoch + session 对象 arena),手工堆逐一重算——已登记对象的堆数据 /// 一口径(手工账目 + BigInt/cell 表长),其上逐一重算已登记对象的堆数据
/// (属性/元素向量 + upvalue 列表 + native 状态盒)、session 串、BigInt /// (属性/元素向量 + upvalue 列表 + native 状态盒)容量——重算即属性区
/// 与 upvalue cell 的容量。三分量(arena / 对象堆数据 / 串-BigInt-cell) /// 扩容等账目盲区的兜底。base 的对象分量是最近分配/回收点的账目快照,
/// 两两不相交,且不含 session 手工堆账目——无交叠不双计,重算即属性区 /// 重算取当前容量,读数偏高,上限判定偏安全侧。
/// 扩容等账目盲区的兜底。
pub(crate) fn run_alloc_bytes_full(&self) -> u64 { pub(crate) fn run_alloc_bytes_full(&self) -> u64 {
let mut bytes = (self.epoch.bump().allocated_bytes() + self.gc_state.session_epoch.allocated_bytes()) as u64; let mut bytes = self.run_alloc_bytes() as u64;
for &ptr in self.gc_state.session_object_ptrs.iter() { for &ptr in self.gc_state.session_object_ptrs.iter() {
if ptr.is_null() { if ptr.is_null() {
continue; continue;
@@ -321,12 +310,6 @@ impl Vm {
// SAFETY: ptr 来自对象表登记,dispatch 安全点处仍有效。 // SAFETY: ptr 来自对象表登记,dispatch 安全点处仍有效。
bytes += SessionGc::object_heap_data_bytes(unsafe { &*ptr }); bytes += SessionGc::object_heap_data_bytes(unsafe { &*ptr });
} }
for &ptr in &self.gc_state.session_string_ptrs {
// SAFETY: ptr 来自字符串表登记,收尾前始终有效。
bytes += (std::mem::size_of::<oxide_types::object::JsString>() + unsafe { (*ptr).payload_bytes() }) as u64;
}
bytes += (self.gc_state.session_bigint_ptrs.borrow().len() * std::mem::size_of::<num_bigint::BigInt>()) as u64;
bytes += (self.gc_state.session_cell_ptrs.borrow().len() * std::mem::size_of::<Cell>()) as u64;
bytes bytes
} }
+4 -3
View File
@@ -28,9 +28,10 @@ pub(crate) struct GcState {
pub(crate) session_gc: SessionGc, pub(crate) session_gc: SessionGc,
pub(crate) session_object_ptrs: Vec<*mut JsObject>, pub(crate) session_object_ptrs: Vec<*mut JsObject>,
pub(crate) session_string_ptrs: Vec<*mut JsString>, pub(crate) session_string_ptrs: Vec<*mut JsString>,
/// BigInt 堆 box(`Box<i128>`)追踪表。`RefCell` 使 `&self` 的 /// BigInt 堆 box 追踪表。`RefCell` 使 `&self` 的分配入口(常量池
/// `convert_immutables` 也能登记新 box。与字符串不同,BigInt 不参与 /// `convert_immutables`)也能登记新 box。BigInt 参与 mark/sweep 回收
/// mark/sweep 回收(值量少),只在 full_reset 统一释放。 /// (死 BigInt 随收集释放,存活字节由 `run_alloc_bytes` 公式按表长
/// 单列),full_reset 为收尾兜底(表已空时 no-op)。
pub(crate) session_bigint_ptrs: RefCell<Vec<*mut num_bigint::BigInt>>, pub(crate) session_bigint_ptrs: RefCell<Vec<*mut num_bigint::BigInt>>,
/// upvalue cell(`Box<Cell>`)追踪表。`RefCell` 使 `&self` 的分配入口也能 /// upvalue cell(`Box<Cell>`)追踪表。`RefCell` 使 `&self` 的分配入口也能
/// 登记新 box。cell 独立堆分配、地址稳定,不参与对象搬移(原地 sweep /// 登记新 box。cell 独立堆分配、地址稳定,不参与对象搬移(原地 sweep
+2 -2
View File
@@ -449,8 +449,8 @@ fn reset_drops_stale_uncaught_value() {
assert!(vm.last_uncaught_value.is_none(), "full_reset 应清空未捕获异常侧通道"); assert!(vm.last_uncaught_value.is_none(), "full_reset 应清空未捕获异常侧通道");
} }
/// run 边界换新 Bump 后双 arena 保留锚恰 0:重源 run 冲高水位, /// 全量重置后 epoch/session 两 arena 均空:容量不跨 reset 保留。对象
/// full_reset 后 epoch/session 两 arena 均空(容量不跨 reset 保留)。 /// Box 化后 arena 零分配,此不变量是分配包络公式排除 arena 计数器的前提。
#[test] #[test]
fn full_reset_zeroes_arena_retained() { fn full_reset_zeroes_arena_retained() {
let mut vm = Vm::new(); let mut vm = Vm::new();