docs: 汇总打开大图纸的性能结论(截面对画之外) #107

Merged
gjm merged 1 commits from company-desktop/company-desktop-computer/doc-open-perf-summary AGit into main 2026-10-29 15:59:18 +08:00
+101
View File
@@ -0,0 +1,101 @@
# 打开大图纸性能优化:结论与现状
> 本文是这条线的收尾总结。过程细节见 `pipe-section-region-performance-plan.md`,
> 原始热点表见 `report-cpu-hotspots.md`(第一台机器)与分析脚本 `tools/perf_diagsession.py`。
> 测试基准:`E:\桌面\big1.dwg`(8 MB,约 1000+ 管道、管件、护栏、楼梯),
> AutoCAD 2020 / R230 Debug 构建,`tools/run_acad_perf.ps1` 自动开图取数。
## 1. 优化前后的数字
| 阶段 | 开图墙钟 | 截面恢复耗时 | 开图期回写 | 临时文件 |
| --- | ---: | ---: | ---: | ---: |
| 最初 | 113 s | 22.4 s | 1140 次 / 2.38 s | 6773 次 / 9.34 MB |
| + 截面内容缓存(含 SAT 头部时间戳归一化) | 98 s | 9.87 s | 1140 次 | 2694 次 / 3.34 MB |
| + 把管道 clone 延迟到 ToJson | **93 s** | **9.4 s** | **0** | **1733 次 / 2.34 MB** |
| + X3dPart 重试修复 | 93 s | 9.4 s | 0 | 1733 次 / 2.34 MB |
对应 PR:#97/#98(计数器)、#99(属性 key 统计)、#100(截面缓存)、
#102/#103(回写定位)、#104(延迟 clone)、#105(内存读取评估)、#106(X3dPart 修复)。
三项改动各自解决了什么:
1. **截面缓存**:一次开图要做 5641 次"SAT 文本 → 临时文件 → acisIn",
其中大量内容重复。关键发现是 SAT 第 2 行带存盘时间戳,
导致同几何被判成"唯一"(命中率 54 %)——跳过该行后唯一文本从 2490 降到 1560。
2. **延迟 clone**:护栏/爬梯绘制时 `clone()` 一份管道实体只为导出 JSON,
而 clone 走"整实体序列化",把截面写成临时 SAT 文件。改为只存参数、
`ToJson()` 时按 XData 重建截面后,1140 次回写归零。
3. **X3dPart 修复**:`EnsureCreated` 的命中条件要求 `m_pSolid != NULL`,
而 Hide 构件按设计就不建几何 → 每次访问重试。加 `m_bCreated` 后
单对象最多重建次数由 544 降到 ≤3。**这一条没有提速**(早退分支在布尔运算之前),
属于正确性修复。
## 2. 剩下的时间花在哪
优化后一次开图的 CPU 采样(81,046 采样 ≈ 81 s CPU / 93 s 墙钟):
| 指标 | 占比 |
| --- | ---: |
| 在 `AecDbEntityCommon::base_ViewportDraw` 之下(我们的实体绘制) | **74.6 %** |
| **Parasolid(ASM\* 全家桶)** | **67.5 %** |
| ↑ 其中发生在我们的视口绘制里 | **93.4 %** |
| ↑ 其中在绘制之外(dwgInFields 等) | 6.6 % |
| 我们自己的 arx 自耗时(叶子帧) | **< 1 %** |
| 截面恢复(`StringToRegion` + `RegionAcisIn` 栈内) | 2.5 % |
| sqlite | ≈ 0 |
**Parasolid 时间里 93.4 % 是被我们的绘制代码调起来的**,栈里最内层的"我们的帧"分布:
| 占比 | 最内层我们的函数 | 性质 |
| ---: | --- | --- |
| 29.8 % | `AcGiViewDraw::DrawEntity` | 把已建好的实体/实体交给 CAD 画 |
| 14.5 % | `GiDrawUtility::DrawPipe` | 我们自己的扫掠封装 |
| 6.6 % | `GiDrawUtility::DrawLoft` | 我们自己的放样封装 |
| 1.9 % | `createRegion` | 截面构建 |
| 1.5 % | `AcDbX3dSolid::CreateExtrude` | 拉伸 |
| 1.4 % | `AecDbEwEquipment::Draw3dSolid` | 设备实体 |
| 1.3 % | `NewExtrude::makeSolid` | |
| 1.1 % | `AecDbBDPipeBase::GetBooleanPart3D` | 布尔 |
## 3. 结论:是"我们让它建的几何太多",不是"CAD 画得慢"
- **不是"CAD 自己在拖时间"**:只有 6.6 % 的 Parasolid 时间发生在我们的绘制路径之外。
换句话说,这张图并没有在加载期乱建几何(截面缓存已经把这块压到 2.5 %)。
- **也不是"我们的代码慢"**:我们的 arx 自耗时 < 1 %,瓶颈全在被调用的建模内核里。
- **真正的原因是建模工作量**:一次开图要建 1335 个 `AcDbX3dSolid` + 1248 个 `AecDbX3dPart`
(含布尔、扫掠、放样、蒙皮),**每一个都只建一次**(计数器验证:1335 个 solid 全部 built_once)。
这部分成本是"这张图有这么多个三维构件"的必然结果。
所以更准确的说法是:**剩余时间既不是 CAD 的锅也不是我们代码的锅,
是"构件数量 × 单构件建模成本",而这两项都由我们的设计决定。**
## 4. 还能动的地方(按性价比排序)
| 方向 | 说明 | 代价 / 风险 |
| --- | --- | --- |
| 减少要建的几何 | 视口外/遮挡/仅二维表达时跳过 3D 构建(LOD) | 改变显示行为,要按视图判断,容易出错 |
| 几何跨会话持久化 | 存盘时把建好的实体几何一起存(Phase 3,modeler filer) | 要升版本号 + 兼容矩阵,之前评估后暂缓 |
| 优化 `DrawPipe` / `DrawLoft` | 用更便宜的基本体代替扫掠(直管用圆柱/圆台) | 会改变与 CAD `Extrude` 一致的观感,需实测比对 |
| 截面恢复再优化 | 只剩 7.4 s 纯解析,命中率已接近上限 | 收益 < 0.5 %,**已判定不做**(见计划文档) |
## 5. 诊断方法与踩过的坑(可复用)
| 坑 | 结论 |
| --- | --- |
| CPU 采样只能算占比,算不出调用次数 | 一次开图 5641 次截面恢复,采样反推只估到 550~1000(差 5~10 倍)。**次数类结论必须用计数器** |
| 加密/混淆构建下函数级符号化不可靠 | trace 里 `AecDbGuardBar::Draw3d` 显示 27 %,实际计数器证明它 39 次调用全部早退。**函数级排行要用插桩核实** |
| VS 的 `VSDiagnostics.exe` 在本机会卡死 | `/launch:` 依赖 DiagnosticsHub 收集器服务,CAD 起不来 |
| `wpr -stop` 本机报 0x80010106(COM 线程模式) | 改用 `xperf -on … -f raw.etl` / `-stop` / `-merge raw out.etl` |
| 栈里只有"模块+RVA"时别猜 | `tools/resolve_rva.ps1` 用 dbghelp + 本地 PDB 解析,本文件第 5 节的两次误判都是没解析符号就下结论 |
| 计数器别写进同一份报告文件 | 抓栈若写进 `aec_section_perf.txt`,自动化脚本会把"栈写出来了"当成"开图完成",导致提前结束采集 |
相关工具:
| 工具 | 用途 |
| --- | --- |
| `tools/run_acad_perf.ps1` | 自动开图 + 执行 `ENVIPERFDUMP` + 收报告;`-CpuTrace` 可同时采 CPU 采样 |
| `tools/perf_diagsession.py` | 解析 `.diagsession` / `.etl`,出模块/函数/线程/调用链报告 |
| `tools/resolve_rva.ps1` | 模块+RVA → 函数名(dbghelp + PDB) |
| `ENVIPERFDUMP` / `ENVIPERFRESET` / `AEC_PERF_COUNTER=1` | 截面恢复、回写、延迟建几何的计数器 |
| `AEC_SECTION_CACHE=0` | 关闭截面缓存做 A/B |
| `AEC_WRITEBACK_TRACE=1` | 首次回写时抓调用栈到 `%TEMP%\aec_writeback_trace.txt` |