diag: 实测 3D 缓存存盘方案,判定对管道不做 #112

Merged
gjm merged 1 commits from company-desktop/company-desktop-computer/diag-3d-cache-dwg-verdict AGit into main 2026-10-29 22:16:51 +08:00
3 changed files with 117 additions and 0 deletions

No files matched your search

+110
View File
@@ -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。
@@ -3914,6 +3914,9 @@ Acad::ErrorStatus AecDbEwPipeBase::Draw3d(AcGiDraw &dc) const
{
assertReadEnabled();
//AEC_PERF_COUNTER=1 的 Debug 下才计时:用于算"管道的 3D 绘制到底占开图多少"
SectionPerf::Scope perfScope("EwPipeBase::Draw3d");
AcCmTransparency tr = transparency();
AcCmTransparency oldTr = dc.SetTransparency(tr);
@@ -3954,6 +3957,7 @@ Acad::ErrorStatus AecDbEwPipeBase::Draw3d(AcGiDraw &dc) const
//AcDb3dSolid
if (Has3DBuffer())
{
SectionPerf::Scope hitScope("EwPipeBase::Draw3d.cachehit"); //本次进程内已建过
AcDbEntity* pEnt = NULL;
const AcDbVoidPtrArray &ar3D = Get3DBuffer();
for (int i = 0;i < ar3D.length();i++)
@@ -4258,6 +4262,7 @@ Acad::ErrorStatus AecDbEwPipeBase::Draw3d(AcGiDraw &dc) const
dc.SetColor(nCrBk);
dc.SetTransparency(oldTr);
SectionPerf::Scope buildScope("EwPipeBase::Draw3d.build"); //只用来数"真的重建了几次"
return Acad::eOk;
}
@@ -958,6 +958,8 @@ bool AecDbX3dPart::EnsureCreated() const
}
SectionPerf::NoteEnsure("X3dPart", this, true);
//试点计时:X3dPart 重建总耗时(用来看"几何存盘"这套机制的天花板在哪)
SectionPerf::Scope perfScope("X3dPart::Rebuild");
const bool bOk = const_cast<AecDbX3dPart*>(this)->Rebuild();
SectionPerf::NoteEnsure(m_pSolid != NULL ? "X3dPart.rebuild_ok" : "X3dPart.rebuild_null", this, false);
if (m_pSolid == NULL)