# Monaco 打开卡顿的原因与修复(2026-10-16) 主要差异来自 JavaScript 引擎与浏览器运行时。Windows 上的 Edge、Chrome、VS Code/Electron 使用 V8;XFESystem 使用 Jint 4.11.0 解释执行 Monaco。NativeAOT 编译的是 C# 解释器,JavaScript 仍由解释器执行。当前内核实际执行一个 BSP,使用软件绘制,运行时配置 `gcConcurrent=0`。解释器中的重复搜索和大量临时分配会延长打开过程,也可能通过暂停式 GC 影响界面。 同一套 Jint 浏览器代码在 Windows 宿主中也能复现长文件初始化缓慢,因此不能仅归因于裸机 CPU 算力或显卡。Monaco 资源来自本地打包,禁用网络的原生对照也复现了耗时。这里没有换用简化编辑器,也没有修改官方 Monaco、网站 C# 语法或调色板。 ## 找到的热点 宿主 CPU 采样中,`RegExpInterpreter.ExecBacktrack`、复制内存和 GC 占据主要开销。进一步在原始 `RegExp.prototype.exec` 外安装仅用于诊断的计时器后,259 行 C# 样例中的同一个 4,240 字符正则被调用 256 次,累计约 2,202 ms。该正则已确认后续文本没有匹配,却在每个更靠后的下标重新搜索剩余文本。这类搜索可形成二次方重复工作。 另一个热点是 CSS 继承:每次全页排版都为每个后代复制整套 Monaco 主题自定义属性,并重新遍历、解析这些值。语法缓存命中时,短文件初始化同样产生约 231 MB 的临时分配;长文件约 374 MB。GC 与解释执行共享当前实际运行的 CPU,增加虚拟 CPU 数量不会自动并行执行这条调用链。 ## 改动 - Jint 私有副本增加失败后缀复用:同一 `JsRegExp` 实例、同一字符串引用、同一已编译正则、非粘连搜索,若某个下标到末尾已经无匹配,后续更靠后的搜索直接返回无匹配。保留外层 `lastIndex` 转换、返回值和其他可观察行为。不同文本、重新编译、回退下标、粘连匹配均不会误用该事实;异常、超时不缓存。每个实例至多保留一段不超过 128 KiB 的输入,不共享页面或引擎状态。 - CSS 自定义属性使用不可变共享环境;发生覆盖时才复制。普通属性、伪元素、动态主题和旧布局快照仍分别计算和保留。 - 添加布局和 DOM 预算计数、实际正则计时及命中数诊断,并记录每个宿主进程实际加载的 Jint SHA256 和补丁标记。 诊断中上述正则仍收到 256 次 JavaScript 调用,其中 255 次复用已证实的无匹配后缀;累计时间约 18 ms。其他正则继续使用原始匹配引擎。没有移除语法检查、执行预算或正则超时。 ## 测量 宿主使用真实字体、实际 Monaco 模型、着色 DOM 和绘制,每项运行五次,使用中位数;关闭 .NET 分层编译以降低阶段切换波动。短文件第一轮包含冷解析,后续使用已解析语法和新的独立引擎。长文件使用 259 行 C#。宿主数字不代表裸机性能。 | 测量 | 修改前 | 修改后 | | --- | ---: | ---: | | 宿主三行 C# 完整初始化中位数 | 564.84 ms | 405.13 ms | | 宿主 259 行 C# 完整初始化中位数 | 3,176.53 ms | 736.26 ms | | 宿主长文件初始化分配量,第一轮 | 374,076,008 B | 227,295,136 B | | 原生默认 JavaScript 编辑器冷打开完整首显 | 11.672 s | 8.391 s | | 原生重复打开完整首显中位数 | 7.906 s | 4.898 s | 原生使用两个已验证的 Desktop 单系统镜像、QEMU TCG、8 个虚拟 CPU / 实际 BSP 1、相同配置、实际键盘导航。每个镜像三轮,其中一轮冷打开、两轮重复打开;通过真实模型、实际字形和连续两帧像素稳定后计时。六帧逐像素对应一致,两个镜像的实际键盘编辑均通过。单次冷测量和两次重复打开不足以证明所有硬件上稳定达到同一倍率,完整打开仍需要数秒。 菜单输入探针与首显计时分开执行,记录键盘到实际呈现菜单的时间,包含 QMP 和串口轮询。修复版使用全局 Windows 键:初始化期间 234 ms,空闲对照 218 ms,差值 16 ms。单次探针不能作为流畅度、输入硬件延迟或 GC 暂停时间的证明。这里没有测量独立 GC 暂停时间,也不宣称彻底消除了所有停顿。 ## 验证与记录 448 项浏览器检查、40 项 Jint 补丁检查通过,覆盖真实 Monaco 初始化、实际编辑、预算限制、引擎隔离、CSS 和伪元素,以及正则下标、换文本、重新编译、粘连模式、捕获组、Unicode、超时和内存保留边界。新 Desktop 内核通过启动动画、Monaco 实际显示和编辑、桌面及终端验收;同一私有可写虚拟 USB 镜像的两次冷启动存储回归通过。493 个来宾绑定输入在验收时保持一致。 本轮按用户要求未写实体 U 盘,也没有生成新发行 ZIP。未重新验收的 Bing、完整文档工作流、实体启动、GPU、SMP 或长期稳定性不因本次结果取得资格。 宿主对照脚本的初次比较存在隔离错误:放在归档外的旧版探针继承了当前构建目标,导致修改前、修改后都加载新 Jint。该结果保留在 `HostComparison-1792095711332`,不能用于完整补丁的性能结论。最终探针和构建输出移入旧源码归档,重新执行;确认旧进程无失败后缀补丁、新进程有该补丁,且实际 Jint 哈希不同。 初次空闲菜单对照使用 Ctrl+Esc,已获得焦点的 Monaco 会接收该快捷键,导致测试等待菜单超时;当时编辑器仍持续呈现。该失败保留在 `IdleControl-1792095946154434100`。改用内核始终路由给桌面的 Windows 键释放后,初始化与空闲对照、实际模型及编辑检查通过;没有将原失败覆盖或归因于编辑器初始化未完成。 完整路径、摘要、原始样本和保留记录见 [机器可读证据](validation-results/MonacoStutter20261016.json)。原生完整首显比较的实际旧脚本也单独归档,防止后续增加空闲输入对照后混淆测试字节。 ## 对照 V8 查看了 V8 的 [正则执行入口](https://github.com/v8/v8/blob/main/src/regexp/regexp.cc)、[正则解释器](https://github.com/v8/v8/blob/main/src/regexp/regexp-interpreter.cc)、[正则分层编译说明](https://v8.dev/blog/regexp-tier-up)及 [JavaScript 基线编译说明](https://v8.dev/blog/sparkplug)。V8 有热点正则机器码和专门的解释器/扫描优化,JavaScript 也有机器码编译层。Jint 的 [自定义正则路径](https://github.com/sebastienros/jint/blob/v4.11.0/Jint/Native/RegExp/RegExpPrototype.cs)在部分 ECMAScript 语义无法交给 .NET Regex 时使用字节码回溯解释器。 本次实现针对实测重复工作进行优化,没有移植 V8 的 JIT,也没有把不兼容的正则强行交给 .NET Regex。剩余启动时间仍受 Jint 执行、完整 AST 构造、DOM 投影、字体及软件排版绘制影响。