mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
fix: 直 session 分配站(函数对象/prototype 子对象/标签模板数组)补 session 堆账目,配单 run 分配包络高水位 O(1) 采样锚与执行期 churn 峰值校准用例
This commit is contained in:
@@ -6,6 +6,7 @@ use std::time::Instant;
|
||||
use oxide_compiler::compiler::{compiled_module_hash, Compiler};
|
||||
use oxide_kernel::kernel::KernelCore;
|
||||
use oxide_parser::Allocator;
|
||||
use oxide_types::value::JsValue;
|
||||
use oxide_vm::vm::Vm;
|
||||
use oxide_vm::vm_pool::VmPool;
|
||||
|
||||
@@ -436,6 +437,90 @@ pub fn run_mem_pool_high_water(kernel: &Arc<KernelCore>) -> ExitCode {
|
||||
report_series("pool_high_water", "round", &series)
|
||||
}
|
||||
|
||||
/// 校准用例:单次 run(无 full_reset)跑 N 轮「闭包 + 对象 + 数组」churn
|
||||
/// 源,度量执行期死对象滞留的峰值。主锚 = in-engine 分配包络高水位
|
||||
/// (`run_alloc_peak`,dispatch 循环顶 O(1) 采样);次锚 = RSS 序列
|
||||
/// (块边界采样,单 run 内上升即 churn 本体,仅记录形态不做泄漏判定);
|
||||
/// 参考 = session/epoch 对象计数与留存账目。
|
||||
///
|
||||
/// # 注意事项
|
||||
/// 不走 VM 池、不接 full_reset:churn 峰值是 run 内量。源分 CHUNKS 块连续
|
||||
/// run(块间无 reset,arena/对象表/账目跨块累积,等价单次 run),RSS 在
|
||||
/// 块边界采样以获得序列形态。降幅验收走宿内 pre/post 对照(宿主安静
|
||||
/// 时点),跨宿主绝对比较禁止。
|
||||
pub fn run_mem_object_churn_peak(kernel: &Arc<KernelCore>) -> ExitCode {
|
||||
const N: usize = 500_000;
|
||||
const CHUNKS: usize = 8;
|
||||
const M: usize = N / CHUNKS;
|
||||
// 每轮对 t 的贡献 = f() + o.a + o.b.length = 2i + 2;Σ(i=0..M) = M² + M。
|
||||
const EXPECTED: u64 = (M as u64) * (M as u64) + (M as u64);
|
||||
|
||||
let js = format!(
|
||||
"var t = 0; for (var i = 0; i < {M}; i++) {{ var f = function() {{ return i; }}; var o = {{ a: i, b: [i, i + 1] }}; t += f() + o.a + o.b.length; }} t === {EXPECTED}"
|
||||
);
|
||||
let allocator = Allocator::default();
|
||||
let program = match oxide_parser::parse(&allocator, &js) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return ExitCode::FAILURE,
|
||||
};
|
||||
let hash = compiled_module_hash(&program);
|
||||
let module = match kernel
|
||||
.code_forge()
|
||||
.get_or_insert_with(hash, || Compiler::new().compile(&program))
|
||||
{
|
||||
Ok(m) => m,
|
||||
Err(_) => return ExitCode::FAILURE,
|
||||
};
|
||||
|
||||
let mut vm = Vm::with_kernel_core(Arc::clone(kernel));
|
||||
let mut series: Vec<(usize, f64)> = Vec::new();
|
||||
if let Some(kb) = read_vmrss_kb() {
|
||||
series.push((0, kb as f64));
|
||||
}
|
||||
for c in 0..CHUNKS {
|
||||
let result = match vm.run(&module) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("[object_churn_peak] chunk {c} run failed: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
if result != JsValue::bool(true) {
|
||||
eprintln!("[object_churn_peak] chunk {c} result mismatch: {result:?}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
if let Some(kb) = read_vmrss_kb() {
|
||||
series.push((c + 1, kb as f64));
|
||||
}
|
||||
}
|
||||
|
||||
let (slope, r2) = linreg(&series);
|
||||
let (rss_first, rss_last) = (series.first().map(|(_, v)| *v), series.last().map(|(_, v)| *v));
|
||||
eprintln!(
|
||||
"[object_churn_peak] N={} chunks={} run_alloc_peak={} bytes (主锚)",
|
||||
N,
|
||||
CHUNKS,
|
||||
vm.run_alloc_peak()
|
||||
);
|
||||
eprintln!(
|
||||
"[object_churn_peak] session_bytes_allocated={} session_bytes_peak={} session_objects={} epoch_objects={} gc_cycles={}",
|
||||
vm.session_bytes_allocated(),
|
||||
vm.session_bytes_peak(),
|
||||
vm.session_object_count(),
|
||||
vm.epoch_object_count(),
|
||||
vm.session_gc_stats().total_collections
|
||||
);
|
||||
eprintln!(
|
||||
"[object_churn_peak] rss_kb={}..{} (delta {:+.0}) slope={:.4} kB/chunk R²={:.4} (单 run 内上升 = churn 本体,不做泄漏判定)",
|
||||
rss_first.unwrap_or(0.0),
|
||||
rss_last.unwrap_or(0.0),
|
||||
rss_last.unwrap_or(0.0) - rss_first.unwrap_or(0.0),
|
||||
slope,
|
||||
r2
|
||||
);
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
/// 全序列回归报告:打印首末 RSS、全序列斜率/R² 与窗口(末 20 样本)总
|
||||
/// 增量;斜率显著为正(R² > 0.9)返回失败,表示泄漏签名。
|
||||
fn report_series(case: &str, unit: &str, series: &[(usize, f64)]) -> ExitCode {
|
||||
|
||||
@@ -42,6 +42,7 @@ pub fn run_benchmarks(config: BenchConfig, kernel: Arc<KernelCore>, pool: Arc<Vm
|
||||
Some("dirty_rebuild") => leak_detect::run_mem_dirty_rebuild_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),
|
||||
Some("object_churn_peak") => leak_detect::run_mem_object_churn_peak(&kernel),
|
||||
_ => leak_detect::run_leak_detect(&config, &kernel, &pool),
|
||||
},
|
||||
_ => {
|
||||
|
||||
@@ -1002,6 +1002,12 @@ impl Vm {
|
||||
self.epoch.bump().allocated_bytes() + self.gc_state.session_epoch.allocated_bytes()
|
||||
}
|
||||
|
||||
/// 本 run 累计分配字节的高水位:`run_alloc_bytes` 的顶层指令边界
|
||||
/// 采样上界,run 边界(reset/full_reset)重起算。留存内存观测锚。
|
||||
pub fn run_alloc_peak(&self) -> usize {
|
||||
self.gc_state.run_alloc_peak
|
||||
}
|
||||
|
||||
/// 本 run 累计分配字节:epoch arena + session 对象 arena + session 手工堆
|
||||
/// 账目(session 串 + session 对象及其属性向量 + GC 后补回的 BigInt)。
|
||||
/// 单次 run 内单调不减(执行期对象不回收、
|
||||
@@ -1635,6 +1641,11 @@ impl Vm {
|
||||
if bytes > self.gc_state.session_bytes_peak {
|
||||
self.gc_state.session_bytes_peak = bytes;
|
||||
}
|
||||
// 单 run 分配包络高水位:O(1) 三计数器读(双 arena + session 账目)。
|
||||
let alloc = self.run_alloc_bytes();
|
||||
if alloc > self.gc_state.run_alloc_peak {
|
||||
self.gc_state.run_alloc_peak = alloc;
|
||||
}
|
||||
if bytes >= self.gc_state.string_gc_watermark {
|
||||
self.maybe_collect_session_strings();
|
||||
}
|
||||
|
||||
@@ -1062,6 +1062,9 @@ impl Vm {
|
||||
clone.set_session_epoch(true);
|
||||
let ptr = self.gc_state.session_epoch.alloc(clone) as *mut JsObject;
|
||||
self.gc_state.session_object_ptrs.push(ptr);
|
||||
// 直 session 分配计入堆账目(与 promote 同式:对象头 + 对象堆数据)。
|
||||
self.gc_state.session_bytes_allocated += std::mem::size_of::<JsObject>()
|
||||
+ crate::session_gc::SessionGc::object_heap_data_bytes(unsafe { &*ptr }) as usize;
|
||||
ptr
|
||||
};
|
||||
let cooked = alloc_session_array(n);
|
||||
|
||||
@@ -41,6 +41,9 @@ pub(crate) struct GcState {
|
||||
/// 执行期 session 堆账目的峰值高水位(`session_bytes_allocated` 的采样上界),
|
||||
/// 顶层指令边界采样,全量重置清零。留存内存观测用。
|
||||
pub(crate) session_bytes_peak: usize,
|
||||
/// 单 run 分配包络(`run_alloc_bytes`)的高水位:顶层指令边界采样,
|
||||
/// run 边界(reset/full_reset)重起算。留存内存观测锚(churn 峰值主锚)。
|
||||
pub(crate) run_alloc_peak: usize,
|
||||
/// 执行期字符串 GC 的触发水位:本次收集后的存活字节 + 阈值增量。
|
||||
/// 仅当账目超过水位才在指令边界触发回收——活串超阈值时不会每指令重复
|
||||
/// 触发无死串可回收的白跑,且保证触发点恒在无 builtin 局部活值的边界。
|
||||
|
||||
@@ -114,6 +114,7 @@ impl Vm {
|
||||
session_cell_ptrs: std::cell::RefCell::new(Vec::new()),
|
||||
session_bytes_allocated: 0,
|
||||
session_bytes_peak: 0,
|
||||
run_alloc_peak: 0,
|
||||
string_gc_watermark: gc_threshold,
|
||||
gc_threshold_cached: gc_threshold,
|
||||
gc_watermark: gc_threshold,
|
||||
@@ -228,6 +229,7 @@ impl Vm {
|
||||
session_cell_ptrs: std::cell::RefCell::new(Vec::new()),
|
||||
session_bytes_allocated: 0,
|
||||
session_bytes_peak: 0,
|
||||
run_alloc_peak: 0,
|
||||
string_gc_watermark: gc_threshold,
|
||||
gc_threshold_cached: gc_threshold,
|
||||
gc_watermark: gc_threshold,
|
||||
@@ -348,6 +350,7 @@ impl Vm {
|
||||
self.gc_state.session_epoch = bumpalo::Bump::new();
|
||||
self.gc_state.session_bytes_allocated = 0;
|
||||
self.gc_state.session_bytes_peak = 0;
|
||||
self.gc_state.run_alloc_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();
|
||||
@@ -499,6 +502,8 @@ impl Vm {
|
||||
self.free_epoch_object_heap_data();
|
||||
self.epoch.reset();
|
||||
self.gc_state.epoch_object_ptrs.clear();
|
||||
// 单 run 分配包络按 run 边界重起算(与 run_alloc_bytes 起算口径同源)。
|
||||
self.gc_state.run_alloc_peak = 0;
|
||||
self.root_reg_limit = 0;
|
||||
self.active_reg_limit = 0;
|
||||
}
|
||||
@@ -668,6 +673,9 @@ impl Vm {
|
||||
obj.set_session_epoch(true);
|
||||
let obj_ptr = self.gc_state.session_epoch.alloc(obj) as *mut JsObject;
|
||||
self.gc_state.session_object_ptrs.push(obj_ptr);
|
||||
// 直 session 分配计入堆账目(与 promote 同式:对象头 + 对象堆数据)。
|
||||
self.gc_state.session_bytes_allocated += std::mem::size_of::<JsObject>()
|
||||
+ crate::session_gc::SessionGc::object_heap_data_bytes(unsafe { &*obj_ptr }) as usize;
|
||||
let func_val = JsValue::object(obj_ptr as *mut u8);
|
||||
|
||||
if !is_arrow {
|
||||
@@ -686,6 +694,9 @@ impl Vm {
|
||||
prototype.set_session_epoch(true);
|
||||
let prototype_obj = self.gc_state.session_epoch.alloc(prototype) as *mut JsObject;
|
||||
self.gc_state.session_object_ptrs.push(prototype_obj);
|
||||
// 直 session 分配计入堆账目(与 promote 同式:对象头 + 对象堆数据)。
|
||||
self.gc_state.session_bytes_allocated += std::mem::size_of::<JsObject>()
|
||||
+ crate::session_gc::SessionGc::object_heap_data_bytes(unsafe { &*prototype_obj }) as usize;
|
||||
let prototype_val = JsValue::from_js_object(prototype_obj);
|
||||
|
||||
if !is_generator {
|
||||
@@ -1220,6 +1231,44 @@ mod tests {
|
||||
assert_eq!(vm.gc_state.session_epoch.allocated_bytes(), 0);
|
||||
}
|
||||
|
||||
/// 直 session 分配站(函数对象 + prototype 子对象)计入 session 堆账目:
|
||||
/// 分配点计数 = 对象头(属性区容量扩张属 promote 同口径的既有盲区,不钉)。
|
||||
/// 另含 CREATE_CLOSURE 站将推断函数名物化为 session 串(name 数据属性,
|
||||
/// 既有行为),钉值一并计入。
|
||||
#[test]
|
||||
fn direct_session_alloc_counted_in_session_bytes() {
|
||||
let mut vm = Vm::new();
|
||||
let base = vm.session_bytes_allocated();
|
||||
run_source(&mut vm, "var f = function(){}; 0");
|
||||
let delta = vm.session_bytes_allocated() - base;
|
||||
let name_len = "f".len();
|
||||
assert_eq!(
|
||||
delta,
|
||||
2 * std::mem::size_of::<JsObject>() + std::mem::size_of::<JsString>() + name_len
|
||||
);
|
||||
}
|
||||
|
||||
/// 标签模板 cooked/raw 数组直 session 分配计入 session 堆账目:
|
||||
/// 含模板的 run 账目增量须超出同等函数对象口径(两数组各含对象头 + 元素区)。
|
||||
/// 单 run 内完成(标签函数定义 + 模板调用):跨 run 调用异模块函数受
|
||||
/// sub_module_index 缺口限制,不在本钉面。
|
||||
#[test]
|
||||
fn tagged_template_object_counted_in_session_bytes() {
|
||||
let mut vm = Vm::new();
|
||||
let base = vm.session_bytes_allocated();
|
||||
// 对照 run:仅函数对象(本体 + prototype 子对象)。
|
||||
run_source(&mut vm, "var tag = function(){ return 0; }; 0");
|
||||
let fn_cost = vm.session_bytes_allocated() - base;
|
||||
// 实验 run:同函数 + 标签模板(cookeD/raw 两数组)。
|
||||
let before = vm.session_bytes_allocated();
|
||||
run_source(&mut vm, "var tag2 = function(){ return 0; }; tag2`a${1}b`; 0");
|
||||
let both_cost = vm.session_bytes_allocated() - before;
|
||||
assert!(
|
||||
both_cost > fn_cost + 2 * std::mem::size_of::<JsObject>(),
|
||||
"模板数组未计入账目: both={both_cost} fn={fn_cost}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn immutables_cache_filled_once_per_module() {
|
||||
let mut vm = Vm::new();
|
||||
|
||||
Reference in New Issue
Block a user