15 KiB
机器人手臂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(原件=备份,已校验一致):
- 优化后 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会让每帧多一次缓冲拷贝。 - 改法:
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 时机主导),不作为结论性指标;确定性内存指标用索引缓冲字节数。
六、复现方法
# 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 忽略),成品文件保持干净、无任何插桩代码。