test: 新增内存占用基准用例与留存内存采集

bench 每轮采集在 workload 后 promote 根持有/会话引用的 epoch 对象并强制
一次 full GC,新增 peak_bytes / retained_bytes / retained_objects 三口径
(原 session_* 字段为执行期累计分配,不反映 workload 后留存);dispatch
循环增加 session 堆高水位采样,VM 暴露 session_bytes_peak 与
collect_session_gc。新增 8 个 mem_* 压力用例(对象图/字符串/闭包链/数组
增删/跨调用保留/Map/Set 保留/字符串拼接),基线扩至 76 条目。
This commit is contained in:
root
2026-09-10 08:18:52 +00:00
parent 31d3f59d2b
commit 1b87e212f9
20 changed files with 1359 additions and 697 deletions
+1 -1
View File
@@ -79,7 +79,7 @@ pub fn compare_baseline(current: &[MetricCollection]) -> Vec<Regression> {
let tolerance = match name {
"wall_time_us" => 0.10,
"gc_trigger_count" | "gc_bytes_freed" | "gc_objects_scanned" | "gc_collection_us" => 0.50,
"session_bytes" => 0.20,
"session_bytes" | "peak_bytes" | "retained_bytes" | "retained_objects" => 0.20,
_ => 0.20,
};
let ratio = cur_val / base_val;
+45 -11
View File
@@ -91,25 +91,53 @@ pub fn run_js_stress_bench(config: &BenchConfig, kernel: &Arc<KernelCore>, pool:
let _result = vm.run(&module);
let exec_time = exec_start.elapsed();
// 执行期指标须在强制 GC 前读取:GC 会搬移/清扫 session 对象,
// 其后读到的计数是存活集而非本执行期的累计量
let gc_stats = vm.session_gc_stats();
let gc_trigger_count = gc_stats.total_collections;
let gc_bytes_freed = gc_stats.last_collection_bytes_freed;
let gc_objects_scanned = gc_stats.last_collection_objects_scanned;
let gc_collection_us = gc_stats.last_collection_duration_us;
let session_objects = vm.session_object_count().saturating_sub(pre_session);
let session_bytes = vm.session_bytes_allocated();
let epoch_objects = vm.epoch_object_count().saturating_sub(pre_epoch);
let instruction_count = vm.instruction_count();
let ic_hit_rate = vm.ic_hit_rate();
let ic_hits = vm.ic_hit_count();
let ic_misses = vm.ic_miss_count();
// 留存口径:先让存活集对 session 完全可见——根直接持有的 epoch 对象
// (顶层 var 等)晋升进 session,session 对象持有的 epoch 子引用
// (闭包捕获、原生盒直插)同样晋升(均与 epoch 边界同机制);
// 再强制完整 GC 清掉不可达垃圾,清扫后账目即 workload 留存堆
vm.promote_rooted_epoch_objects();
vm.promote_session_epoch_refs();
vm.collect_session_gc();
let retained_bytes = vm.session_bytes_allocated();
let retained_objects = vm.session_object_count();
// 峰值高水位取执行期采样与 GC 后存活字节的较大者(存活集亦属高水位)
let peak_bytes = vm.session_bytes_peak().max(retained_bytes);
metrics.push(MetricCollection {
test_name: test_name.clone(),
wall_time_us: (compile_time + exec_time).as_micros() as u64,
session_objects: vm.session_object_count().saturating_sub(pre_session),
session_bytes: vm.session_bytes_allocated(),
epoch_objects: vm.epoch_object_count().saturating_sub(pre_epoch),
session_objects,
session_bytes,
epoch_objects,
epoch_bytes: 0,
gc_trigger_count: gc_stats.total_collections,
gc_bytes_freed: gc_stats.last_collection_bytes_freed,
gc_objects_scanned: gc_stats.last_collection_objects_scanned,
gc_collection_us: gc_stats.last_collection_duration_us,
instruction_count: vm.instruction_count(),
gc_trigger_count,
gc_bytes_freed,
gc_objects_scanned,
gc_collection_us,
instruction_count,
compile_time_us: compile_time.as_micros() as u64,
exec_time_us: exec_time.as_micros() as u64,
ic_hit_rate: vm.ic_hit_rate(),
ic_hits: vm.ic_hit_count(),
ic_misses: vm.ic_miss_count(),
ic_hit_rate,
ic_hits,
ic_misses,
peak_bytes: peak_bytes as u64,
retained_bytes: retained_bytes as u64,
retained_objects: retained_objects as u64,
});
}
@@ -170,6 +198,9 @@ fn average_metrics(metrics: &[MetricCollection]) -> MetricCollection {
ic_hit_rate: 0.0,
ic_hits: 0,
ic_misses: 0,
peak_bytes: 0,
retained_bytes: 0,
retained_objects: 0,
});
}
MetricCollection {
@@ -189,5 +220,8 @@ fn average_metrics(metrics: &[MetricCollection]) -> MetricCollection {
ic_hit_rate: metrics.iter().map(|m| m.ic_hit_rate).sum::<f64>() / n,
ic_hits: (metrics.iter().map(|m| m.ic_hits as f64).sum::<f64>() / n) as u64,
ic_misses: (metrics.iter().map(|m| m.ic_misses as f64).sum::<f64>() / n) as u64,
peak_bytes: (metrics.iter().map(|m| m.peak_bytes as f64).sum::<f64>() / n) as u64,
retained_bytes: (metrics.iter().map(|m| m.retained_bytes as f64).sum::<f64>() / n) as u64,
retained_objects: (metrics.iter().map(|m| m.retained_objects as f64).sum::<f64>() / n) as u64,
}
}
@@ -138,6 +138,9 @@ pub fn run_leak_detect(config: &BenchConfig, kernel: &Arc<KernelCore>, pool: &Ar
ic_hit_rate: vm.ic_hit_rate(),
ic_hits: vm.ic_hit_count(),
ic_misses: vm.ic_miss_count(),
peak_bytes: vm.session_bytes_peak() as u64,
retained_bytes: vm.session_bytes_allocated() as u64,
retained_objects: vm.session_object_count() as u64,
});
}
}
+12
View File
@@ -19,6 +19,15 @@ pub struct MetricCollection {
pub ic_hit_rate: f64,
pub ic_hits: u64,
pub ic_misses: u64,
/// 执行期 session 堆账目的峰值高水位(字节)。旧基线文件无此字段,反序列化补 0。
#[serde(default)]
pub peak_bytes: u64,
/// workload 结束并强制完整 GC 后的 session 存活堆字节(留存内存)。
#[serde(default)]
pub retained_bytes: u64,
/// workload 结束并强制完整 GC 后的 session 存活对象数。
#[serde(default)]
pub retained_objects: u64,
}
impl MetricCollection {
@@ -40,6 +49,9 @@ impl MetricCollection {
("ic_hit_rate", self.ic_hit_rate),
("ic_hits", self.ic_hits as f64),
("ic_misses", self.ic_misses as f64),
("peak_bytes", self.peak_bytes as f64),
("retained_bytes", self.retained_bytes as f64),
("retained_objects", self.retained_objects as f64),
]
}
}
+35 -4
View File
@@ -5,19 +5,47 @@ pub fn format_json(results: &[MetricCollection]) -> String {
serde_json::to_string_pretty(results).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
}
/// 内存基准用例(`mem_*` 前缀)的留存形态说明,随表格尾部输出。
const MEMORY_BENCH_NOTES: &str = "\n内存基准用例(mem_*):\n
mem_object_graph 大型对象图留存:15 层满二叉树 32767 节点整体驻留 session 堆
mem_string_accum 字符串累积留存:40000 个互不相同的字符串驻留数组(interner 压力)
mem_closure_chain 闭包/upvalue 长链:10000 层闭包各自捕获前层闭包与 payload 驻留
mem_array_grow 数组增缩容量:扩到 200000 元素后截回 1000,观测元素向量容量是否回落(账目按 Vec capacity 计)
mem_cross_call_retain 跨调用长存活数据:全局 store 经 30000 次调用累积 30000 条记录
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)。Ret B 为 workload 后
晋升 + 完整 GC 的真存活集。
";
/// 把结果格式化为对齐的文本表格(供终端打印)。
///
/// `Sess B`/`Peak B`/`Ret B` 为 session 堆账目(字节):执行期累计 / 峰值高水位 /
/// workload 后强制 GC 的存活留存。
pub fn format_text_table(results: &[MetricCollection]) -> String {
let mut out = String::new();
out.push_str(&format!(
"{:<35} {:>10} {:>10} {:>10} {:>12} {:>8} {:>8} {:>10} {:>10}\n",
"Test Name", "Wall(ms)", "Comp(ms)", "Exec(ms)", "Instrs", "GC Ct", "IC Rate", "Sess Objs", "Sess B",
"{:<35} {:>10} {:>10} {:>10} {:>12} {:>8} {:>8} {:>10} {:>10} {:>10} {:>10}\n",
"Test Name",
"Wall(ms)",
"Comp(ms)",
"Exec(ms)",
"Instrs",
"GC Ct",
"IC Rate",
"Sess Objs",
"Sess B",
"Peak B",
"Ret B",
));
out.push_str(&"-".repeat(120));
out.push_str(&"-".repeat(150));
out.push('\n');
for m in results {
out.push_str(&format!(
"{:<35} {:>10.2} {:>10.3} {:>10.3} {:>12} {:>8} {:>7.1}% {:>10} {:>10}",
"{:<35} {:>10.2} {:>10.3} {:>10.3} {:>12} {:>8} {:>7.1}% {:>10} {:>10} {:>10} {:>10}",
truncate(&m.test_name, 35),
m.wall_time_us as f64 / 1000.0,
m.compile_time_us as f64 / 1000.0,
@@ -27,9 +55,12 @@ pub fn format_text_table(results: &[MetricCollection]) -> String {
m.ic_hit_rate * 100.0,
m.session_objects,
m.session_bytes,
m.peak_bytes,
m.retained_bytes,
));
out.push('\n');
}
out.push_str(MEMORY_BENCH_NOTES);
out
}
+35 -1
View File
@@ -130,7 +130,7 @@ impl Vm {
/// 函数目标豁免均直落 epoch 值);Map/Set 等原生盒按键值直插 epoch 值。
/// `rewrite_object_values` 覆盖元素/meta/属性/proto/captured_this/home_object/
/// cell 值;克隆子树经 forwarding 去重,环与共享引用各克隆一次。
pub(crate) fn promote_session_epoch_refs(&mut self) {
pub fn promote_session_epoch_refs(&mut self) {
let objects = std::mem::take(&mut self.gc_state.session_object_ptrs);
if objects.is_empty() {
return;
@@ -185,6 +185,40 @@ impl Vm {
self.gc_state.session_object_ptrs.extend(objects);
}
/// 把根直接持有的 epoch 对象(顶层 var 寄存器、挂起句柄等)晋升进 session,
/// 并把根引用改写到克隆体。
///
/// # 副作用
/// - 每个根 epoch 对象深克隆进 session(含 native 状态盒,子引用递归晋升),
/// 账目计入克隆字节;根引用按转发表重写。
///
/// # 注意事项
/// - 供 workload 后观测点(基准留存测量)使用:本方法之后存活集完全对
/// session 可见,后续完整 GC 的留存账目不再漏"仅驻留 epoch arena 的对象"。
/// - 与 `promote_session_epoch_refs` 互补:本方法处理根直接持有的 epoch 对象,
/// 后者处理 session 对象持有的 epoch 子引用(闭包捕获等绕过写屏障的来源)。
pub fn promote_rooted_epoch_objects(&mut self) {
let mut epoch_roots = Vec::new();
self.for_each_root(|value| {
if value.is_object() {
let ptr = value.as_js_object_ptr();
if !ptr.is_null() {
// SAFETY: 根上的对象值均为 VM 自有存活 JsObject。
if unsafe { &*ptr }.is_epoch() {
epoch_roots.push(ptr);
}
}
}
});
let mut forwarding = std::mem::take(&mut self.gc_state.forwarding);
for ptr in epoch_roots {
self.promote_object_inner(ptr, &mut forwarding);
}
crate::session_gc::rewrite_vm_roots(self, &forwarding);
forwarding.clear();
self.gc_state.forwarding = forwarding;
}
/// 逃逸写屏障:写向全局/session 对象的对象值不指向 epoch,否则 epoch
/// 重置后悬垂。session 函数目标豁免晋升——克隆会令写入侧与字节码持有的
/// 原始对象分裂(类构造器在原型上续建方法会落到非克隆体);函数持有的
+1 -1
View File
@@ -915,7 +915,7 @@ fn rewrite_forwarded_value(
.unwrap_or(value)
}
fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>) {
pub(crate) 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));
+29 -3
View File
@@ -969,6 +969,26 @@ impl Vm {
self.gc_state.session_bytes_allocated
}
/// 执行期 session 堆账目的峰值高水位(顶层指令边界采样,全量重置清零)。
pub fn session_bytes_peak(&self) -> usize {
self.gc_state.session_bytes_peak
}
/// 无条件执行一次完整 session GC(mark + 移动式 sweep + 串/BigInt 清扫)。
///
/// # 副作用
/// - 存活对象复制进新 session arena,全部根与原生盒按转发表重写;
/// `session_bytes_allocated` 重置为清扫后的存活字节。
///
/// # 注意事项
/// - 须在执行外的安全点调用(无在途 builtin 局部裸指针、dispatch 未重入);
/// 执行期触发仍走水位路径,本入口供事后观测(如基准测 workload 后留存堆)。
pub fn collect_session_gc(&mut self) {
let mut session_gc = std::mem::take(&mut self.gc_state.session_gc);
session_gc.collect(self);
self.gc_state.session_gc = session_gc;
}
/// 当前 epoch 中已分配并跟踪的对象数量(未晋升到 session 的临时对象)。
pub fn epoch_object_count(&self) -> usize {
self.gc_state.epoch_object_ptrs.len()
@@ -1517,9 +1537,15 @@ impl Vm {
// inline 状态(非 GC 根),此时回收会把调用方 regs 中的活值当死值释放。
// 返回顶层后检查恢复,存活值此时已回拷为执行根。账目未超水位时仅
// 少量字段比较。
if self.native_call_depth == 0 && self.gc_state.session_bytes_allocated >= self.gc_state.string_gc_watermark
{
self.maybe_collect_session_strings();
if self.native_call_depth == 0 {
// 峰值高水位:同一边界采样 session 堆账目上界
let bytes = self.gc_state.session_bytes_allocated;
if bytes > self.gc_state.session_bytes_peak {
self.gc_state.session_bytes_peak = bytes;
}
if bytes >= self.gc_state.string_gc_watermark {
self.maybe_collect_session_strings();
}
}
if let Some(max_steps) = max_steps {
if steps > max_steps {
+3
View File
@@ -38,6 +38,9 @@ pub(crate) struct GcState {
/// `cell.value` 中的对象引用),只在 full_reset 统一释放。
pub(crate) session_cell_ptrs: RefCell<Vec<*mut UpvalueCell>>,
pub(crate) session_bytes_allocated: usize,
/// 执行期 session 堆账目的峰值高水位(`session_bytes_allocated` 的采样上界),
/// 顶层指令边界采样,全量重置清零。留存内存观测用。
pub(crate) session_bytes_peak: usize,
/// 执行期字符串 GC 的触发水位:本次收集后的存活字节 + 阈值增量。
/// 仅当账目超过水位才在指令边界触发回收——活串超阈值时不会每指令重复
/// 触发无死串可回收的白跑,且保证触发点恒在无 builtin 局部活值的边界。
+3
View File
@@ -100,6 +100,7 @@ impl Vm {
session_bigint_ptrs: std::cell::RefCell::new(Vec::new()),
session_cell_ptrs: std::cell::RefCell::new(Vec::new()),
session_bytes_allocated: 0,
session_bytes_peak: 0,
string_gc_watermark: gc_threshold,
gc_threshold_cached: gc_threshold,
gc_watermark: gc_threshold,
@@ -212,6 +213,7 @@ impl Vm {
session_bigint_ptrs: std::cell::RefCell::new(Vec::new()),
session_cell_ptrs: std::cell::RefCell::new(Vec::new()),
session_bytes_allocated: 0,
session_bytes_peak: 0,
string_gc_watermark: gc_threshold,
gc_threshold_cached: gc_threshold,
gc_watermark: gc_threshold,
@@ -329,6 +331,7 @@ impl Vm {
self.gc_state.epoch_object_ptrs.clear();
self.gc_state.session_epoch.reset();
self.gc_state.session_bytes_allocated = 0;
self.gc_state.session_bytes_peak = 0;
self.gc_state.string_gc_watermark = self.kernel_core.config().session_gc_threshold;
self.gc_state.session_gc = crate::session_gc::SessionGc::new();
self.symbols.reset();