@@ -0,0 +1,110 @@
# 工作计划第 2 项:3D 缓存存盘(modeler filer)实测结论
> 结论:**按原方案(把 `m_ar3DDrableBufPtrs_` 存进 DWG)对管道没有意义,建议不做。**
> 门槛不在 "能不能存",而在两件更硬的事:管道在开图期只画了可见的那一小部分
> (多则 3.34 s / 89 s ≈ 3.7 %,少则 0.2 s),而 "存盘时会调用 `dwgOutFields`"
> 这个前提在增量存盘下不成立。真正值得动的目标是 `AecDbX3dPart` 的重建
> ( 16~19 s / 89 s ≈ 18~22 %),但那是另一条路。
## 1. 实测数据(big1.dwg, AutoCAD 2020 / R230 Debug, `AEC_PERF_COUNTER=1`)
开图墙钟 **89 s ** (同一构建、同一图、冷启动,三次运行 89 / 92 / 95 s)。
| 环节 | 次数 | 总耗时 | 占开图 |
| --- | ---: | ---: | ---: |
| `X3dPart::Rebuild` (构件重建) | 1248 | **16.4 ~ 19.4 s ** | **18 ~ 22 % ** |
| 基础体构造合计(GDU: DrawPipe / Loft / Cylinder / Cone) | 7266 | **36.7 s ** | 41 % |
| `EwPipeBase::Draw3d` (管道 3D 绘制,整函数) | 106 ~ 650 | **0.20 ~ 3.34 s ** | **0.2 ~ 3.7 % ** |
| 其中真正重建几何(`.build` ) | 同上 | 同左 | 同左 |
| 截面恢复 `StringToRegion` | 2559 | 10.5 s | 12 % |
关键事实:**一张图里 1088 个 `AecDbEwPipe` ,开图期只有一部分真的画了 3D**
(视口没画到就不建几何)。同一份图的两个保存视图,实测分别只画了 **106 个(0.20 s) **
和 **650 个(3.34 s) ** —— 也就是说管道这条线的收益完全取决于"开图时视图里有多少管道",
上限也在 4 % 以内,而且这部分省下来的是"可见几何",一缩放又得重建。
基础体那 36.7 s 的大头不在管道身上:它和 `X3dPart::Rebuild` 的 19.4 s 有重叠
(构件重建内部会调 GDU),剩下的是构件/设备/板墙那几条线,不是 `AecDbEwPipeBase` 。
## 2. 机制层面的三个坑(都是实测撞出来的,写下来免得重踩)
### 2.1 附加块只能挂在**叶子类**上
第一版把写/读挂在 `AecDbEwPipeBase::dwgOutFields/dwgInFields` ,结果开图直接卡死:
xperf 采样 70 % 的样本落在
`AecDbXWall::dwgInFields → AecComplexCurve::append → CArray::SetAtGrow → memcpy`
上,10 分钟都开不出来(正常 89 s)。
根因:`AecDbBDPipeBase` 、`AecDbXWall` 、`AecDbXSLab` 、`AecDbBeam` 、`AecDbWeir` 、
`AecDbMultiPartEntity` 、`AecDbExtendedEntity` 都继承自 `AecDbEwPipeBase` , **写在自己字段的后面**。
在基类的 `dwgInFields` 里读尾部块,等于把派生类的第一个字段当 magic 吃掉,
派生类的版本号/长度随之读到垃圾 —— XWall 的曲线数组长度读成天文数字,于是无限 append。
→ 结论:这个块只能由**最派生类**(如 `AecDbEwPipe` )读写。基类里放一行都不行。
(这条和 `pipe-section-region-performance-plan.md` 附录 B.2 说的是同一件事,只是那次讲的是
"新块加在末尾",没提继承链。)
### 2.2 增量存盘下 `dwgOutFields` 根本不会被调用
把块挪到叶类 `AecDbEwPipe` 后,开图恢复正常,但**存盘时块一个字节都没写出去**。
加了跟踪日志才看清:
| 动作 | 证据 |
| --- | --- |
| 开图 | `AecDbEwPipe::dwgInFields` 被调 **1088 次 ** (27 个各写各的类里,只有它读了 1088 次) |
| QSAVE(默认增量存盘) | `AecDbEwPipe::dwgOutFields` 被调 **0 次 ** |
| 同一次存盘里其它类 | `AecDbXSlab` 374、`AecDbXWall` 333、`AecDbWeir` 144、`AecDbBeam` 126 …(都写到了) |
DWG 文件确实变了(8 170 004 → 8 334 835 字节),说明存盘跑了,只是**未被修改的对象由
AutoCAD 按字节整块拷贝**,压根不走 `dwgOutFields` 。`ISAVEPERCENT=0` 、`SAVEAS` 到新文件
也都没改变这一点(实测三次)。
→ 结论:**"存盘时顺带把缓存写进实体流"这个思路,只对"本次被改过的对象"成立**。
一张图打开后什么都不改就存盘,缓存永远不会被写出;用户改了哪根管,才可能写出那一根。
### 2.3 3D 缓存本身也只在"画过"之后才有内容
`AecDbEwPipeBase::Draw3d` 里 `Add3DBuffer` 的三处都在真实绘制路径上,
开图期没画到的对象,缓冲区是空的(实测 106 / 1088)。
所以"存盘=把缓存落盘"这件事,天然只覆盖可见部分。
## 3. 与"双写"方案(附录 B)的关系
`pipe-section-region-performance-plan.md` §8 当初暂缓双写的三条理由,本轮全部被实测证实:
1. 收益不明确 → 现在明确了:对管道 ≤ 0.2 s,**可以排除**;
2. 复杂度不低 → 实测多一条:块不能放基类,且增量存盘会让它写不出去;
3. 一条写路径原则 → 依旧成立。
## 4. 建议
| 方案 | 判断 |
| --- | --- |
| 把 `AecDbEwPipeBase` 的 3D 缓存存进 DWG | **不做 ** 。上限 3.7 %(还是可见部分),还要踩 2.1/2.2 两个坑 |
| 把 `AecDbX3dPart::Rebuild` ( 16~19 s)的产物存盘 | **值得单独评估 ** ,但前提是先解决"什么时候写":构件在开图期本来就被改脏(1248 次重建都发生在 load),所以块**写得出去**;难点转移到格式与兼容(`AecDbX3dPart` 是入库对象还是挂件要先确认) |
| 继续压 `GDU::DrawPipe(region)` ( 12.3 s / 6712 次) | 与几何存盘无关,属于"少建/建得更便宜",独立立项 |
| LOD(视口外不建 3D) | 和本轮数据最契合:89 s 里只有一小部分服务于"用户真正看得到的东西" |
## 5. 本轮留下的东西
只留了两处**Debug-only、`AEC_PERF_COUNTER=1` 才生效**的计时(Release 零开销),
用来支撑上面这张表,也方便下次比较:
| 位置 | tag | 用途 |
| --- | --- | --- |
| `AecDbEwPipeBase::Draw3d` | `EwPipeBase::Draw3d` / `.build` / `.cachehit` | 管道 3D 绘制总耗时、重建次数、缓存命中次数 |
| `AecDbX3dPart::EnsureCreated` | `X3dPart::Rebuild` | 构件重建总耗时(几何存盘方案的收益天花板) |
modeler filer 的试点代码(写块 / 读块 / 补非 brep 部分)**已全部回退**:
在结论是否定的前提下留着它只是负债。
## 6. 复现方式
``` powershell
# 开图 + 收计数器(等价于 tools/run_acad_perf.ps1,只是图用拷贝)
powershell -NoProfile -File tools \ run_acad_perf . ps1
# 报告:%TEMP%\aec_section_perf.txt( timing / lazy-create / 回写分表)
```
采数时的关键设置:`AEC_PERF_COUNTER=1` (开图前设好),`AEC_SECTION_CACHE=1` (默认)。
`xperf` 抓栈用来定位 2.1 那个卡死,命令见 `open-drawing-perf-summary.md` §5。