From 4c3ac707f79d010b54bb875cb5a65e9ad274c86a Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 11 Sep 2026 19:44:07 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=9B=B4=20session=20=E5=88=86=E9=85=8D?= =?UTF-8?q?=E7=AB=99=EF=BC=88=E5=87=BD=E6=95=B0=E5=AF=B9=E8=B1=A1/prototyp?= =?UTF-8?q?e=20=E5=AD=90=E5=AF=B9=E8=B1=A1/=E6=A0=87=E7=AD=BE=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E6=95=B0=E7=BB=84=EF=BC=89=E8=A1=A5=20session=20?= =?UTF-8?q?=E5=A0=86=E8=B4=A6=E7=9B=AE=EF=BC=8C=E9=85=8D=E5=8D=95=20run=20?= =?UTF-8?q?=E5=88=86=E9=85=8D=E5=8C=85=E7=BB=9C=E9=AB=98=E6=B0=B4=E4=BD=8D?= =?UTF-8?q?=20O(1)=20=E9=87=87=E6=A0=B7=E9=94=9A=E4=B8=8E=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E6=9C=9F=20churn=20=E5=B3=B0=E5=80=BC=E6=A0=A1?= =?UTF-8?q?=E5=87=86=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_cli/src/bench/leak_detect.rs | 85 +++++++++++++++++++++++ crates/oxide_cli/src/bench/mod.rs | 1 + crates/oxide_vm/src/vm.rs | 11 +++ crates/oxide_vm/src/vm_dispatch_misc.rs | 3 + crates/oxide_vm/src/vm_state.rs | 3 + crates/oxide_vm/src/vm_support.rs | 49 +++++++++++++ 6 files changed, 152 insertions(+) diff --git a/crates/oxide_cli/src/bench/leak_detect.rs b/crates/oxide_cli/src/bench/leak_detect.rs index 8f870607..d339435f 100644 --- a/crates/oxide_cli/src/bench/leak_detect.rs +++ b/crates/oxide_cli/src/bench/leak_detect.rs @@ -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) -> 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) -> 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 { diff --git a/crates/oxide_cli/src/bench/mod.rs b/crates/oxide_cli/src/bench/mod.rs index 80f2bf1e..d53aca88 100644 --- a/crates/oxide_cli/src/bench/mod.rs +++ b/crates/oxide_cli/src/bench/mod.rs @@ -42,6 +42,7 @@ pub fn run_benchmarks(config: BenchConfig, kernel: Arc, pool: Arc 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), }, _ => { diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 42392508..3edde3a3 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -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(); } diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index 0824ed8e..2dfca1ea 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -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::() + + crate::session_gc::SessionGc::object_heap_data_bytes(unsafe { &*ptr }) as usize; ptr }; let cooked = alloc_session_array(n); diff --git a/crates/oxide_vm/src/vm_state.rs b/crates/oxide_vm/src/vm_state.rs index cc126a79..687819c8 100644 --- a/crates/oxide_vm/src/vm_state.rs +++ b/crates/oxide_vm/src/vm_state.rs @@ -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 局部活值的边界。 diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 2a6380cb..1cb85e65 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -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::() + + 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::() + + 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::() + std::mem::size_of::() + 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::(), + "模板数组未计入账目: both={both_cost} fn={fn_cost}" + ); + } + #[test] fn immutables_cache_filled_once_per_module() { let mut vm = Vm::new();