diff --git a/open-drawing-perf-summary.md b/open-drawing-perf-summary.md new file mode 100644 index 0000000..e11f84a --- /dev/null +++ b/open-drawing-perf-summary.md @@ -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` |