浏览器与编辑器的内存生命周期
持续运行验收使用真正的来宾编辑器、JavaScript、布局与 framebuffer。宿主 GC 测试、短时启动成功或显示源文件首帧,均不能代替反复编辑和预览的来宾验收。实际镜像、源码快照、耗时和失败记录以对应的 Validation 报告为准。
DOM 身份与释放
JavaScript 包装器可能反向引用 DOM 节点。用两个强引用字典保存节点与包装器会让已经脱离文档的节点永久存活;普通弱键加上强值也不能解决这个循环。
DependentIdentityMap 使用 System.Runtime.DependentHandle 保存条件依赖关系。只有键仍被其他对象引用时,其包装器值才继续存活;脚本持有的分离节点仍保留对象身份。哈希桶不保存强键,清理死亡条目和页面释放时显式调用 Dispose。原生运行时当前没有完整 finalizer 支持,因此不能依赖终结器归还句柄。
页面释放同时清理 DOM 桥、事件循环、样式、资源队列和布局引用。关闭窗口不等于可以在 UI 线程释放后台引擎:后台 Monaco 始终由其 worker 独占。桌面发送取消并保留待释放操作,队列忙时继续重试,直到 worker 确认完成释放。准备结果的发布、接收和取消通过同一个原子状态协调,防止关闭后遗留已发布但尚未接收的页面。
原生运行时检查
桌面启动前执行五项原生弱引用检查:死亡目标清除、活键保留值、无根循环回收、多级依赖固定点,以及显式释放句柄。检查失败时不启用桌面。它与既有线程、异常恢复和 GC 自检共同构成启动门槛,不能单独证明浏览器长期稳定。
启动门槛还包括八项冻结对象区间检查、六项显式弱集合检查,以及真实 Jint WeakMap/WeakSet、对象形状缓存、私有字段/方法/访问器的执行和释放。解释器保留原有构造器、原型、参数校验与操作算法,只替换其私有弱存储。每个引擎直接持有独立 scope;周期清理与关闭时释放由同一个页面 owner 执行。这些容器不提供任意多线程并发访问保证。
显式存储覆盖 WeakMap/WeakSet,以及实际使用的 Engine _emptyShapes、_privateElementStore。形状缓存保留原回调及同键重入语义,私有字段字典使用具体构造工厂,不依赖 NativeAOT 反射。主构造器在交付 Engine 之前抛错时,通过显式 catch 释放已建立的 scope,再重抛原异常;不使用 finally。
修复前,真实 Monaco 的单个 Engine 已有 190 个未纳入释放的形状缓存条目。窗口关闭且后台已退休后,每轮仍增加约 441–442 个已分配的 dependent 槽。GC 清空死亡键和值,并不自动归还槽;普通 CWT 原本依赖当前运行时尚未支持的终结器。即使空闲 RAM 尚未持续下降,这也是资源泄漏,不能判为稳定。
该替换未扩展到禁用的 CLR interop 弱缓存、WeakRef 或完整 finalization 行为。原生基础检查与实际 Monaco 的有限长测,不能推广成所有 JavaScript 程序的弱引用和最终化语义已完整实现。
诊断必须区分物理空闲页、最大连续空闲区、托管堆跨度和最后一次 GC 的快照。空闲总量足够并不代表大数组能够分配。画面缓存的预算和 LRU 数量有界,也不代表持续创建大数组不会导致碎片化。
RawFreePages 和 RawLargestFreeRunPages 是各次读取时页分配器的即时统计,每页 4096 字节;RawTotalPages 是该分配器管理的区域,并非整台虚拟机的 RAM。LastGc* 是上次已完成 GC 的快照,Committed 也不是整个系统的占用。统计字段分次读取,不应建立严格的同一时点内存等式。DependentHandles 统计 dependent 槽,不能扩称所有原生句柄均已回收。
网页画面缓存保留 32 MiB、最多六项的原预算,使用最多 64 KiB 像素载荷的小数组保存完整画面。命中淘汰策略且尺寸相同的存储会复用,每次完整覆盖;仍有效的历史缓存不会被覆盖。页面和后台编辑器使用同一个画面身份分配器,防止两类会话的编号相撞。原生诊断报告块数、最大块载荷及复用次数,逐像素测试覆盖跨块行、裁剪和前景保护区。
CSS 解析直接扫描样式原文并跳过注释,只在选择器和声明等片段上进行规范化。早期缓存原始资源并不能消除解析时的重复副本:Bootstrap 样式每次去注释曾生成 232,724 字符的字符串,对应实际原生分配请求 465,470 字节。300 秒活动观察通过后,随后文件预览仍曾因这份副本超过连续空闲区而失败;因此长测必须同时包含后续重新打开和预览,不能仅观察同一个已打开页面。
基本编辑器的诊断也必须避免制造压力。EditorReadbackTracker 先比较窗口、路径、总长度和最多 8192 字符的 Span,只有可观察内容变化时才复制前缀、格式化输出;离开基本编辑器时清空缓存。不缓存窗口对象或完整大文件字符串。宿主回归分别验证大 C# / Markdown 内容在预热后一万次无变化读取为零分配,以及完整大字符串可以在 tracker 仍存活时回收;这两项是诊断路径检查,不代替大文件在来宾中的完整运行验收。
持续运行验收
Tools/CSharpBoot/TestBoot.py --editor-stability 在独立镜像和固件变量副本上运行,默认保持代码编辑器活动 300 秒,再执行 12 轮 C#、JavaScript 和 Markdown 的打开、修改、保存、预览、关闭及重新打开。模型内容来自实际 Monaco 回读;基本文本编辑器另有带窗口、路径、长度与截断标记的源文件回读。
测试保留串口、QMP 输入、DOM 证明、画面、物理页采样及镜像哈希。遇到 OOM、panic、心跳停顿或内容不一致即失败,并清理自己的 QEMU。缩短时长或减少轮数只用于调试;结果中的 LongIdleRequirementMet 和 FullLifecycleRequirementMet 必须分别满足,才完成这两项验收。有限时间的通过不承诺无限运行或 OOM 恢复。
每轮三种文件关闭后,还必须观察到该轮 C#/JS 的完整退休确认、仅剩前台 Files 窗口,以及之后两个连续且相同的原生槽计数。第一完整轮次建立固定暖基线,后续计数增长即失败,15 秒内无法取得稳定样本也失败。损坏的完整统计行会隔断连续性,不可静默跳过。该步骤不强制 GC、不暂停虚拟机、不注入输入;ClosedCycleHandleRequirementMet 只有完整至少 12 轮满足才为真。旧真实日志的逐轮增长用作离线负控。
GPU、CUDA 与首次完整预览的一秒目标分别验收。内存测试不把增加 RAM、跳过脚本、禁用编辑器定时器或宿主 GPU 工作视为来宾修复。
开发检查点与磁盘预算(2026-10-15)
当前提交是开发检查点,尚未完成新的 300 秒 / 12 轮原生稳定性验收,不能作为完整发行版。已完成 948 项宿主测试及最新镜像的 4K WHPX 启动检查;随后为处理宿主磁盘占用,停止了正在运行的 TCG 检查。中止的检查不计为通过。GPU/CUDA 和完整预览一秒目标仍未通过,U 盘尚未更新。
新版测试保留原始全局槽计数结果,并另行核对闭合窗口区间的页面资源和原生所有权。只有精确对应活线程 TLS 字段、相同泛型 ArrayPool 实例及其 CWT 槽的首次初始化,才允许归类为运行时资源;未知增长、墓碑增长、旧根替换均拒绝。该判定目前仅有离线检查,新版原生组合验收仍待执行。旧日志的增长数字只用于构造负控,不能宣称已重放丢失的原始日志。
宿主测试复用一个排他锁保护的 build/CSharp/HostArtifacts 编译缓存;每轮日志、TRX、源码哈希和结果继续独立保存。共享缓存可被后续构建修改,不是不可变验收二进制。QEMU 截图转为无损 PNG 并关闭图像句柄后,删除该次生成的 PPM;转换失败保留源图。宿主测试及原生测试在生成产物前检查剩余空间,低于 10 GiB 时拒绝启动。历史产物不会由这些脚本自动删除。