10 KiB
Web/WASM 模块化增量调试方案
结论
可以按项目模块拆分 WASM,采用增量方式调试和发布:已经完成的模块在浏览器会话中保持加载,新模块单独编译、单独加载和单独回放,浏览器页面不关闭。生产版也保持按需动态加载,不要求把各模块 WASM 合成为一个文件。
关键限制是:不能把多个已经包含独立 FreeCAD/Python/Qt/OCCT 运行时的 .wasm 文件直接二进制拼接。每个模块应隔离在自己的 Worker 中,并由页面按需加载和回收;不再设置必须合成单体 WASM 的阶段。
推荐架构
模块拆分服从 OCCT_GEOMETRY_BOUNDARY.md:现成 OCCT WASM 为唯一几何内核出口,Three.js 只显示原生结果。下图描述当前模块隔离与路由,不代表已经实现单一 OCCT 运行实例或完成全部几何入口统一;不能以模块中包含 OCCT 静态依赖代替逐条实际调用确认。
React 页面(保持不关闭)
|
ModuleRegistry / fc_web_dispatch_json
|
+-------+----------------+----------------+
| | | |
Core Part Worker BIM Worker Fem Worker
WASM PartProbe.wasm BimProbe.wasm FemProbe.wasm
| | |
+------ 统一 JSON envelope ------+
- 页面只维护模块注册表、加载状态和统一命令路由。
- 每个模块使用独立 Worker,Worker 内只加载该模块的 WASM/JS/data。
- 所有调用仍使用
{cmd,args}→ 结果 JSON;模块名通过已注册 command 前缀确定。 - 已完成模块的 Worker 和 WASM 保持存活,新模块只新增或替换自己的 Worker。
- 模块替换时只终止旧模块 Worker,页面、React 状态和其它模块不重载。
三种构建形态
A. 开发期独立 probe,推荐默认
每个模块生成自己的 FreeCADWeb<Module>Probe.{js,wasm,data}。它拥有自己的初始化和内存,适合快速增量编译、隔离失败和并行 gate。浏览器通过 Worker 按需加载,已完成模块不重复初始化。
适用:Part、PartDesign、Sketcher、TechDraw、BIM、Fem 等模块的连续移植。
B. 共享主运行时 + side modules
主 WASM 提供 FreeCAD/Python/OCCT/Qt 基础运行时,模块以 Emscripten side module 形式加载。这样可以共享内存和 C++ 符号,但要求统一 ABI、导出表、异常模型、文件系统和生命周期;Python 扩展、Qt 类型注册和全局单例必须特别处理。
适用:运行时已经稳定、需要减少重复下载和内存占用的阶段。不建议作为第一阶段的增量调试形态。
C. 生产期按需动态模块
保留独立的模块 WASM。页面启动 Core 或首屏所需模块;收到命令时才创建对应 Worker 并加载其 runtime/WASM/data。模块 artifact 使用 revision/hash 标识,命中缓存时复用现有 Worker,版本变化时只替换该模块。
适用:发布、增量更新、缓存隔离、低内存设备和持续 Full Parity。
浏览器常驻调试流程
- 页面启动 Core/已完成模块 Worker,并将每个模块状态记为
ready。 - 调度器只构建当前模块;构建完成后复制 JS/WASM/data 到带版本 hash 的 staging 目录。
- 页面收到
module:update,仅终止该模块 Worker,加载新 artifact,执行 ready/type/resource smoke。 - focused gate 通过后,将新 Worker 状态切换为
ready;其它 Worker、页面和用户文档状态保持不变。 - 失败时只回滚该模块 artifact 和 Worker,保留其它模块的运行状态与证据。
- 所有模块稳定后,继续使用同一份模块 manifest 做全量验证;不要求单体链接。
浏览器不关闭并不等于 WASM 实例必须永远不重启。应保持页面和其它模块不变,只替换当前模块 Worker,避免旧线性内存、线程池和 Python interpreter 泄漏。
缓存和版本规则
- artifact 路径必须包含
module、build revision和content hash。 ModuleRegistry记录 loader、wasm、data、ABI schema 和 evidence hash。- 已加载且 hash 未变的模块直接复用;hash 变化只重载该模块。
- 同一模块的旧 Worker 必须先完成
reset/resourcesReleased,再释放并替换。 - 不允许用旧模块的 JS loader 配新模块 WASM,或混用不同 revision 的
.data。
统一 dispatcher 边界
开发期和最终版都保持同一个公开 ABI:
{
"cmd": "bim.pythonFamilyBatch",
"args": {
"tier": "tier-1-readonly",
"records": []
}
}
模块差异只存在于 command registry、artifact 和 native implementation;React、Facade、Worker、取消、重复、reset、错误和资源释放语义不分叉。这样每个独立 WASM 都可以使用同一套 gate。
模块发布顺序
- 冻结每个模块的 command schema、source revision、artifact hash 和资源释放证据。
- 将模块 runtime、WASM、data 和资源复制到带 revision/hash 的发布目录。
- 更新模块 manifest;页面只在命令首次使用时加载对应 Worker 和 artifact。
- 版本变化时发送
module:update,只替换目标模块 Worker;其他模块和页面状态不变。 - 先做增量 smoke,再由用户明确触发全量 API、Full Parity、Chrome、性能和安全验证。
不应采用的做法
- 直接拼接多个
.wasm二进制文件。 - 在每个模块中复制一份公开 JS API 或独立 dispatcher ABI。
- 为了保持浏览器不关闭而长期复用已经超时或资源未释放的 Worker。
- 在独立 probe 阶段把模块状态直接写成全局 Full Parity 完成。
- 让模块 gate 读取或重建其它模块的完整源码、inventory 或历史日志。
对当前项目的落地建议
当前项目已经具备按模块拆分的 probe、独立 Worker、统一 fc_web_dispatch_json 和批次 gate,继续复用 A 形态推进。2026-09-09 核对发布清单:32 个模块条目引用 34 个独立 WASM,引用文件均存在;这只是产物盘点,不是全部业务完成。动态加载基础设施已接入 WebModuleRouter,现状与剩余边界如下:
- 现有 generated registry 和
web/public/modules/manifest.json已记录路由、Worker、产物及 revision/hash,不重新建设注册表。 web/src/module-loader.ts、module-router.ts已支持指定产物、首次命令按需建 Worker、相同 revision 复用、setArtifact仅替换目标模块。web/public/module-preload.js提供可选字节缓存预取,支持模块顶层和分 Worker 地址;同 URL 去重,最多 2 个并发下载,不自动初始化全部模块。- 自动工作台预测预取、单独 WASM 编译/初始化耗时、带版本 URL 的完整发布回滚仍需按实际业务补齐,不把现有测试预取入口宣称为这些功能全部完成。
这条路线能缩短单次增量调试时间;按需加载还可以避免未使用模块占用浏览器内存,并支持单模块回滚和独立缓存。
React 接入示例
import { WebModuleRouter } from "./module-router.js";
const router = new WebModuleRouter();
router.setArtifact("TechDraw", {
revision: "techdraw-2026-09-04-a1b2c3d4",
runtimeUrl: "/modules/TechDraw/techdraw-runtime.js",
wasmUrl: "/modules/TechDraw/techdraw-runtime.wasm",
dataUrl: "/modules/TechDraw/techdraw-runtime.data",
});
// The first call creates only the TechDraw worker. The React page stays mounted.
const response = await router.request("TechDraw", "techdraw.createRecompute", payload);
// A new build replaces only TechDraw. Other module workers remain alive.
router.setArtifact("TechDraw", nextArtifact);
The bootstrap rewrites /kernel/*.js, /kernel/*.wasm and /kernel/*.data requests inside the selected Worker. Existing workers without an artifact override continue using the checked-in /kernel assets.
For deployments with many modules, keep artifact metadata outside the React bundle:
{
"schema": "web-module-manifest/v1",
"modules": {
"TechDraw": {
"revision": "techdraw-2026-09-04-a1b2c3d4",
"runtimeUrl": "/modules/TechDraw/runtime.js",
"wasmUrl": "/modules/TechDraw/runtime.wasm",
"dataUrl": "/modules/TechDraw/runtime.data"
}
}
}
loadWebModuleManifest() validates this generic shape before registration. The manifest may be served independently, so adding a module does not require rebuilding the page shell.
python3 tools/build_web_module_artifacts.py --dry-run reports the existing active-module selection without compiling. The default command builds only that selection; --target <actual probe target> selects the affected runtime explicitly. Use the command-to-Worker manifest mapping, not only the module name: Part document persistence uses FreeCADWebDocumentProbe, while Part geometry uses FreeCADWebPartBoxProbe. Unchanged artifacts need no rebuild. --all is an explicit release/CI sweep, not the normal migration loop. Build results include each exit code and duration in docs/current/web-module-build.json; synchronize only affected assets through the existing pipeline.
The existing React workbench accepts the same manifest without code changes:
?moduleManifest=/modules/manifest.json&preload=Part,TechDraw. moduleManifest only registers artifacts; the optional comma-separated preload list warms those workers. All other modules stay unloaded until their first command.
The workbench route is /workbench.html; the default project workspace has its own document session. The shared page preloader only downloads the selected asset bytes into the HTTP cache. WebModuleRouter.preload() starts the selected command worker and awaits its ready event; that event is not proof that every worker has compiled or initialized its native runtime. Keep download, worker-ready and first native-command timings separate. Independent WASM instances do not share native pointers; preserve the existing authoritative document or use the established serialized transfer path.
Chrome verification uses the same policy through tools/run_p6_chrome_harness.mjs: set P6_CHROME_PRELOAD_MODULES=Part,TechDraw to prefetch the selected modules' WASM/data into the browser cache before the scenario begins. P6_CHROME_PRELOAD_ASSETS=wasm,data,runtime can add runtime JavaScript, and P6_CHROME_PRELOAD_CONCURRENCY is capped at two concurrent downloads. The harness serves /kernel and /modules with a cacheable response so the subsequent Worker can reuse those bytes. The incremental module under test is omitted from this list and remains dynamically loaded by its command worker. Preload results, byte count and failures are recorded as modulePreload in the Chrome evidence JSON.