fix: 深层分配公式改全量重算消除对象堆数据双计

深层采样层不再以手工账目为 base 叠加逐对象堆数据重算,改为对象、串、
BigInt 表长、cell 表长四分量各按当前值计一次、互不相交,消除存活对象
堆数据整份双计;bench 留存说明的账目成分列举与 run 边界 doc 措辞同步
改现语义,公式单测夹具补带元素向量的对象,钉深层与轻层零增长逐位相等。
This commit is contained in:
2026-10-01 14:01:33 +00:00
parent ce8bd6682c
commit 3c6310c586
3 changed files with 47 additions and 18 deletions
+6 -5
View File
@@ -15,11 +15,12 @@ const MEMORY_BENCH_NOTES: &str = "\n内存基准用例(mem_*):\n
mem_map_retain 原生盒边持有:Map 持 20000 组字符串键 + 对象值
mem_set_retain 原生盒边持有:Set 持 20000 个对象(各带 payload 字符串)
mem_string_rope 拼接 rope 链留存:重复二元拼接形成深 Cons 链(账目按逻辑长度计,数值高于实际足迹)
口径:Peak B/Ret B 为 session 堆账目(对象头 + 堆数据 + 存活串/BigInt);epoch arena
的临时驻留不计入字节账目(数量见 JSON 的 epoch_objects)。Peak B 为顶层指令
边界采样的 session arena 高水位;单指令内瞬态与 epoch 高水位不计。Ret B 为 workload 后
晋升 + 完整 GC 的真存活集,至多含一份引擎内部副本:逃逸屏障留 epoch 原件且顶层
寄存器根不回写时,测量时晋升会把原件再克隆一份(object_graph 见留存锚测试)。
口径:Peak B/Ret B 为 session 堆账目(对象头 + 堆数据 + 存活串);BigInt 不入
该账目,由单 run 分配公式按表长单列。epoch arena 的临时驻留不计入字节账目
(数量列恒零)。Peak B 为顶层指令边界采样的 session 堆账目高水位;单指令内瞬态
与 epoch 高水位不计。Ret B 为 workload 后晋升 + 完整 GC 的真存活集,至多含一份
引擎内部副本:逃逸屏障留 epoch 原件且顶层寄存器根不回写时,测量时晋升会把原件
再克隆一份(object_graph 见留存锚测试)。
";
/// 把结果格式化为对齐的文本表格(供终端打印)。
+22 -4
View File
@@ -1544,6 +1544,15 @@ 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 arr = vm.alloc_object(JsObject::new_array(
oxide_kernel::shape_forge::EMPTY_SHAPE_ID,
JsValue::from_js_object(vm.session.builtin_world().array_proto.as_ptr() as *mut JsObject),
2,
vm.epoch.bump(),
));
vm.regs[3] = JsValue::from_js_object(arr);
let s = vm.new_string_owned("formula".repeat(4));
vm.regs[1] = s;
let b = vm.new_bigint(num_bigint::BigInt::from(7_i128));
@@ -1558,16 +1567,25 @@ fn run_alloc_formula_lists_bigint_and_cell_via_tables() {
+ 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 不在手工账目内:账目 = 对象(头 + 堆数据,本例无堆区)+ 串。
// 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);
let arr_heap_bytes = SessionGc::object_heap_data_bytes(unsafe { &*arr });
assert!(arr_heap_bytes > 0, "数组元素向量应计入对象堆数据");
assert_eq!(
vm.gc_state.session_bytes_allocated,
2 * size_of::<JsObject>() + arr_heap_bytes as usize + 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.gc_state.session_bytes_allocated,
2 * size_of::<JsObject>() + arr_heap_bytes as usize + string_bytes
);
// 深采样全量重算与轻层公式零增长下逐位相等:存活对象带堆区(元素向量),
// 双计形态下深层严格大于轻层,该断言即失守。
assert_eq!(vm.run_alloc_bytes_full(), vm.run_alloc_bytes() as u64);
}
+19 -9
View File
@@ -1,7 +1,7 @@
//! GC 根遍历与执行期收集钩子:根统一枚举、搬移后指针重写、执行期两档
//! 收集入口(安全点门控)与 session GC 账目统计访问器。
use oxide_types::object::{Cell, JsObject};
use oxide_types::object::{Cell, JsObject, JsString};
use oxide_types::value::JsValue;
use super::frames::Completion;
@@ -285,7 +285,7 @@ impl Vm {
/// session 串)+ BigInt 表长 × `size_of::<BigInt>` + cell 表长 ×
/// `size_of::<Cell>`。三分量两两不相交(BigInt 与 cell 不在手工账目内),
/// O(1) 读,供单 run 分配上限判定。收集释放后读数回落存活集量级;
/// run 边界(reset/full_reset)后账目与表清零重起算。
/// 轻量 reset 保留账目与表(仅复位峰值与水位),full_reset 清零重起算。
///
/// 注意:手工堆账目只在对象分配/字符串分配/GC 回收点更新,执行期对象
/// 属性区(元素/属性向量扩容)增长对其不可见——上限判定须配合
@@ -296,20 +296,30 @@ impl Vm {
+ self.gc_state.session_cell_ptrs.borrow().len() * std::mem::size_of::<Cell>()
}
/// 本 run 累计分配字节的全量重算版:base 与 [`Self::run_alloc_bytes`] 同
/// 一口径(手工账目 + BigInt/cell 表长),其上逐一重算已登记对象的堆数据
/// (属性/元素向量 + upvalue 列表 + native 状态盒)容量——重算即属性区
/// 扩容等账目盲区的兜底。base 的对象分量是最近分配/回收点的账目快照,
/// 重算取当前容量,读数偏高,上限判定偏安全侧。
/// 本 run 累计分配字节的全量重算版:各分量按当前值各计一次、互不相交——
/// 存活对象头加堆数据(属性/元素/meta 向量容量、upvalue 列表容量、native
/// 状态盒)+ 存活串(头加 payload)+ BigInt 表长 × 尺寸 + cell 表长 ×
/// 尺寸。重算即属性区扩容等账目盲区的兜底,读数为当前真实总量,零增长时
/// 与 [`Self::run_alloc_bytes`] 的轻层公式逐位相等。
pub(crate) fn run_alloc_bytes_full(&self) -> u64 {
let mut bytes = self.run_alloc_bytes() as u64;
let mut bytes = 0u64;
for &ptr in self.gc_state.session_object_ptrs.iter() {
if ptr.is_null() {
continue;
}
// SAFETY: ptr 来自对象表登记,dispatch 安全点处仍有效。
bytes += SessionGc::object_heap_data_bytes(unsafe { &*ptr });
let obj = unsafe { &*ptr };
bytes += std::mem::size_of::<JsObject>() as u64 + SessionGc::object_heap_data_bytes(obj);
}
for &ptr in &self.gc_state.session_string_ptrs {
if ptr.is_null() {
continue;
}
// SAFETY: ptr 在字符串表登记,收尾前有效。
bytes += (std::mem::size_of::<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
}