From 9167303cce6997764f00303bec3ba07f2d9044cf Mon Sep 17 00:00:00 2001 From: company-desktop-computer Date: Wed, 28 Oct 2026 15:29:24 +0800 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20=E5=A2=9E=E5=8A=A0=E7=AE=A1?= =?UTF-8?q?=E9=81=93=E6=88=AA=E9=9D=A2=E6=81=A2=E5=A4=8D=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E6=94=B9=E9=80=A0=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景:diagsession 实测显示,打开大图纸时"截面恢复"是仅次于 Parasolid 的第二大开销, 且每个实体都要落一次 SAT 临时文件(写盘 + acisIn + 删除),会被杀软逐个扫描。 实测依据(report.diagsession,CAD 2019 系 / R190 部署,Debug): - AecDbEwPipeBase::RegionAcisInDwgInFields 栈内 7.30% - CoreTools::StringToRegion 栈内 6.88% - CoreTools::RegionToString(反向回写)3.07% - 参照:base_ViewportDraw 74.28%,Parasolid ≈43% 说明:采样只统计在跑 CPU 的时间,临时文件 I/O 等待不进采样,实际墙钟占比高于上表。 计划分四段,每段可独立回退: - Phase 0 先加计数器(Debug + 环境变量开关),拿到单次成本与文本 hash 唯一数, 用复用率决定后续是否值得投入。 - Phase 1 截面内容缓存:按 SAT 文本 hash 缓存 region 母体,命中返回 clone。 平台/版本无关,不动存盘格式;覆盖 AecDbEwPipeBase 全部子类 + StringToRegion 的调用方; 落盘次数从 O(实体数) 降到 O(唯一截面数)。 - Phase 2 临时目录治理:改用已有的 CoreTools::getTempFilePath 带父目录重载, 父目录从 INI 读,可加入杀软排除。 - Phase 3(R20+ 且非 ZRX)改用 dbacis.h 的 acdbModelerSave/RestoreEntityFromDwgFiler, 几何直接写进 DWG filer,不再产生 SAT 文本、不再落盘。 版本/平台分支:产品从 ARX R20 起支持,R190 与中望保留现有 SAT 分支。 按 AGENTS.md 红线,不在源码里比较 ARX 版本号(ZRX 的 CAD_SDK_VERSION 是年份 2023~2026, 与 ARX 19~26 数值重叠),改为在 cmakelists.txt 里算好能力宏 AEC_HAS_MODELER_FILER, 源码只用该宏切换。 已核实的 SDK 边界(文档内附版本矩阵): - AcDbBody::acisIn 全版本只有文件名重载,没有 buffer 版;Arx 里也没有 api_restore_entity_list, 所以"完全不落盘"只能走 modeler filer 通道。 - acdbModeler*FromDwgFiler 从 R20.0 起才有,R19.0 没有。 - acdbModelerSaveEntityToDwgFiler 需要裸 ENTITY*,只能通过标了 INTERNAL USE ONLY 的 getLockedASMBody/setASMBody 取得,因此 Phase 3 必须先做 round-trip 最小验证。 文档同时记录了存盘兼容性决策(S1 配置切换 / S2 按版本自动 / S3 双写,推荐 S1 且默认仍写 SAT)、 风险与回滚、验证清单(R230/R190/ZRX 编译 + CAD 实测 + 复录 trace 对比)和 5 个未决问题。 验证:仅文档,未改代码,无编译与 CAD 实测。 --- pipe-section-region-performance-plan.md | 287 ++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 pipe-section-region-performance-plan.md diff --git a/pipe-section-region-performance-plan.md b/pipe-section-region-performance-plan.md new file mode 100644 index 0000000..c68f1c9 --- /dev/null +++ b/pipe-section-region-performance-plan.md @@ -0,0 +1,287 @@ +# 管道截面(Region)恢复性能改造计划 + +> 目标:降低"打开大图纸"时**截面构建/恢复**的开销,并消除每实体一次的临时文件往返(含杀软扫描)。 +> 依据:`diagsession-cpu-analysis.md` 的实测数据;`report-cpu-hotspots.md` 为原始表格。 +> 相关既有结论:`GiDrawUtility-analysis.md` §7.4 E2、`AecDbEwPipeBase-region-sat-plan.md` §3.8。 + +## 0. 结论摘要(TL;DR) + +1. **先做缓存(Phase 1)**:按截面**内容 hash** 缓存 region 母体,命中返回 `clone()`。 + 平台/版本无关,不动存盘格式,覆盖 `AecDbEwPipeBase` 全部子类 + `CoreTools::StringToRegion` 的调用方。 + 预期:临时文件落盘次数从 **O(实体数)** 降到 **O(唯一截面数)**(通常几十次),CPU 省 **约 6~10 个百分点**。 +2. **临时目录治理(Phase 2)**:把 SAT 临时文件从 `%TEMP%` 挪到可配置目录,可加入杀软排除。 + 改动极小,立即止血。 +3. **换存储通道(Phase 3,R200+ 且非 ZRX)**:用 `dbacis.h` 的 modeler ↔ filer 接口把几何直接写进 + DWG filer,**不再产生 SAT 文本、不再落盘**。R190 保留现有 SAT 分支(编译期切入)。 + 注意:Phase 1 之后这一项的**纯性能**收益已经不大,它的主要价值变成 + 「消除 SAT 文本转换链」与「DWG 体积」。**建议在 Phase 1 实测收益落地后再决定是否投入。** + +## 1. 数据依据 + +来自本机第二轮 trace(`report.diagsession`,CAD 2019 系 / R190 部署,Debug 构建): +1 ms 采样,acad.exe 42,888 个带栈采样 ≈ **42.9 s CPU**。 + +| 指标(栈内出现率,每栈去重) | 采样 | 占 acad CPU | +| --- | ---: | ---: | +| `AecDbEwPipeBase::RegionAcisInDwgInFields` | 3,130 | **7.30 %** | +| `CoreTools::StringToRegion` | 2,949 | **6.88 %** | +| `AecDbEwPipeBase::dwgInFields`(上层) | 3,420 | 7.97 % | +| `CoreTools::RegionToString`(反向回写) | 1,315 | 3.07 % | +| 参照:`AecDbEntityCommon::base_ViewportDraw` | 31,856 | 74.28 % | +| 参照:Parasolid(ASM* 家族独占) | — | ≈ 43 % | + +两条恢复入口有部分重叠,保守估计 **截面恢复 ≈ 10~14 %**。 + +**两个必须记住的采样局限**: + +- 采样只统计**在跑 CPU** 的时间,临时文件 write/read 的**等待不进采样** → + 实际墙钟占比 **高于** 上表。 +- 这里的 10~14 % 是"载入期付一次";绘制期还会再走扫掠/布尔,两者是不同的问题。 + +## 2. 现状:三条相关代码路径 + +### 2.1 `AecDbEwPipeBase::RegionAcisInDwgInFields`(`AecDbEwPipeBase.cpp:794`) + +DWG 里存的是 **SAT 文本 + 一个文件名**。恢复流程: + +```cpp +pFiler->readString(&pszFileName); pFiler->readUInt32(&nLen); pFiler->readBytes(pszTxt, nLen); +// errorfile 分支(原生 dwgInFields)见下 +sFullPath = FilePath::getSystemTemporaryFolder() + "\\" + SanitizeRegionTempName(sFileName); +file.Open(sFullPath, CFile::modeCreate | CFile::modeWrite); file.Write(pszTxt, nLen); file.Close(); +AcDbBody::acisIn(sFullPath, arBodyPtrs); // 从盘上解析 +::DeleteFile(sFullPath); +``` + +该函数是 `AecDbEwPipeBase` 的方法 → **`AecDbEwPipe` 及所有管件子类共用**(`AecDbEwPipeBase.cpp:910` 由 +`SectionRegionDwgInFields` 调用,`:955` 赋给 `m_pRegion`)。 + +### 2.2 `CoreTools::StringToRegion`(`CoreTools.cpp:3519` / `:3581`) + +同样的"写临时文件 → `acisIn` → `DeleteFile`",比 2.1 多一步 `fixACISFormat` 改写文本。 +调用方覆盖 `AecDbEwEquipment`、`AecDbInsulation`、`AecDbBDPipeBase`、`PipelineCmd`(`EQU_MODIFY.cpp`)等。 +另有 `CoreTools::StringToBody`(`:3712`)同一模式。 + +### 2.3 绘制期:直管几何的构建(`AcDbX3dSolid.cpp:1242~1300`) + +```cpp +createFromCurves([外圆 pl]) → pRegO // 第 1 次 +createFromCurves([内圆 pPlI]) → pRegI // 第 2 次 +extrudeAlongPath(pRegO, ln) → pSolidO // 扫掠 1 +extrudeAlongPath(pRegI, ln) → pSolidI // 扫掠 2 +pSolidO->booleanOper(kBoolSubtract, pSolidI) // 布尔减 +``` + +`GiDrawUtility::DrawPipe`(`GiDrawUtility.cpp:938`)里另有 `pRegion->clone()` + `transfromRegion()`, +因为原 region 会被变换。**这也是 Phase 1 能零风险接入的原因:clone 语义本来就存在。** + +### 2.4 SDK 能力边界(已核实) + +| 接口 | 位置 | R19.0 | R20.0+ | +| --- | --- | --- | --- | +| `AcDbBody::acisIn(const ACHAR* fileName, ...)` | `Arx/*/inc/dbBody.h:49` | 有(**仅文件名**,无 buffer 重载) | 有 | +| `acdbModelerSaveEntityToDwgFiler` | `Arx/*/inc/dbacis.h` | **无** | 有 | +| `acdbModelerRestoreEntityFromDwgFiler` | 同上 | **无** | 有 | +| `acdbModeler*ToDxfFiler` | 同上 | **无** | 有 | + +- `Arx/` 里搜不到 `api_restore_entity_list` 等 ACIS 底层 API → **想彻底不落盘,只能用 modeler filer 通道**。 +- modeler filer 的 `modelerVersion` 参数(`<0` 跟随 DWG、`0` 当前、`>0` 指定如 `2170`)与 + `restoreOptions` 的 `kReadForeignEntities | kReadUnknownEntities`,是官方提供的**跨版本兼容机制**—— + 这正好回应了 `region-sat-plan.md` §3.8 当初选 SAT 的理由。 +- **坑**:`acdbModelerSaveEntityToDwgFiler` 要的是裸 `ENTITY*`,而 `AcDbBody` 只提供了标 + **INTERNAL USE ONLY** 的 `getLockedASMBody()` / `unlockASMBody()` / `setASMBody()` + (`Arx/R23.0/inc/dbBody.h:38-43`,R19 也有但无对应 save/restore 函数)。 + → Phase 3 必须先在 R230 上做一次 round-trip 最小验证,确认这套 INTERNAL 接口在目标版本上稳定。 + +## 3. 方案 + +### Phase 0:先测量(不改行为) + +在 `RegionAcisInDwgInFields` 与 `StringToRegion` 两个入口 + 临时文件写/删处加计数, +`_DEBUG` 编译进、环境变量 `AEC_PERF_COUNTER=1` 开启、命令 `ENVIPERFDUMP` 打印汇总: + +| 计数项 | 用途 | +| --- | --- | +| 调用次数、单次/累计 µs(QPC 包住整段) | 单次成本(预估 3~15 ms) | +| 输入文本 hash 的**唯一值数** | 复用率 = 1 − 唯一/总数 → Phase 1 收益上限 | +| **模拟命中率**(只统计、不改行为) | 同上,更直观 | +| 临时文件写入次数 + 累计字节 | 杀软影响面 | +| `RegionToString` / `RegionAcisoutDwgOutFields` 次数 | 查清打开时为何在回写(3.07 %) | + +**决策点 D0**:唯一数是否远小于总次数(例如 30 种 × 1500 次)。 +若是 → Phase 1 直接做;若否(每个实体文本都不同)→ 回退,重新评估。 + +### Phase 1:截面内容缓存(核心,全平台) + +新增一个缓存模块(建议 `CoreLibrary/SectionRegionCache.h/.cpp`,私有实现放匿名 namespace): + +- **key**:输入 SAT 文本字节的 64 位 hash(自实现 FNV-1a/xxHash,不引第三方); + 可选混入 `nLen`,避免不同实现下 hash 碰撞语义不一致。 + → **内容寻址,不需要任何失效逻辑**:样式/参数变了,文本变了,key 自然变。 +- **value**:**母体** `AcDbRegion*`(heap 对象,不入库,永不外泄)。 +- **API**:`AcDbRegion* SectionRegionCache::Acquire(const char* psz, Adesk::UInt32 nLen)`, + 命中返回 `clone()`;miss 返回 NULL 由调用方走原逻辑,成功后 `Put()` 存入母体。 +- **容量**:条目上限(如 512)+ 总字节上限(如 32 MB)+ LRU;条目析构 `delete` 母体。 +- **线程**:先加 `std::mutex`(当前 trace 显示 98 % 单线程,开销可忽略)。 + 若将来绘制并行,注意 modeler 实体不能跨 partition 共享,需要改为每线程缓存(在注释里写清楚)。 + +接入点(两处即可覆盖绝大部分): + +1. `AecDbEwPipeBase::RegionAcisInDwgInFields` —— 在**写临时文件之前**先查缓存。 +2. `CoreTools::StringToRegion`(两个重载)与 `StringToBody` —— 同样在写文件前查缓存。 + +**不变量(必须守住)**: + +- 缓存只发 **clone**;调用方拿到的永远是独立对象(现有 `DrawPipe` 的 `clone()` + `transfromRegion()` 不受影响)。 +- 母体不被任何调用方 `delete`。 +- miss 路径的 `DeleteFile` 逻辑保持原样(含失败分支)。 + +### Phase 2:临时目录治理(止血) + +现状两处硬编码 `%TEMP%`,而 `%TEMP%` 通常**不在**客户"工作目录排除"范围内 → 每次落盘都被扫。 + +- 改用仓库已有的 `CoreTools::getTempFilePath(main, suffix, pszParentPath, ...)`(`CoreTools.h:495`), + 父目录从 INI 读(如 `SectionTempDir`),空/不存在时回落系统临时目录。 +- 安装/部署脚本把该目录加入 Defender 排除: + `Add-MpPreference -ExclusionPath ""`。 +- 顺带收益:现在临时文件名来自 DWG 里存的 `sFileName`,多实体同名会互相覆盖 + (`temp-file-naming.md` 已记录此类隐患);缓存命中路径不落盘,命中时该隐患消失。 + +### Phase 3:改用 modeler filer 存储(R200+ / 非 ZRX) + +**编译期能力开关**(见 §4),源码里不直接比较 ARX 版本号。 + +- **写**(`SectionRegionDwgOutFields`):`acdbModelerSaveEntityToDwgFiler(pFiler, pEntity, pDb, modelerVersion)` +- **读**(`SectionRegionDwgInFields`):`acdbModelerRestoreEntityFromDwgFiler(pFiler, pEntity, ...)` + → `setASMBody()` 装配到新的 `AcDbRegion` +- 不再产生 SAT 文本、不再有临时文件;文本/曲线两条老通道保留为**只读兼容层**。 +- 计划先做一个**最小验证**:round-trip 存/读后比对 + ① 拓扑与体积 ② 与 `acisIn` 结果的一致性 ③ 高低版本互换(R230 存 → R260 读,反向各一次)。 + +**收益重估**:Phase 1 落地后,落盘次数降到"唯一截面数"量级(几十次), +Phase 3 省下的 CPU 很小。它的真实价值转为: + +| 价值 | 说明 | +| --- | --- | +| 消除 `SAT 文本 ⇄ 几何` 这条转换链 | `region-sat-plan.md` §3.8 指出该链历史 bug 集中;新通道不经过它 | +| DWG 体积 | SAT 文本每截面 100 B ~ 数 KB,二进制 modeler 数据通常更小(需实测) | +| 首读成本 | 每个唯一截面少一次"写盘 + acisIn" | + +**成本/风险**:需要升版本号、需要兼容矩阵决策(见 §5)、依赖 INTERNAL 接口。 + +## 4. 平台与版本矩阵 + 宏方案 + +产品从 **ARX R20.0** 起支持;R190(AutoCAD 2013-2014)仅本地习惯在用;中望(ZRX)也要支持。 + +| 编译目标 | `acdbModeler*FromDwgFiler` | 本计划走哪条路 | +| --- | --- | --- | +| ARX **R19.0**(AutoCAD 2013-2014) | 无 | **保留现有 SAT 分支**(Phase 1/2 仍生效) | +| ARX R20.0 ~ R26.0 | 有 | Phase 3 可用 | +| ZRX Z2023 ~ Z2026 | 无(大概率) | **保留现有 SAT 分支** | + +**不要**在源码里比较 ARX 版本号:`cmakepresets.json` 里 ZRX 的 `CAD_SDK_VERSION` 是年份(`2023`~`2026`), +与 ARX 的 `19`~`26` 数值区间重叠,直接比较必然误判(AGENTS.md 已列为红线)。 + +改为在 CMake 里算好**能力宏**(`cmakelists.txt` 必须保持 ASCII 无 BOM): + +```cmake +# 与现有 ZWCAD 判定同一处(cmakelists.txt 第 78~81 行附近) +if (NOT IS_ZWCAD AND CAD_SDK_VERSION GREATER_EQUAL 20) + message(STATUS ">>> 启用 modeler filer 存储通道(ARX >= 20.0)") + add_compile_definitions(AEC_HAS_MODELER_FILER) +endif() +``` + +源码里只用能力宏(`#ifdef ZWCAD` 的既有用法不变): + +```cpp +#if defined(AEC_HAS_MODELER_FILER) + // 新通道 +#else + // 现有 SAT 文本通道(R190 / ZRX) +#endif +``` + +好处:判断只在一处(CMake,`IS_ZWCAD` 已知),源码里没有版本号比较,R190/ZRX 分支天然保留。 + +## 5. 存盘兼容性:必须先定的决策 + +新通道写出的数据,**R190 与 ZRX 插件读不懂**(这两条分支只会解析 SAT 文本/曲线)。 + +候选策略: + +| 方案 | 做法 | 优点 | 缺点 | +| --- | --- | --- | --- | +| **S1 配置切换**(推荐先做) | 默认仍写 SAT;INI/系统变量打开后才走新通道 | 一条写路径(同时只有一条生效);风险可控 | 需要用户/部署决定;两套代码都要维护 | +| S2 按 DWG 版本自动选 | `acdbGetModelerDwgVer()` / DWG 保存版本决定 | 无人工干预 | 判断依据与"谁来打开这张图"无关,仍可能误伤 | +| S3 双写 | 同时写 SAT 与 modeler 数据 | 任何版本都能读 | **明确违反 `region-sat-plan.md` §3.8「一条写路径」原则**,不推荐 | + +无论选哪个,都需要: + +- 升 `kCurrentVersionNumber`(当前管道族各版本号见各自头文件),新老读端都要能识别; +- 读端保持**三通道兼容**:曲线通道(老图)→ SAT 文本(现行)→ modeler 数据(新); +- 写端在"降级"时(配置关、或显式另存低版本)必须回到 SAT。 + +**决策点 D1**:选 S1 / S2 / S3。本文推荐 **S1**,并把默认值设为"写 SAT", +即:Phase 3 上线后**默认行为不变**,只在验证过的项目上开启。 + +## 6. 风险与回滚 + +| 风险 | 影响 | 应对 | +| --- | --- | --- | +| 缓存母体被调用方污染 | 几何错乱 | 只发 clone;母体不去库、不外泄;Code Review 专项检查 | +| `AcDbRegion::clone()` 深拷贝语义不足 | 共享脏数据 | 用现有 `m_pRegion->clone()` 的同一语义;加单测式验证(同一母体 clone 两次分别变换,互不影响) | +| hash 碰撞 | 用错截面 | 64 位 hash + 长度参与;命中后再做一次字节比对(成本远低于重建) | +| 内存增长 | 大图纸爆内存 | 条目数 + 总字节双上限 + LRU;打印峰值供现场反馈 | +| modeler filer 依赖 INTERNAL 接口 | SDK 升级后编译/运行异常 | Phase 3 先做 round-trip 验证;能力宏可一键关掉(回到 SAT) | +| 新格式导致老版本读不到 | 数据丢失观感 | 默认写 SAT(S1);转换前提示;保留只读兼容层 | +| 回写(`RegionToString` 3.07 %)原因不明 | 白做一次序列化 | Phase 0 计数确认;若确属不必要,单独一个 PR 修掉 | + +**回滚**:每个 Phase 独立可回退—— +Phase 1 由宏/配置一键关闭(走原路径);Phase 2 只改路径来源;Phase 3 由能力宏/配置关闭。 + +## 7. 验证清单 + +编译: + +- [ ] `R230`(日常调试目标)Debug/Release 编译通过 +- [ ] `R190` 编译通过(验证 SAT 分支仍完整) +- [ ] 至少一个 ZRX preset(如 `Z2026`)编译通过 +- [ ] `cmakelists.txt` 仍为 ASCII 无 BOM;改动的 `.cpp/.h` 为 UTF-8 with BOM + +功能(CAD 实测,逐项记录): + +- [ ] 新建管道 → 存盘 → 重开:截面几何、面积、位置一致 +- [ ] 老图纸(曲线通道 / SAT 文本通道)打开后截面正确 +- [ ] 老图纸另存 → 重开,截面仍正确 +- [ ] 复制粘贴(`copyFrom` 走 `m_pRegion->clone()`)不丢截面 +- [ ] 管件、设备(`AecDbEwEquipment`)、保温(`AecDbInsulation`)截面均正确 +- [ ] Phase 3(若开启):R230 存 → R260 读、R260 存 → R230 读,双向几何一致 + +性能(用 `tools/perf_diagsession.py`): + +- [ ] 同一张图、同一 preset、同一构建配置下各录一次 trace 对比 +- [ ] 关键指标:截面恢复占比、临时文件写入次数、`base_ViewportDraw` 总占比 +- [ ] 记录墙钟耗时(点开到可操作)与 CPU 采样时间两个口径 + +## 8. 未决问题 + +1. **D0**:SAT 文本的唯一 hash 数到底是多少?(Phase 0 用计数器回答) +2. **D1**:Phase 3 的写路径策略选 S1/S2/S3?(本文推荐 S1,默认仍写 SAT) +3. 打开图纸时为什么在**回写** SAT(`RegionToString` 3.07 %)?是 `copyFrom`/深拷贝走了 `dwgOutFields`, + 还是别的原因——Phase 0 计数确认后再定是否单独立项。 +4. Phase 1 之后 `AecDbX3dPart::EnsureCreated/Rebuild` 仍每次都重建(栈上 8.4 % 却不命中)—— + 这是**另一个独立问题**,不在本计划范围内,但优先级可能更高(见 `diagsession-cpu-analysis.md` §4)。 +5. `AcDbBody::acisIn` 只有文件名版本;若将来想彻底去掉临时文件而 Phase 3 又被否, + 只剩"自行接入 ACIS API"一条路,成本与授权都需要再评估。 + +## 9. 与既有文档的关系 + +- `AecDbEwPipeBase-region-sat-plan.md` §3.8:否决的是「**存储**按标准/异形分流」与「曲线通道写路径保留」。 + 本计划的 Phase 1 是**运行期缓存**,Phase 3 是**换一条统一写路径**(不是分流),不违背该结论。 +- `GiDrawUtility-analysis.md` §7.4 E2:否决的是「同一 draw 内跨分段复用截面」, + 理由是"大头是 N−1 次 boolean unite"。本计划不再碰那个分支,而是把"造截面"整体次数降下来; + 同时提醒:**别指望截面缓存解决 Parasolid 的扫掠/布尔开销**,那是另一个 P0 项。 +- `temp-file-naming.md`:临时文件命名/多实例规范,Phase 2 改动需遵守。 +- `sqlite_query_cache.md`:同样属于"绘制期重复查/重复造"的问题,已由 #87/#88 解决 + (trace 实测 sqlite 从 17.0 % 降到 0.06 %)。 -- 2.54.0 From 38bf61072f9009ce03212905bc08f17cf0f6378f Mon Sep 17 00:00:00 2001 From: company-desktop-computer Date: Wed, 28 Oct 2026 15:44:11 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=E8=B0=83=E6=95=B4=E6=88=AA?= =?UTF-8?q?=E9=9D=A2=E6=94=B9=E9=80=A0=E8=AE=A1=E5=88=92=EF=BC=9A=E6=9C=AC?= =?UTF-8?q?=E8=BD=AE=E5=8F=AA=E5=81=9A=E7=BC=93=E5=AD=98=EF=BC=8C=E5=AD=98?= =?UTF-8?q?=E7=9B=98=E6=A0=BC=E5=BC=8F=E4=BF=9D=E6=8C=81=20SAT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版把"改用 acdbModelerSaveEntityToDwgFiler 存盘"列为 Phase 3,经讨论后本轮暂缓, 本次重写计划文档的范围与结构。 暂缓存盘改造的理由: 1. 收益不明确 —— 无法确认 modeler filer 通道比 "SAT 文本 -> 临时文件 -> acisIn" 快多少, 目前能确认的只有"省掉临时文件、从而省掉杀软扫描时间"这一条; 2. 这条收益在 Phase 1 之后很小 —— 缓存落地后落盘次数已经降到 O(唯一截面数),通常几十次; 3. 复杂度不低 —— 不能升 kCurrentVersionNumber(AecDbEwPipeBase.cpp:938 里 version > kCurrentVersionNumber 会直接 eMakeMeProxy,老版本整实体变 proxy)、 读端必须延后构造否则会白读一遍 SAT、DWG 体积翻倍、还要过整套老版本兼容验证矩阵; 4. region-sat-plan.md §3.8 已确立"一条写路径"原则,双写与之相悖。 本轮范围改为: - Phase 0 计数器(Debug + 环境变量开关),拿单次成本与文本 hash 唯一数; - Phase 1 截面内容缓存(按 SAT 文本 hash 缓存 region 母体,命中返回 clone), 覆盖 AecDbEwPipeBase 全部子类 + CoreTools::StringToRegion 的调用方; - Phase 2 临时目录治理(可选,止血)。 因为本轮不涉及任何按版本/平台分叉的代码,§4 的平台矩阵简化为"全平台一致、无需能力宏"; 将来若重启存盘改造再按 NOT IS_ZWCAD AND CAD_SDK_VERSION GREATER_EQUAL 20 算能力宏 (不能直接在源码里比版本号,ZRX 的 CAD_SDK_VERSION 是年份,与 ARX 19~26 数值重叠)。 调研结论没有丢,收进两个附录备查: - 附录 A:SDK 能力边界(acisIn 只有文件名重载;modeler filer 从 R20.0 起才有; save 需要裸 ENTITY*,只能走标了 INTERNAL USE ONLY 的 getLockedASMBody/setASMBody); - 附录 B:将来若做双写必须注意的点(红线不能升版本号、读端延后构造的逻辑漏洞与修法、 块格式建议、七项老版本兼容验证矩阵、体积代价、helper 抽取方式)。 验证:仅文档,未改代码,无编译与 CAD 实测。 --- pipe-section-region-performance-plan.md | 315 ++++++++++++++---------- 1 file changed, 179 insertions(+), 136 deletions(-) diff --git a/pipe-section-region-performance-plan.md b/pipe-section-region-performance-plan.md index c68f1c9..f318d6c 100644 --- a/pipe-section-region-performance-plan.md +++ b/pipe-section-region-performance-plan.md @@ -1,24 +1,27 @@ # 管道截面(Region)恢复性能改造计划 -> 目标:降低"打开大图纸"时**截面构建/恢复**的开销,并消除每实体一次的临时文件往返(含杀软扫描)。 +> 目标:降低"打开大图纸"时**截面恢复**的开销。 > 依据:`diagsession-cpu-analysis.md` 的实测数据;`report-cpu-hotspots.md` 为原始表格。 > 相关既有结论:`GiDrawUtility-analysis.md` §7.4 E2、`AecDbEwPipeBase-region-sat-plan.md` §3.8。 -## 0. 结论摘要(TL;DR) +## 0. 结论摘要(本轮范围) -1. **先做缓存(Phase 1)**:按截面**内容 hash** 缓存 region 母体,命中返回 `clone()`。 - 平台/版本无关,不动存盘格式,覆盖 `AecDbEwPipeBase` 全部子类 + `CoreTools::StringToRegion` 的调用方。 +**本轮只做运行期优化,不动存盘格式。** + +1. **Phase 0:先测量**(不改行为,Debug + 环境变量开关)。 +2. **Phase 1:截面内容缓存**(核心)——按 SAT 文本 hash 缓存 region 母体,命中返回 `clone()`。 + 平台/版本无关,不涉及任何格式/协议改动,覆盖 `AecDbEwPipeBase` 全部子类 + `CoreTools::StringToRegion` 的调用方。 预期:临时文件落盘次数从 **O(实体数)** 降到 **O(唯一截面数)**(通常几十次),CPU 省 **约 6~10 个百分点**。 -2. **临时目录治理(Phase 2)**:把 SAT 临时文件从 `%TEMP%` 挪到可配置目录,可加入杀软排除。 - 改动极小,立即止血。 -3. **换存储通道(Phase 3,R200+ 且非 ZRX)**:用 `dbacis.h` 的 modeler ↔ filer 接口把几何直接写进 - DWG filer,**不再产生 SAT 文本、不再落盘**。R190 保留现有 SAT 分支(编译期切入)。 - 注意:Phase 1 之后这一项的**纯性能**收益已经不大,它的主要价值变成 - 「消除 SAT 文本转换链」与「DWG 体积」。**建议在 Phase 1 实测收益落地后再决定是否投入。** +3. **Phase 2:临时目录治理**(可选,止血)——把 SAT 临时文件从 `%TEMP%` 挪到可配置目录,可加入杀软排除。 + +**明确不在本轮范围**:改用 `acdbModelerSaveEntityToDwgFiler` 存盘(含"双写"方案)。 +决策理由见 §8,调研结论与将来要做时的注意事项见附录 A/B,避免以后重新踩一遍。 + +**长期唯一硬要求**:新版本软件必须能读老图纸。当前 SAT 文本通道继续保留,天然满足。 ## 1. 数据依据 -来自本机第二轮 trace(`report.diagsession`,CAD 2019 系 / R190 部署,Debug 构建): +本机第二轮 trace(`report.diagsession`,CAD 2019 系 / R190 部署,Debug 构建): 1 ms 采样,acad.exe 42,888 个带栈采样 ≈ **42.9 s CPU**。 | 指标(栈内出现率,每栈去重) | 采样 | 占 acad CPU | @@ -36,25 +39,24 @@ - 采样只统计**在跑 CPU** 的时间,临时文件 write/read 的**等待不进采样** → 实际墙钟占比 **高于** 上表。 -- 这里的 10~14 % 是"载入期付一次";绘制期还会再走扫掠/布尔,两者是不同的问题。 +- 这里的 10~14 % 是"载入期付一次";绘制期还会再走扫掠/布尔,两者是不同问题(见 §7 第 4 条)。 -## 2. 现状:三条相关代码路径 +## 2. 现状:相关代码路径 ### 2.1 `AecDbEwPipeBase::RegionAcisInDwgInFields`(`AecDbEwPipeBase.cpp:794`) DWG 里存的是 **SAT 文本 + 一个文件名**。恢复流程: ```cpp -pFiler->readString(&pszFileName); pFiler->readUInt32(&nLen); pFiler->readBytes(pszTxt, nLen); -// errorfile 分支(原生 dwgInFields)见下 +pFiler->readString(&pszFileName); pFiler->readUInt32(&nLen); pFiler->readBytes(pszTxt, nLen); sFullPath = FilePath::getSystemTemporaryFolder() + "\\" + SanitizeRegionTempName(sFileName); file.Open(sFullPath, CFile::modeCreate | CFile::modeWrite); file.Write(pszTxt, nLen); file.Close(); AcDbBody::acisIn(sFullPath, arBodyPtrs); // 从盘上解析 ::DeleteFile(sFullPath); ``` -该函数是 `AecDbEwPipeBase` 的方法 → **`AecDbEwPipe` 及所有管件子类共用**(`AecDbEwPipeBase.cpp:910` 由 -`SectionRegionDwgInFields` 调用,`:955` 赋给 `m_pRegion`)。 +该函数是 `AecDbEwPipeBase` 的方法 → **`AecDbEwPipe` 及所有管件子类共用** +(`AecDbEwPipeBase.cpp:910` 由 `SectionRegionDwgInFields` 调用,`:955` 赋给 `m_pRegion`)。 ### 2.2 `CoreTools::StringToRegion`(`CoreTools.cpp:3519` / `:3581`) @@ -75,25 +77,14 @@ pSolidO->booleanOper(kBoolSubtract, pSolidI) // 布尔减 `GiDrawUtility::DrawPipe`(`GiDrawUtility.cpp:938`)里另有 `pRegion->clone()` + `transfromRegion()`, 因为原 region 会被变换。**这也是 Phase 1 能零风险接入的原因:clone 语义本来就存在。** -### 2.4 SDK 能力边界(已核实) +### 2.4 存盘现状(本轮不改) -| 接口 | 位置 | R19.0 | R20.0+ | -| --- | --- | --- | --- | -| `AcDbBody::acisIn(const ACHAR* fileName, ...)` | `Arx/*/inc/dbBody.h:49` | 有(**仅文件名**,无 buffer 重载) | 有 | -| `acdbModelerSaveEntityToDwgFiler` | `Arx/*/inc/dbacis.h` | **无** | 有 | -| `acdbModelerRestoreEntityFromDwgFiler` | 同上 | **无** | 有 | -| `acdbModeler*ToDxfFiler` | 同上 | **无** | 有 | +- `kCurrentVersionNumber = 7`(`AecDbEwPipeBase.cpp:382`) +- region 块布局:`writeUInt32(0)`(SAT 通道标志)+ `RegionAcisoutDwgOutFields` 写 SAT 文本 + (`SectionRegionDwgOutFields:1279`) +- `dwgInFields:938` 有 `if (version > kCurrentVersionNumber) return Acad::eMakeMeProxy;` -- `Arx/` 里搜不到 `api_restore_entity_list` 等 ACIS 底层 API → **想彻底不落盘,只能用 modeler filer 通道**。 -- modeler filer 的 `modelerVersion` 参数(`<0` 跟随 DWG、`0` 当前、`>0` 指定如 `2170`)与 - `restoreOptions` 的 `kReadForeignEntities | kReadUnknownEntities`,是官方提供的**跨版本兼容机制**—— - 这正好回应了 `region-sat-plan.md` §3.8 当初选 SAT 的理由。 -- **坑**:`acdbModelerSaveEntityToDwgFiler` 要的是裸 `ENTITY*`,而 `AcDbBody` 只提供了标 - **INTERNAL USE ONLY** 的 `getLockedASMBody()` / `unlockASMBody()` / `setASMBody()` - (`Arx/R23.0/inc/dbBody.h:38-43`,R19 也有但无对应 save/restore 函数)。 - → Phase 3 必须先在 R230 上做一次 round-trip 最小验证,确认这套 INTERNAL 接口在目标版本上稳定。 - -## 3. 方案 +## 3. 本轮方案 ### Phase 0:先测量(不改行为) @@ -109,21 +100,22 @@ pSolidO->booleanOper(kBoolSubtract, pSolidI) // 布尔减 | `RegionToString` / `RegionAcisoutDwgOutFields` 次数 | 查清打开时为何在回写(3.07 %) | **决策点 D0**:唯一数是否远小于总次数(例如 30 种 × 1500 次)。 -若是 → Phase 1 直接做;若否(每个实体文本都不同)→ 回退,重新评估。 +- 是 → Phase 1 直接做 +- 否(每个实体文本都不同)→ 停下重新评估(此时缓存收益接近 0) ### Phase 1:截面内容缓存(核心,全平台) 新增一个缓存模块(建议 `CoreLibrary/SectionRegionCache.h/.cpp`,私有实现放匿名 namespace): -- **key**:输入 SAT 文本字节的 64 位 hash(自实现 FNV-1a/xxHash,不引第三方); - 可选混入 `nLen`,避免不同实现下 hash 碰撞语义不一致。 +- **key**:输入 SAT 文本字节的 64 位 hash(自实现 FNV-1a/xxHash,不引第三方),长度参与计算。 → **内容寻址,不需要任何失效逻辑**:样式/参数变了,文本变了,key 自然变。 - **value**:**母体** `AcDbRegion*`(heap 对象,不入库,永不外泄)。 -- **API**:`AcDbRegion* SectionRegionCache::Acquire(const char* psz, Adesk::UInt32 nLen)`, - 命中返回 `clone()`;miss 返回 NULL 由调用方走原逻辑,成功后 `Put()` 存入母体。 +- **API**: + - `AcDbRegion* SectionRegionCache::Acquire(const char* psz, Adesk::UInt32 nLen)` —— 命中返回 `clone()`,未命中返回 NULL + - `void SectionRegionCache::Put(const char* psz, Adesk::UInt32 nLen, AcDbRegion* pMaster)` —— 由调用方在成功构造后存入 - **容量**:条目上限(如 512)+ 总字节上限(如 32 MB)+ LRU;条目析构 `delete` 母体。 -- **线程**:先加 `std::mutex`(当前 trace 显示 98 % 单线程,开销可忽略)。 - 若将来绘制并行,注意 modeler 实体不能跨 partition 共享,需要改为每线程缓存(在注释里写清楚)。 +- **线程**:先加 `std::mutex`(trace 显示 98 % 单线程,开销可忽略)。 + 若将来绘制并行,注意 modeler 实体不能跨 partition 共享,需改为每线程缓存(注释里写清楚)。 接入点(两处即可覆盖绝大部分): @@ -136,118 +128,59 @@ pSolidO->booleanOper(kBoolSubtract, pSolidI) // 布尔减 - 母体不被任何调用方 `delete`。 - miss 路径的 `DeleteFile` 逻辑保持原样(含失败分支)。 -### Phase 2:临时目录治理(止血) +### Phase 2:临时目录治理(可选,止血) -现状两处硬编码 `%TEMP%`,而 `%TEMP%` 通常**不在**客户"工作目录排除"范围内 → 每次落盘都被扫。 +Phase 1 之后残余落盘只剩"每个唯一截面一次",收益小;但如果现场仍被杀软拖慢,可做这一步: +- 现状两处硬编码 `%TEMP%`,而 `%TEMP%` 通常**不在**客户"工作目录排除"范围内 → 每次落盘都被扫。 - 改用仓库已有的 `CoreTools::getTempFilePath(main, suffix, pszParentPath, ...)`(`CoreTools.h:495`), 父目录从 INI 读(如 `SectionTempDir`),空/不存在时回落系统临时目录。 -- 安装/部署脚本把该目录加入 Defender 排除: - `Add-MpPreference -ExclusionPath ""`。 +- 安装/部署脚本把该目录加入 Defender 排除:`Add-MpPreference -ExclusionPath ""`。 - 顺带收益:现在临时文件名来自 DWG 里存的 `sFileName`,多实体同名会互相覆盖 - (`temp-file-naming.md` 已记录此类隐患);缓存命中路径不落盘,命中时该隐患消失。 + (`temp-file-naming.md` 已记录此类隐患);缓存命中时该隐患消失。 -### Phase 3:改用 modeler filer 存储(R200+ / 非 ZRX) +## 4. 平台与版本矩阵 -**编译期能力开关**(见 §4),源码里不直接比较 ARX 版本号。 +本轮**不引入任何按版本/平台分叉的代码**,因此: -- **写**(`SectionRegionDwgOutFields`):`acdbModelerSaveEntityToDwgFiler(pFiler, pEntity, pDb, modelerVersion)` -- **读**(`SectionRegionDwgInFields`):`acdbModelerRestoreEntityFromDwgFiler(pFiler, pEntity, ...)` - → `setASMBody()` 装配到新的 `AcDbRegion` -- 不再产生 SAT 文本、不再有临时文件;文本/曲线两条老通道保留为**只读兼容层**。 -- 计划先做一个**最小验证**:round-trip 存/读后比对 - ① 拓扑与体积 ② 与 `acisIn` 结果的一致性 ③ 高低版本互换(R230 存 → R260 读,反向各一次)。 - -**收益重估**:Phase 1 落地后,落盘次数降到"唯一截面数"量级(几十次), -Phase 3 省下的 CPU 很小。它的真实价值转为: - -| 价值 | 说明 | +| 编译目标 | 本轮改动 | | --- | --- | -| 消除 `SAT 文本 ⇄ 几何` 这条转换链 | `region-sat-plan.md` §3.8 指出该链历史 bug 集中;新通道不经过它 | -| DWG 体积 | SAT 文本每截面 100 B ~ 数 KB,二进制 modeler 数据通常更小(需实测) | -| 首读成本 | 每个唯一截面少一次"写盘 + acisIn" | +| ARX R19.0(AutoCAD 2013-2014,本地习惯目标) | Phase 0/1/2 全部生效 | +| ARX R20.0 ~ R26.0 | 同上 | +| ZRX Z2023 ~ Z2026 | 同上 | -**成本/风险**:需要升版本号、需要兼容矩阵决策(见 §5)、依赖 INTERNAL 接口。 - -## 4. 平台与版本矩阵 + 宏方案 - -产品从 **ARX R20.0** 起支持;R190(AutoCAD 2013-2014)仅本地习惯在用;中望(ZRX)也要支持。 - -| 编译目标 | `acdbModeler*FromDwgFiler` | 本计划走哪条路 | -| --- | --- | --- | -| ARX **R19.0**(AutoCAD 2013-2014) | 无 | **保留现有 SAT 分支**(Phase 1/2 仍生效) | -| ARX R20.0 ~ R26.0 | 有 | Phase 3 可用 | -| ZRX Z2023 ~ Z2026 | 无(大概率) | **保留现有 SAT 分支** | - -**不要**在源码里比较 ARX 版本号:`cmakepresets.json` 里 ZRX 的 `CAD_SDK_VERSION` 是年份(`2023`~`2026`), -与 ARX 的 `19`~`26` 数值区间重叠,直接比较必然误判(AGENTS.md 已列为红线)。 - -改为在 CMake 里算好**能力宏**(`cmakelists.txt` 必须保持 ASCII 无 BOM): +不需要能力宏。将来若重启"换存盘通道"(附录 B),再按下面的方式加开关, +**不要**在源码里比较 ARX 版本号:`cmakepresets.json` 里 ZRX 的 `CAD_SDK_VERSION` 是年份 +(`2023`~`2026`),与 ARX 的 `19`~`26` 数值区间重叠,直接比较必然误判(AGENTS.md 红线)。 ```cmake -# 与现有 ZWCAD 判定同一处(cmakelists.txt 第 78~81 行附近) +# 将来需要时:在 cmakelists.txt 里与现有 ZWCAD 判定放在一起 if (NOT IS_ZWCAD AND CAD_SDK_VERSION GREATER_EQUAL 20) - message(STATUS ">>> 启用 modeler filer 存储通道(ARX >= 20.0)") add_compile_definitions(AEC_HAS_MODELER_FILER) endif() ``` -源码里只用能力宏(`#ifdef ZWCAD` 的既有用法不变): - -```cpp -#if defined(AEC_HAS_MODELER_FILER) - // 新通道 -#else - // 现有 SAT 文本通道(R190 / ZRX) -#endif -``` - -好处:判断只在一处(CMake,`IS_ZWCAD` 已知),源码里没有版本号比较,R190/ZRX 分支天然保留。 - -## 5. 存盘兼容性:必须先定的决策 - -新通道写出的数据,**R190 与 ZRX 插件读不懂**(这两条分支只会解析 SAT 文本/曲线)。 - -候选策略: - -| 方案 | 做法 | 优点 | 缺点 | -| --- | --- | --- | --- | -| **S1 配置切换**(推荐先做) | 默认仍写 SAT;INI/系统变量打开后才走新通道 | 一条写路径(同时只有一条生效);风险可控 | 需要用户/部署决定;两套代码都要维护 | -| S2 按 DWG 版本自动选 | `acdbGetModelerDwgVer()` / DWG 保存版本决定 | 无人工干预 | 判断依据与"谁来打开这张图"无关,仍可能误伤 | -| S3 双写 | 同时写 SAT 与 modeler 数据 | 任何版本都能读 | **明确违反 `region-sat-plan.md` §3.8「一条写路径」原则**,不推荐 | - -无论选哪个,都需要: - -- 升 `kCurrentVersionNumber`(当前管道族各版本号见各自头文件),新老读端都要能识别; -- 读端保持**三通道兼容**:曲线通道(老图)→ SAT 文本(现行)→ modeler 数据(新); -- 写端在"降级"时(配置关、或显式另存低版本)必须回到 SAT。 - -**决策点 D1**:选 S1 / S2 / S3。本文推荐 **S1**,并把默认值设为"写 SAT", -即:Phase 3 上线后**默认行为不变**,只在验证过的项目上开启。 - -## 6. 风险与回滚 +## 5. 风险与回滚 | 风险 | 影响 | 应对 | | --- | --- | --- | | 缓存母体被调用方污染 | 几何错乱 | 只发 clone;母体不去库、不外泄;Code Review 专项检查 | -| `AcDbRegion::clone()` 深拷贝语义不足 | 共享脏数据 | 用现有 `m_pRegion->clone()` 的同一语义;加单测式验证(同一母体 clone 两次分别变换,互不影响) | -| hash 碰撞 | 用错截面 | 64 位 hash + 长度参与;命中后再做一次字节比对(成本远低于重建) | +| `AcDbRegion::clone()` 深拷贝语义不足 | 共享脏数据 | 沿用现有 `m_pRegion->clone()` 的语义;验证"同一母体 clone 两次分别变换,互不影响" | +| hash 碰撞 | 用错截面 | 64 位 hash + 长度参与;可选命中后做一次字节比对(成本远低于重建) | | 内存增长 | 大图纸爆内存 | 条目数 + 总字节双上限 + LRU;打印峰值供现场反馈 | -| modeler filer 依赖 INTERNAL 接口 | SDK 升级后编译/运行异常 | Phase 3 先做 round-trip 验证;能力宏可一键关掉(回到 SAT) | -| 新格式导致老版本读不到 | 数据丢失观感 | 默认写 SAT(S1);转换前提示;保留只读兼容层 | | 回写(`RegionToString` 3.07 %)原因不明 | 白做一次序列化 | Phase 0 计数确认;若确属不必要,单独一个 PR 修掉 | -**回滚**:每个 Phase 独立可回退—— -Phase 1 由宏/配置一键关闭(走原路径);Phase 2 只改路径来源;Phase 3 由能力宏/配置关闭。 +**回滚**:Phase 1 由宏/配置一键关闭(走原路径);Phase 2 只改路径来源; +Phase 0 只在 Debug 编译进,无发布风险。 -## 7. 验证清单 +## 6. 验证清单 编译: - [ ] `R230`(日常调试目标)Debug/Release 编译通过 -- [ ] `R190` 编译通过(验证 SAT 分支仍完整) +- [ ] `R190` 编译通过 - [ ] 至少一个 ZRX preset(如 `Z2026`)编译通过 -- [ ] `cmakelists.txt` 仍为 ASCII 无 BOM;改动的 `.cpp/.h` 为 UTF-8 with BOM +- [ ] 改动涉及 `.cpp/.h` 时为 UTF-8 with BOM;`cmakelists.txt` 保持 ASCII 无 BOM 功能(CAD 实测,逐项记录): @@ -256,32 +189,142 @@ Phase 1 由宏/配置一键关闭(走原路径);Phase 2 只改路径来源 - [ ] 老图纸另存 → 重开,截面仍正确 - [ ] 复制粘贴(`copyFrom` 走 `m_pRegion->clone()`)不丢截面 - [ ] 管件、设备(`AecDbEwEquipment`)、保温(`AecDbInsulation`)截面均正确 -- [ ] Phase 3(若开启):R230 存 → R260 读、R260 存 → R230 读,双向几何一致 +- [ ] 同一母体 clone 出的两个 region 分别做不同变换,互不影响 性能(用 `tools/perf_diagsession.py`): - [ ] 同一张图、同一 preset、同一构建配置下各录一次 trace 对比 -- [ ] 关键指标:截面恢复占比、临时文件写入次数、`base_ViewportDraw` 总占比 +- [ ] 关键指标:截面恢复占比、临时文件写入次数、`base_ViewportDraw` 总占比、`AecDbX3dPart::EnsureCreated` 命中情况 - [ ] 记录墙钟耗时(点开到可操作)与 CPU 采样时间两个口径 -## 8. 未决问题 +## 7. 未决问题 1. **D0**:SAT 文本的唯一 hash 数到底是多少?(Phase 0 用计数器回答) -2. **D1**:Phase 3 的写路径策略选 S1/S2/S3?(本文推荐 S1,默认仍写 SAT) -3. 打开图纸时为什么在**回写** SAT(`RegionToString` 3.07 %)?是 `copyFrom`/深拷贝走了 `dwgOutFields`, +2. 打开图纸时为什么在**回写** SAT(`RegionToString` 3.07 %)?是 `copyFrom`/深拷贝走了 `dwgOutFields`, 还是别的原因——Phase 0 计数确认后再定是否单独立项。 -4. Phase 1 之后 `AecDbX3dPart::EnsureCreated/Rebuild` 仍每次都重建(栈上 8.4 % 却不命中)—— - 这是**另一个独立问题**,不在本计划范围内,但优先级可能更高(见 `diagsession-cpu-analysis.md` §4)。 -5. `AcDbBody::acisIn` 只有文件名版本;若将来想彻底去掉临时文件而 Phase 3 又被否, - 只剩"自行接入 ACIS API"一条路,成本与授权都需要再评估。 +3. Phase 1 之后 `AecDbX3dPart::EnsureCreated/Rebuild` 仍每次都重建(栈上 8.4 % 却不命中)—— + 这是**另一个独立问题**,优先级可能高于截面缓存(见 `diagsession-cpu-analysis.md` §4)。 +4. `AcDbBody::acisIn` 只有文件名版本;若将来想彻底去掉临时文件,只能走附录 B 的通道 + (或自行接入 ACIS API,成本与授权需另评)。 -## 9. 与既有文档的关系 +## 8. 决策记录:为什么本轮不改存盘格式 + +考虑过的方案是"**双写**":既写现有 SAT 文本,又把几何用 +`acdbModelerSaveEntityToDwgFiler` 写成一个附加块放到存盘数据末尾,新版本优先读新块, +老版本忽略尾部数据 → 兼顾"新版本更快"与"老版本可读"。 + +**暂缓的理由**: + +1. **收益不明确**。无法确定 `acdbModelerSave/RestoreEntityFromDwgFiler` 比 + `SAT 文本 → 临时文件 → acisIn` 快多少。目前能确认的收益只有一条: + **省掉临时文件,从而省掉杀软扫描时间**(用户对此有切身体会)。 +2. **而这条收益在 Phase 1 之后变得很小**。缓存落地后,落盘次数已从 O(实体数) 降到 + O(唯一截面数)(几十次),杀软影响面同步缩小。 +3. **复杂度不低**:不能升版本号(否则老版本整实体变 proxy)、读端要延后构造否则会白读一遍 SAT、 + DWG 体积翻倍、还要过一整套老版本兼容验证矩阵(附录 B)。 +4. **`region-sat-plan.md` §3.8 已确立"一条写路径"原则**,双写与之相悖,需要更强的收益理由。 + +**结论**:先把缓存做掉,用 Phase 0/1 的数据说话。等有实测证明"modeler 通道比 SAT 通道显著更快" +(或 DWG 体积成为独立诉求)时,再回到附录 B。 + +--- + +## 附录 A:SDK 能力边界(调研结论,备查) + +| 接口 | 位置 | R19.0 | R20.0+ | +| --- | --- | --- | --- | +| `AcDbBody::acisIn(const ACHAR* fileName, ...)` | `Arx/*/inc/dbBody.h:49` | 有(**仅文件名**,无 buffer 重载) | 有 | +| `acdbModelerSaveEntityToDwgFiler` / `...ToDxfFiler` | `Arx/*/inc/dbacis.h` | **无** | 有 | +| `acdbModelerRestoreEntityFromDwgFiler` / `...FromDxfFiler` | 同上 | **无** | 有 | +| `acdbGetModelerDwgVer(dwgVer, pObj)` | 同上 | 无 | 有 | + +- `Arx/` 里搜不到 `api_restore_entity_list` 等 ACIS 底层 API → **想彻底不落盘,只能用 modeler filer 通道**。 +- modeler filer 的 `modelerVersion` 参数(`<0` 跟随 DWG、`0` 当前、`>0` 指定如 `2170`)与 + `restoreOptions` 的 `kReadForeignEntities | kReadUnknownEntities`,是官方的**跨版本兼容机制**。 +- **坑**:`acdbModelerSaveEntityToDwgFiler` 要裸 `ENTITY*`,而 `AcDbBody` 只提供标 + **INTERNAL USE ONLY** 的 `getLockedASMBody()` / `unlockASMBody()` / `setASMBody()` + (`Arx/R23.0/inc/dbBody.h:38-43`;R19 也有这些函数但没有对应的 save/restore)。 + → 将来要用,必须先做 round-trip 最小验证(存/读后比对拓扑、体积、与 `acisIn` 结果一致性)。 + +## 附录 B:将来若做"双写",必须注意的点 + +### B.1 红线:绝对不能升 `kCurrentVersionNumber` + +```cpp +// AecDbEwPipeBase.cpp:938 +if ( version > AecDbEwPipeBase::kCurrentVersionNumber ) + return (Acad::eMakeMeProxy) ; +``` + +当前版本号 7。升到 8 → **老版本软件打开新图时整个实体变 proxy**,比"截面读不到"严重得多。 +做法应沿用 SAT 迁移时的既有模式(`nLen == 0` 作标志、不动版本号)。 + +### B.2 逻辑漏洞:新软件会白读一遍 SAT + +当前流布局(`dwgOutFields:1343`): + +``` +1 父类字段 → 2 version=7 → 3 m_mapSpecXData → 4 m_pBaseCurve +→ 5 ★region 块(writeUInt32(0) + SAT 文本)→ 6 m_arBreaks +→ 7 m_nInfActive / GUID / 各种 Id → 8 WriteUserInterface() → 9 ← 新块只能加在这里 +``` + +SAT 在**中间**、新块在**末尾** → 读到 SAT 时还不知道末尾有没有新块。 +若"先按 SAT 恢复、末尾再替换",等于每个截面多付一次"写盘 + acisIn", +**双写反而更慢**。 + +**修法**:延后的是**构造**,不是读取。 + +- 读到 region 块时只把 SAT 文本读进内存(不落盘、不 `acisIn`) +- 继续读完 6~8(需逐项确认不依赖 `m_pRegion`,尤其 `ReadUserInterface`) +- 末尾尝试读新块:成功 → 用它构造 `m_pRegion` 并丢弃文本;失败 → 用文本走原路径 + +### B.3 块格式建议 + +``` +magic(8B) + blockVer(2B) + modelerVer(2B) + flags(4B) + len(4B) + payload +``` + +`blockVer` 是这个块自己的版本,与 `kCurrentVersionNumber` 无关;`len` 用于跳过与自愈。 +读端任何一步失败都**静默回退**(读不到 magic 属正常老数据,不能刷日志)。 + +### B.4 必须验证的兼容矩阵 + +- 老版本插件正常打开新图 / 外部参照 / 局部打开 +- 老版本 `AUDIT` / `RECOVER` 不报错、不删实体 +- 新图 `WBLOCK` / 复制粘贴到老图 +- 老版本打开后另存 → 新版本再打开(应回落 SAT,不报错) +- 确认未触发 `eMakeMeProxy` +- DXF 路径(`acdbModeler*ToDxfFiler`)同样过一遍 +- `copyFrom` / 深拷贝(内存 filer 走同一套 `dwgOut/dwgIn`) + +### B.5 代价 + +| 代价 | 说明 | +| --- | --- | +| DWG 体积 | 几何存两份(SAT 文本 + 二进制),需实测单构件增量 × 实体数 | +| 存盘时间 | `acisOut` 与 modeler save 各跑一遍 | +| 打开 I/O | 流变长,即使跳过 SAT 也要读过去/跳过 | +| 维护面 | 两套序列化都要测;读端优先级必须明确(新块优先、SAT 兜底) | +| 老版本保存后 | 新块丢失 → 新版本回落 SAT,性能退化但不丢数据(需在文档中写明) | + +### B.6 覆盖范围 + +`AecDbEwPipeBase` 是"region 块"结构,而 `AecDbEwEquipment` / `AecDbInsulation` 走 `StringToRegion` +(文本存在普通字符串字段里),追加位置不同。建议先抽一对 helper 再推广: + +```cpp +Acad::ErrorStatus WriteSectionModelerBlock(AcDbDwgFiler*, const AcDbRegion*); +bool TryReadSectionModelerBlock(AcDbDwgFiler*, AcDbRegion*& out); // 失败返回 false,不报错 +``` + +## 附录 C:与既有文档的关系 - `AecDbEwPipeBase-region-sat-plan.md` §3.8:否决的是「**存储**按标准/异形分流」与「曲线通道写路径保留」。 - 本计划的 Phase 1 是**运行期缓存**,Phase 3 是**换一条统一写路径**(不是分流),不违背该结论。 + 本计划的 Phase 1 是**运行期缓存**,不改存储,不违背该结论。 - `GiDrawUtility-analysis.md` §7.4 E2:否决的是「同一 draw 内跨分段复用截面」, 理由是"大头是 N−1 次 boolean unite"。本计划不再碰那个分支,而是把"造截面"整体次数降下来; 同时提醒:**别指望截面缓存解决 Parasolid 的扫掠/布尔开销**,那是另一个 P0 项。 - `temp-file-naming.md`:临时文件命名/多实例规范,Phase 2 改动需遵守。 -- `sqlite_query_cache.md`:同样属于"绘制期重复查/重复造"的问题,已由 #87/#88 解决 - (trace 实测 sqlite 从 17.0 % 降到 0.06 %)。 +- `sqlite_query_cache.md`:同类问题(绘制期重复查库),已由 #87/#88 解决; + trace 实测 sqlite 从 17.0 % 降到 0.06 %,可作为"先测量、再缓存"的参考范例。 -- 2.54.0