diff --git a/3d-cache-dwg-verdict.md b/3d-cache-dwg-verdict.md new file mode 100644 index 0000000..b205b24 --- /dev/null +++ b/3d-cache-dwg-verdict.md @@ -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。 diff --git a/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp b/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp index fb8486b..e7c5fb0 100644 --- a/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp +++ b/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp @@ -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; } diff --git a/SourceCode/Code2026/CoreLibrary/AecDbX3dPart.cpp b/SourceCode/Code2026/CoreLibrary/AecDbX3dPart.cpp index 2197be9..fdf7375 100644 --- a/SourceCode/Code2026/CoreLibrary/AecDbX3dPart.cpp +++ b/SourceCode/Code2026/CoreLibrary/AecDbX3dPart.cpp @@ -958,6 +958,8 @@ bool AecDbX3dPart::EnsureCreated() const } SectionPerf::NoteEnsure("X3dPart", this, true); + //试点计时:X3dPart 重建总耗时(用来看"几何存盘"这套机制的天花板在哪) + SectionPerf::Scope perfScope("X3dPart::Rebuild"); const bool bOk = const_cast(this)->Rebuild(); SectionPerf::NoteEnsure(m_pSolid != NULL ? "X3dPart.rebuild_ok" : "X3dPart.rebuild_null", this, false); if (m_pSolid == NULL)