206 lines
15 KiB
Markdown
206 lines
15 KiB
Markdown
# 机器人手臂3D查看器 · 性能优化报告
|
||||
|
|
|
|||
|
|
- 目标文件:`机器人手臂3D查看器.html`(15,314,130 → 15,316,745 字节)
|
|||
|
|
- 优化日期:2026-09-29
|
|||
|
|
- 优化前版本标识:`v1_2026-09-29_2151`(原件最后写入时间)
|
|||
|
|
- 备份:`backups/机器人手臂3D查看器.v1_2026-09-29_2151_优化前.html`
|
|||
|
|
- SHA-256(原件=备份,已校验一致):`1BDA568269842C403D0FB5D63B7610818BE2DDC28C3E7B9B90498349A3B1252E`
|
|||
|
|
- 优化后 SHA-256:`4F672CD8A66107D3EFDE6CD55FDC2C2CD878CBF64140F8497FB8CA3F2BC9B57D`
|
|||
|
|
- 回滚:`Copy-Item backups/机器人手臂3D查看器.v1_2026-09-29_2151_优化前.html 机器人手臂3D查看器.html`(原件为只读属性,已按原状恢复)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、摘要(实测数据)
|
|||
|
|
|
|||
|
|
同一台机器、同一个浏览器(Edge,160 Hz 显示器)、同一份插桩脚本,改前 / 改后各测 2 次:
|
|||
|
|
|
|||
|
|
| 指标 | 改前 | 改后 | 变化 |
|
|||
|
|
| --- | ---: | ---: | ---: |
|
|||
|
|
| **空闲 5 s 渲染次数** | 800 / 800 | **0 / 0** | **−100%** |
|
|||
|
|
| 空闲渲染 CPU 时长 | 271.4 / 289.9 ms | **0 / 0 ms** | −100% |
|
|||
|
|
| 单次滑块输入后 500 ms 渲染次数 | 82 / 82 | **1 / 1** | −98.8% |
|
|||
|
|
| 进入「运动模拟」后 4 s 渲染次数 | 626 / 617 | **103 / 104** | −83.5% |
|
|||
|
|
| 按钮求解后 4 s 渲染次数 | 626 / 622 | **103 / 102** | −83.6% |
|
|||
|
|
| 30 次 pointermove 的射线拾取次数 | 30 / 30 | **9 / 9** | −70% |
|
|||
|
|
| 30 次 pointermove 的拾取总耗时 | 13.1 / 19.3 ms | **5.2 / 4.4 ms** | −60% ~ −77% |
|
|||
|
|
| 索引缓冲内存 | 4,123,320 B(Uint32×92) | **2,061,660 B(Uint16×92)** | **−2.00 MiB(−50%)** |
|
|||
|
|
| draw call / 三角面 | 92 / 340,178 | 92 / 340,178 | 不变(画质不变) |
|
|||
|
|
| 约 20 s 会话总渲染次数 | 2576 / 2564 | **299 / 296** | −88.4% |
|
|||
|
|
| 约 20 s 会话渲染 CPU 总时长 | 1114 / 1267.9 ms | **143.9 / 205.6 ms** | −84% ~ −87% |
|
|||
|
|
| 首帧时间(导航→首帧) | 691 / 749 ms | 771 / 518 ms | 噪声内(中位数 720 → 645 ms) |
|
|||
|
|
| DOMContentLoaded | 685 / 745 ms | 766 / 512 ms | 噪声内 |
|
|||
|
|
|
|||
|
|
一句话:**空闲时不再空转满帧渲染(160 次/s → 0),交互时才渲染且每次状态变化恰好渲染 1 次**;拾取与几何内存同步下降;渲染输出(draw call、三角面、截图逐像素)与改前一致。
|
|||
|
|
|
|||
|
|
功能验证:改前 / 改后跑同一份 18.5 s 全项自检脚本,**每一项结果逐字相同**(含 92 零件、72 螺钉、BOM 高亮 18 颗与定位跳转、爆炸/线框/半透/包络开关、装配图↔三维切换、IK 求解残差 1.1e-13 / 1.8e-13 mm、姿态控制、右键隐藏与撤销)。截图对比:`scratch/perf/shots/before_0*.png` 与 `after_0*.png` 中模型视图、装配图、螺钉高亮三张**字节数完全相同**(357630 / 338438 / 347999),运动视图仅动画时序差 167 字节。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、改动清单
|
|||
|
|
|
|||
|
|
共 6 处代码改动(成品 HTML 内 19 个 diff 块),全部集中在查看器主逻辑与 IK glue,**未触碰任何数据行与求解器脚本**。
|
|||
|
|
|
|||
|
|
### 1. 按需渲染(核心)
|
|||
|
|
|
|||
|
|
- 问题:`loop()` 无条件每次 rAF 都 `resize() + updCam() + rend.render()`;`needRender` 变量被 `requestRender()` 置位后**全文件没有任何读取点**(改动前 grep 仅 3 处:声明、置位、注释),等于恒以显示器刷新率空转满帧渲染 34 万三角面 ×92 draw call,并且 `preserveDrawingBuffer:true` 会让每帧多一次缓冲拷贝。
|
|||
|
|
- 改法:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
function loop() {
|
|||
|
|
if (needRender && cv.style.display !== 'none') {
|
|||
|
|
needRender = false;
|
|||
|
|
resize();
|
|||
|
|
updCam();
|
|||
|
|
rend.render(scene, cam);
|
|||
|
|
}
|
|||
|
|
requestAnimationFrame(loop);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- 关键约束:**保留 `preserveDrawingBuffer:true`**(不重绘时靠它保住最后一帧画面);**保留 rAF 循环**(入场动画 `animTo`、实时追踪、拖拽都依赖后续帧渲染);装配图页画布隐藏时**不清标志**,切回三维页由 tab 处理器 `resize()+requestRender()` 补渲染。
|
|||
|
|
- 安全性核对:改动前逐一核对全部渲染状态变更点均已调用 `requestRender()`(查看器主逻辑 36 处;glue 的 `refreshTarget()` / `setJoints()` / `highlightSpec()` / `setView()` 内部均调用),窗口尺寸变化由 `ResizeObserver → resize() → requestRender()` 覆盖。
|
|||
|
|
- 顺带收益:`resize()` 读 `stage.clientWidth/clientHeight` 会造成强制布局,旧版每帧都读,现在只在真正渲染前读一次。
|
|||
|
|
|
|||
|
|
### 2. 悬浮拾取限流 + 零分配
|
|||
|
|
|
|||
|
|
- 问题:射线要对 34 万三角面全量求交(实测 0.5~1.5 ms/次),旧版每个 `pointermove` 都算(rAF 合帧后仍≈刷新率次数);且每次 `meshes.filter(...)` 都新建数组 + `new THREE.Vector2`。
|
|||
|
|
- 改法:`schedHover()` 加 **60 ms 时间配额 + 位置去重**(指针静止时一次都不算,尾随仍补最新位置);`pick()` 复用模块级 `pickList` 数组与 `ndcPick` 向量,可见性仍逐次重算。
|
|||
|
|
- 注意:three r159 的 `Raycaster` **不检查 `object.visible`**(已在 `web/three_inline.js` 源码确认),因此可见性过滤必须保留。
|
|||
|
|
- 点击 / 右键拾取不经过 `schedHover`,仍是即时求交,交互判定不受影响。
|
|||
|
|
|
|||
|
|
### 3. `applyJoint()` 去分配
|
|||
|
|
|
|||
|
|
- 问题:每次调用新建 2 个 `Vector3` + 3 个 `Matrix4`,且用 `JO.find` 线性查找;一次关节写入 = 6 次调用 = 30 个临时对象,动画/实时追踪每帧一批。
|
|||
|
|
- 改法:轴心 `JP` / 轴向 `JD` 预计算一次;矩阵合成改用 3 个共享 `Matrix4` 实例。数学与原式逐位相同(`T(P)·R(D,θ)·T(−P)`)——场景图残差复算结果与改前完全一致(1.1e-13 / 1.8e-13 mm)即为证据。
|
|||
|
|
|
|||
|
|
### 4. 索引缓冲降位(Uint16)
|
|||
|
|
|
|||
|
|
- 问题:92 件零件索引一律 `Uint32Array`。
|
|||
|
|
- 改法:按顶点数选择 `p.v.length <= 65535 ? Uint16Array : Uint32Array`(实测单件最大顶点 47,483,全部走 Uint16),索引内存 4.08 MB → 2.00 MB。
|
|||
|
|
|
|||
|
|
### 5. BOM 高亮只记一行
|
|||
|
|
|
|||
|
|
- 问题:点选 / ESC 时 `for (const tr of tb.children) tr.style.background = ''` 把整张表(13 行零件 + 10 行螺钉)刷一遍。
|
|||
|
|
- 改法:新增 `bomHlRow` / `bomHlClear()`,只清理上一次高亮的那一行,设置高亮时同步记录。可见结果完全一致。
|
|||
|
|
|
|||
|
|
### 6. Worker Blob URL 回收
|
|||
|
|
|
|||
|
|
- 问题:`new Worker(URL.createObjectURL(new Blob([...])))` 不回收,常驻一份 ~86 KB Blob;Worker 构造失败路径同样泄漏。
|
|||
|
|
- 改法:先取 URL,`try { new Worker(url) } finally { URL.revokeObjectURL(url) }`(URL 只在构造时被读取)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、验证方法与证据
|
|||
|
|
|
|||
|
|
### 3.1 静态校验(`scratch/perf/verify_edit.py`)
|
|||
|
|
|
|||
|
|
- 4 个内联 `<script>` 的 SHA-256 对比:
|
|||
|
|
- `[0] three.js`:**SAME**(9a4c1f2f…)
|
|||
|
|
- `[1] 数据+查看器`:DIFF(改动区)
|
|||
|
|
- `[2] IK 求解器`:**SAME**(49e969dc…)——指纹依赖的求解器源码一字未动
|
|||
|
|
- `[3] IK glue`:DIFF(仅第 6 项那一处)
|
|||
|
|
- 与备份逐行 diff:**19 个改动块,全部落在上述 6 项改动内**;`DATA` / `SHEETS` / `TRAVEL` / `COLLIDE` 数据行零改动。
|
|||
|
|
- `node --check`:改后的查看器主脚本(14.1 MB)与 glue 脚本均语法通过;模板补丁后的脚本同样通过。
|
|||
|
|
|
|||
|
|
### 3.2 运行期一致性
|
|||
|
|
|
|||
|
|
| 检查项 | 改前 | 改后 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| 预置自检指纹(`chkSig()` 当前值) | `3e03f22b` | **`3e03f22b`(未变)** |
|
|||
|
|
| IK 残差(闭式 / 场景图) | 1.1e-13 / 1.8e-13 mm | **1.1e-13 / 1.8e-13 mm** |
|
|||
|
|
| 零件 / 螺钉 / draw call / 三角面 | 92 / 72 / 92 / 340,178 | 相同 |
|
|||
|
|
| 关节矩阵元(J2 滑块 25°→夹到 7.867°) | 7.1441 | 7.1441 |
|
|||
|
|
|
|||
|
|
### 3.3 功能对照自检(`scratch/perf/func_check.js`,18.5 s 全项)
|
|||
|
|
|
|||
|
|
改前 / 改后逐项结果**完全相同**:首屏 92 网格 / 91 可见;零件树 30 行、BOM 13 行、螺钉表 10 行;J2 滑块(含动态限位夹取到 7.867°)与画面重绘;爆炸 60% 最大位移 129.6 mm 且「装配视图」回位;线框 91 开 / 0 关;电机半透 0.34;螺钉组 72/72 隐藏与恢复;可达包络显示;复位相机;悬浮 tooltip 命中 #11 小臂骨架;BOM S01 行点击→18 颗高亮、行 `.scrHl` 标记、HUD「第 1/18 处」→ 再点「第 2/18 处」→ ESC 全清;装配图↔三维切换且画布内容完好;运动模拟「已到达」、6 行关节表、6 个互异构型;自检面板文本;姿态控制(取当前 / 垂直向下 / ball,head,knob 拖拽把手);视图拾取模式;右键隐藏 1 件 + 撤销恢复;全选恢复 92 件。
|
|||
|
|
|
|||
|
|
截图:`scratch/perf/shots/`(before/after 各 4 张)。
|
|||
|
|
|
|||
|
|
### 3.3b 交互路径补充探针(`scratch/perf/interact_check.js`,改前 / 改后各跑 1 次)
|
|||
|
|
|
|||
|
|
针对"按需渲染是否会漏帧"这一风险点,逐条验证交互后的即时重绘(画布 64×36 降采样指纹对比):
|
|||
|
|
|
|||
|
|
| 交互 | 改前 | 改后 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| 轨道拖拽(pointerdown+10×pointermove) | Δaz −0.64、Δpo −0.213、画面重绘 ✓ | 完全相同 |
|
|||
|
|
| 滚轮缩放 | Δrd −159.9、画面重绘 ✓ | 完全相同 |
|
|||
|
|
| 爆炸滑块连续拖拽(7 次 input) | 最大偏移 226.8 mm、状态栏「爆炸 70%」、画面重绘 ✓ | 完全相同 |
|
|||
|
|
| 线框开关点击 | 91 件线框、画面重绘 ✓ | 完全相同 |
|
|||
|
|
| 关节滑块连续拖拽(4 次 input) | 见下方说明 | 完全相同 |
|
|||
|
|
| 目标球拖拽 | 见下方说明 | 完全相同 |
|
|||
|
|
|
|||
|
|
说明:表中"关节滑块 dAng=0"与"目标球 moved=0"两项在**改前改后结果逐字相同**,属合成事件测试的固有现象(合成 pointerdown 无活动指针,`setPointerCapture` 抛错中断了抓取判定;且当时手臂姿态经动态限位夹取后 J3 只允许 ±0.06°),**不是回归**——同一探针在改前文件上得到同一结果即为证据。真实鼠标操作不受此影响(`screenshot` 与 func_check 中的姿态控制、拖拽把手、视图拾取等均已通过)。
|
|||
|
|
|
|||
|
|
### 3.3c URL 参数脚本化视图(`?view=motion&tgt=150,128,320&j1=20&expl=40&env=1&xray=0&hide=小臂`)
|
|||
|
|
|
|||
|
|
改前 / 改后返回的 12 项状态**逐字相同**:view=motion;tgt=[150,128,320];J1 经动态限位夹取到 19.09°;J2=0;运动模拟下自动回到装配状态(expl=0);可达包络已显示;电机半透已关(opacity 1);「小臂」相关 8 个网格全部隐藏;徽标「已到达」;画布/图纸显示状态正确。
|
|||
|
|
|
|||
|
|
### 3.4 性能插桩(`scratch/perf/harness.js` + `make_measure.py`)
|
|||
|
|
|
|||
|
|
插桩副本(`measure_before.html` / `measure_after.html`)只多一个尾部 `<script>`,不改变原有 4 个脚本索引,因此指纹逻辑不受影响。包装 `rend.render` 统计次数与 CPU 时长、包装 `hover` 统计拾取次数与耗时,探针覆盖空闲 / 滑块 / 动画 / 求解 / 悬浮扫过;结果同时输出到页面浮层、`document.title` 与 console,便于截图与读取。
|
|||
|
|
|
|||
|
|
环境:Edge(Chromium)+ `--headed`,`visible`、`rafHz≈160` 均已记录(若非前台标签页 rAF 会被节流,数据无效,故测量必须在可见窗口进行)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、经实测否决 / 未做的优化
|
|||
|
|
|
|||
|
|
| 候选 | 实测 | 结论 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `scene.updateMatrixWorld(true)` 改 6 级关节链更新 | 全量强制刷新仅 **0.004 ms/次**(≈110 对象) | 绝对值太小,不值得引入风险,保留原实现 |
|
|||
|
|
| `updXform()` / `updStat()` 优化(92 件零件的 reduce/filter/拷贝) | 微秒量级 | 不做 |
|
|||
|
|
| 材质合并 / 几何合并 | 只有 92 个 draw call,合并会破坏零件树勾选显隐、点选与单独高亮 | 不做 |
|
|||
|
|
| 分片 / 延迟建场景 | 初始化约 106 ms(JSON.parse 69 ms + 建几何 37 ms),分片不减少总时间 | 不做 |
|
|||
|
|
| 降低 `antialias` / `setPixelRatio` 上限 | 属画质取舍 | 保留原样 |
|
|||
|
|
| 实时追踪时对结果面板 DOM 节流 | `renderResult()` 含"场景图残差复算"与"备选构型"两块**验证语义**内容 | 保留原样,避免削弱验证可观测性 |
|
|||
|
|
| 按需渲染是否该去掉 `preserveDrawingBuffer` | 不重绘时画布会被清空 | 必须保留 `true` |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、重要发现与后续建议
|
|||
|
|
|
|||
|
|
### 5.1 预置自检指纹在**优化前就已经不匹配**(非本次改动引入)
|
|||
|
|
|
|||
|
|
原件打开「结构与精度自检」会显示:`预置结果已失效:预置指纹 47141160 ≠ 当前 3e03f22b(模型或求解器已变更)`。
|
|||
|
|
说明文件里的 `PRECOMPUTED_CHK`(生成时间戳 `2026-09-29 14:37`)是**更早状态**下算出的,而文件在 21:51 又被改动过。本次优化前后该值都是 `3e03f22b`,未使情况变化。
|
|||
|
|
|
|||
|
|
建议:点面板里的「重新计算」(本机分片实测,约 20–40 s),或按文件内注释所述用离线脚本重算,把新的 FK/IK/姿态/工作空间结果与指纹写回 `PRECOMPUTED_CHK`。
|
|||
|
|
|
|||
|
|
### 5.2 生成器源文件与成品已漂移(**在完成反向同步前不要重跑构建**)
|
|||
|
|
|
|||
|
|
`src/build_viewer.py` 由 `src/viewer_tpl.html` + `web/ik-glue.js` + `web/arm-ik-solver.js` + `web/mesh_arm.json` 拼装成品,但成品比源文件新:**Worker 化求解、`PRECOMPUTED_CHK` 预置自检、`refreshJointLabelsLazy` 节流、±360° 滑块**等只存在于成品里(`web/ik-glue.js` 仍是同步求解的旧版,776 行 vs 成品 961 行)。
|
|||
|
|
|
|||
|
|
本次处理:按约定**直接优化成品**,并把性能补丁同步回 `src/viewer_tpl.html`(补丁前副本:`backups/viewer_tpl.html.v1_2026-09-29_性能补丁前.bak`)。`web/ik-glue.js` 中不存在本次改动的对应代码(无 Worker、无拾取/渲染循环/几何构建),故无需同步。
|
|||
|
|
|
|||
|
|
**在把成品里的新代码反向同步回模板/glue 之前,重跑 `python src/arm_model.py` 或 `build_viewer.py` 会把成品回退到旧功能(并丢掉本次性能优化)。**
|
|||
|
|
|
|||
|
|
### 5.3 内存与泄漏审计结论
|
|||
|
|
|
|||
|
|
- 几何:`p.v / p.t / p.s` 在转成 typed array 后立即 `delete`,嵌套 JSON 数组可被回收(保留的只有 92 份 position/index 缓冲);索引缓冲本次减半。
|
|||
|
|
- 运行时 DOM:IK 结果表与备选构型每次求解重建,旧节点随闭包一起被 GC;`hidePart` 的「撤销」按钮用属性赋值而非 `addEventListener`,无监听器累积。
|
|||
|
|
- Worker:单例 + Blob URL 已回收;`IKWjob` 每次覆盖,无排队泄漏。
|
|||
|
|
- `performance.memory`(`usedJSHeapSize`)在本次环境波动较大(改前 145–291 MB,改后 264–421 MB,GC 时机主导),不作为结论性指标;确定性内存指标用索引缓冲字节数。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、复现方法
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
# 1) 本机静态服务器(把 /v.html /mb.html /ma.html 等别名映射到对应文件)
|
|||
|
|
python scratch/perf/serve.py 8123
|
|||
|
|
|
|||
|
|
# 2) 生成插桩副本(源 HTML → 尾部追加 harness.js)
|
|||
|
|
python scratch/perf/make_measure.py "机器人手臂3D查看器.html" "scratch/perf/measure_after.html" after
|
|||
|
|
|
|||
|
|
# 3) 浏览器打开 http://127.0.0.1:8123/ma.html ,约 25 s 后读取 #perfOut(或 document.title)
|
|||
|
|
# 注意:必须用可见窗口(headed),后台标签页的 rAF 会被节流导致数据无效
|
|||
|
|
|
|||
|
|
# 4) 静态校验(脚本哈希 + diff 范围)
|
|||
|
|
python scratch/perf/verify_edit.py
|
|||
|
|
|
|||
|
|
# 5) 功能对照自检:把 scratch/perf/func_check.js 用 base64 喂给浏览器 eval,
|
|||
|
|
# 约 19 s 后读取 window.__FC
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
测量脚本均在 `scratch/`(已被 `.gitignore` 忽略),成品文件保持干净、无任何插桩代码。
|