Files
Robot_Arm/docs/查看器性能优化报告.md
T

15 KiB
Raw Blame History

机器人手臂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 会让每帧多一次缓冲拷贝。
  • 改法:
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 忽略),成品文件保持干净、无任何插桩代码。