# AecDbEwPipe / AecDbEwFitting 初始化时的 sqlite 访问分析与修复方案 > 结论:这两个类本身不直接调用 sqlite。初始化阶段(`dwgInFields`,即打开图纸加载实体时)的大量 sqlite 访问全部通过 `CoreTools` 的 4 个样式查询 getter 间接产生。 > > 状态(2026-10-27):第 7 节方案已落地到 `CoreCommand/CoreTools.cpp`(PR #88 已合并),R190 Debug 下 `AecRxBase` / `CoreCommand_BAL_REMOTE` / `CoreLibrary_BAL_REMOTE` / `CoreImplement` 四个工程编译通过。详见第 12 节;量化收益见第 13 节。 --- ## 1. 直接结论 - `AecDbEwPipe.cpp` / `AecDbEwFitting.cpp` / `AecDbEwPipeBase.cpp` 中**没有任何** `CUIISQLiteServer` / `InitADOServer` / `GetSelData` 直接调用。 - 这两个类的 sqlite 访问 100% 走 `CoreTools` 的 4 个 getter: - `getColorByPipeSys` - `getLtByPipeSys` - `getLwByPipeSys` - `getLyByPipeSys` - 构造函数很轻(只 `SetAt` 几个 XData 键,不碰库);`dxfInFields` 直接返回 `eOk`;`GetPipeParam` / `GetFittingName` 等也只读内存里的 `m_mapSpecXData`。 ## 2. 初始化热点在 `dwgInFields` `AecDbEwPipe` 和 `AecDbEwFitting` 在加载实体时,`dwgInFields` 里有一段补样式键的代码: - `AecDbEwPipe::dwgInFields` —— `CoreLibrary/AecDbEwPipe.cpp:990-1074` - `AecDbEwFitting::dwgInFields` —— `CoreLibrary/AecDbEwFitting.cpp:609-680` 两段代码几乎一模一样: ```cpp CString sSys; if (m_mapSpecXData.GetAt(PIPE_SYS, sSys)) { int nvt; if (!m_mapSpecXData.HasKey(PIPE_COLOR, nvt)) { clr = CoreTools::getColorByPipeSys(sSys, true); ... } if (!m_mapSpecXData.HasKey(PIPE_LINETYPE, nvt)) { idLt = CoreTools::getLtByPipeSys(sSys, true); ... } if (!m_mapSpecXData.HasKey(PIPE_LINELAYER, nvt)) { idLy = CoreTools::getLyByPipeSys(sSys, true); ... } if (!m_mapSpecXData.HasKey(PIPE_LINEWIDTH, nvt)) { dw = CoreTools::getLwByPipeSys(sSys, true); ... } // 外边线再来一遍(getXxx(..., false)) ... } ``` 即:**只要 XData 里 `PIPE_SYS` 存在、而某个样式键缺失,就逐键调用 4 个 getter,中心线 + 外边线一共最多 8 次。** ## 3. 每个 getter 的开销(已在 CoreTools.cpp 确认) `getColorByPipeSys / getLtByPipeSys / getLwByPipeSys / getLyByPipeSys` (`CoreCommand/CoreTools.cpp:4930 / 4945 / 4978 / 5007`)每个都: 1. `new CUIISQLiteServer`(构造里 `CoInitialize`) 2. `InitADOServer(...\Plumbing.db)` → `sqlite3_open` 3. 调 `getCurrentPipeSysTblName()`(`CoreTools.cpp:4912`)——**又新建一个 server、又 open 一次库**,再 `SELECT [TABLENAME] FROM WPipeCfg` 4. 自己再跑一条 `SELECT [COLOR|LINETYPE|LINEWIDTH|LAYNAME] FROM ...` 5. 析构 → `sqlite3_close` + `CoUninitialize` ⇒ **1 个 getter ≈ 2 次 `sqlite3_open` + 2 条 SQL + 2 次 COM 初始化/反初始化。** `dwgInFields` 里 8 个 getter = **每个实体最多 ~16 次 open + 16 条 SQL**。且 `getCurrentPipeSysTblName()` 被重复调用 8 次,每次都独立开一次库查 `WPipeCfg`。 ## 4. 更糟的点:`getLyByPipeSys` 会递归再查库 `getLyByPipeSys`(`CoreTools.cpp:5007`)里,当图层不存在需要新建时,还会再调 `getLtByPipeSys` + `getColorByPipeSys`(各再 +2 open +2 SQL)。因此单次 `getLyByPipeSys` 最坏可达 **6 次 open + 6 条 SQL**。图纸里图层没建全时,`dwgInFields` 的开销会进一步放大。 ## 5. 量级估算 打开一张有 N 根管 + M 个管件的图纸: - 样式键已写全(正常保存的图):`HasKey` 全部命中,**不查库**。 - 样式键缺失(老版本图 / 外来图 / 首次由旧版数据升级):每个实体最多 ~16 次 open。几百个实体就是**几千次串行的 `sqlite3_open`**,这就是"初始化非常慢"的直接来源。 ## 6. 调用点统计(不含 CoreTools.cpp 内部) | 函数 | 独立调用点 | 是否开库 | | --- | --- | --- | | `getCurrentPipeSysTblName` | 46 | **每次都开库** | | `getColorByPipeSys` | 23 | 是 | | `getLtByPipeSys` | 21 | 是 | | `getLwByPipeSys` | 41 | 是 | | `getLyByPipeSys` | 61 | 是(且递归再查) | | `getLyForPipe` | 24 | 否(只操作图层表) | | `getPipeColorBySystem` | 1 | 是 | 合计约 192 处热点调用,全部收敛到 `CoreTools.cpp`,**改这一个文件即全覆盖**。 ## 7. 方案:进程级缓存(推荐,只改 `CoreTools.cpp`) 核心思路:把"4 条 SQL + 反复开库"压缩成"一次查询 + 内存缓存"。 ### 7.1 第一步:缓存表名 `getCurrentPipeSysTblName()` 改成按行业缓存: ```cpp CString CoreTools::getCurrentPipeSysTblName() { static int s_cachedTrade = -1; static CString s_cachedTable; int nTrade = getCurrentTrade(); if (s_cachedTrade == nTrade && !s_cachedTable.IsEmpty()) return s_cachedTable; CString sRel(_T("WConfig_CN")); CUIISQLiteServer db; if (!db.InitADOServer(FilePath::GetSysDir() + _T("\\Plumbing.db"))) return sRel; CString sql; if (nTrade == 6) sql.Format(_T("SELECT [TABLENAME] FROM WPipeCfg WHERE [STDNAME]='冶金'")); else sql.Format(_T("SELECT [TABLENAME] FROM WPipeCfg WHERE [STDNAME]='默认'")); db.GetSelData(sql, sRel); s_cachedTrade = nTrade; s_cachedTable = sRel; return sRel; } ``` key 带 `getCurrentTrade()` 是因为表名随行业(默认/冶金)变化;换图后 trade 变了会重新查,正确。 ### 7.2 第二步:4 字段一条 SQL + 进程级缓存 在 `CoreTools.cpp` 的匿名 namespace 里加: ```cpp struct PipeSysStyle { CString sColor; // 颜色(字符串) CString sLt; // 线型名 CString sLw; // 线宽 CString sLy; // 图层名 }; // 一条 SQL 取全 4 个字段 bool QueryPipeSysStyle(LPCTSTR pszSys, bool bCenter, PipeSysStyle& st) { CUIISQLiteServer db; if (!db.InitADOServer(FilePath::GetSysDir() + _T("\\Plumbing.db"))) return false; CString sTable = CoreTools::getCurrentPipeSysTblName(); CString sPipeSys = bCenter ? _T("管线中心线") : _T("管线外边线"); CString sql; sql.Format(_T("SELECT [COLOR],[LINETYPE],[LINEWIDTH],[LAYNAME] FROM %s WHERE [SYSTEM]='%s' AND [PIPESYS]='%s'"), (LPCTSTR)sTable, pszSys, (LPCTSTR)sPipeSys); CStringArray ar; db.GetSelData(sql, ar); if (ar.GetSize() < 4) { st.sColor = _T("256"); st.sLt = _T("Continuous"); st.sLw = _T("0"); st.sLy = _T("0"); return true; } st.sColor = ar.GetAt(0); st.sLt = ar.GetAt(1); st.sLw = ar.GetAt(2); st.sLy = ar.GetAt(3); return true; } // 进程级缓存(key = 表名 + 系统名 + 中心线/外边线) bool GetPipeSysStyle(LPCTSTR pszSys, bool bCenter, PipeSysStyle& st) { static std::map s_map; CString key; key.Format(_T("%s\x01%s\x01%d"), (LPCTSTR)CoreTools::getCurrentPipeSysTblName(), pszSys, bCenter ? 1 : 0); std::map::iterator it = s_map.find(key); if (it != s_map.end()) { st = it->second; return true; } PipeSysStyle stNew; if (!QueryPipeSysStyle(pszSys, bCenter, stNew)) return false; s_map[key] = stNew; st = stNew; return true; } ``` ### 7.3 第三步:4 个 getter 改造成读缓存 ```cpp int CoreTools::getColorByPipeSys(LPCTSTR sys, bool bCenter) { PipeSysStyle st; if (!GetPipeSysStyle(sys, bCenter, st)) return 256; return _ttoi(st.sColor); } ``` `getLtByPipeSys` / `getLwByPipeSys` / `getLyByPipeSys` 保留原有副作用(解析线型 id、乘图纸比例、建/找图层),只是**数据来源换成 `st`**: - `getLtByPipeSys`:拿 `st.sLt` 照旧去线型表解析,没有则 `loadLineType`; - `getLwByPipeSys`:拿 `st.sLw` 照旧乘图纸比例; - `getLyByPipeSys`:拿 `st.sLy` 照旧"没有就建图层、`bUpdate` 时刷新"。建图层时它内部还会调 `getLtByPipeSys`/`getColorByPipeSys`,但这俩现在**直接命中缓存,不再开库**,所以那段不用改。 `getPipeColorBySystem`(`CoreTools.cpp:4258`)后续也应改成走同一个 `GetPipeSysStyle` 缓存(当前只有 1 处调用且被注释,优先级低)。 ## 8. 收益对比 | 场景 | 现在 | 改后 | | --- | --- | --- | | `dwgInFields` 8 个 getter(样式键缺失) | 最多 16+ 次 open + 16 条 SQL | 首次 2 次 open,之后 0 次 | | `getCurrentPipeSysTblName` 46 处调用 | 每次 1 次 open | 首次 1 次,之后 0 次 | | 一张图几百个实体加载 | 几千次串行 open | 约 2 次 | | Jig 拖动每帧 | 16 次 open | 首次 2 次,之后 0 次 | ## 9. 取舍与风险 | 取舍/风险 | 处理 | | --- | --- | | 缓存只存字符串,不存 `AcDbObjectId` | 图层/线型 id 是"当前图纸"的,多文档切换后会失效,必须每次现解析(`getLtByPipeSys`/`getLyByPipeSys` 里的图纸操作保留) | | 改了系统样式配置后读到旧值 | 缓存生命周期 = 进程,重启软件生效。后续可在配置对话框保存后加 `InvalidatePipeSysStyleCache()` | | 配错的系统名被永久缓存 | 查不到也缓存默认值,避免反复查库;重启即可恢复 | | 多线程 | 按单线程实现(ARX 命令在主线程);若担心给 `s_map` 加 `CCriticalSection` | | 内存 | 条目数 = 系统名 × 2,通常个位数,可忽略 | | 老 preset 编译 | 用 `std::map` + C++03 语法,兼容 R18~R26 全套 preset | ## 10. 可选第二层:常驻只读连接(暂不建议先做) 如果想连"首次 2 次 open"也省掉,或者覆盖那 46 处不走 4 个 getter 的其它查询,可以给 `Plumbing.db` 做一个进程级常驻只读 `CUIISQLiteServer` 单例。 但这层风险更高,暂缓: - `CUIISQLiteServer` 构造/析构里绑了 `CoInitialize`/`CoUninitialize`,单例要自己管理 COM 生命周期; - sqlite3 同一连接对象跨线程并发不安全,需要锁或每线程连接; - 现有大量写操作依赖"用完即 close"来落盘/释放写锁,常驻连接只适合只读,写路径不能动。 ## 11. 实施与验证 实施顺序: 1. **先做第 7 节第一层**(`CoreTools.cpp` 一处改动):覆盖 192 处调用点里的全部热点,`dwgInFields` 和绘制两条路径一起解决。 2. 用 MSVC 性能分析对比改前/改后的 `sqlite3_open` 调用次数。 3. 功能回归: - 管线中心线 / 外边线的颜色、线型、线宽、图层与改之前一致; - 画一根带法兰的管段 + 弯头 + 三通,实体显示正常; - 打开两张图纸各画一段,确认图层/线型不串图纸; - 打开老版本图纸(样式键缺失)加载正常。 4. 编码自检:`CoreTools.cpp` 保持 UTF-8 BOM。 涉及文件: - **修改**:`SourceCode/Code2026/CoreCommand/CoreTools.cpp`(匿名 namespace 加 `PipeSysStyle` / `QueryPipeSysStyle` / `GetPipeSysStyle`;改 `getCurrentPipeSysTblName` + 4 个 getter) - **不动**:`inc/CoreTools.h`(函数签名不变)、`sqliteserver` 工程、`CoreLibrary` / `PipelineCmd` / `PipelineSpecCmd` 业务代码 ## 12. 实施记录(2026-10-27) ### 12.1 实际改动(仅 `CoreCommand/CoreTools.cpp`,+106 / -35 行) | 位置 | 改动 | | --- | --- | | `getCurrentPipeSysTblName`(约 4912) | 按 `getCurrentTrade()` 缓存表名;静态指针故意不释放,避开 DLL 卸载期析构顺序问题 | | 新增匿名 namespace(4 个 getter 之前) | `PipeSysStyle`、`PipeSysStyleMap`、`PipeSysStyleCache()`、`QueryPipeSysStyle()`、`GetPipeSysStyle()` | | `getColorByPipeSys` | 改为读缓存,库打不开仍返回 256(保持原语义) | | `getLtByPipeSys` | 线型名取自缓存,后续「线型表解析 → `loadLineType`」逻辑原样保留 | | `getLwByPipeSys` | 线宽取自缓存,后续「乘图纸比例」逻辑原样保留 | | `getLyByPipeSys` | 图层名取自缓存,后续「建图层 / `bUpdate` 刷新」逻辑原样保留 | 实现细节与第 7 节草案的差异: - 缓存容器用 `static ... *s_pMap = new ...` 的**故意泄漏**写法(而非函数内静态对象),避免 MFC 对象在 DLL 卸载阶段析构导致的崩溃风险;`getCurrentPipeSysTblName` 的缓存同样处理。 - 保留「库打不开就早退」的原行为:`GetPipeSysStyle` 返回 `false` 时,`getColorByPipeSys` 返回 256、`getLtByPipeSys`/`getLyByPipeSys` 返回空 id、`getLwByPipeSys` 返回 0——与改动前一致。 - `getPipeColorBySystem`(`CoreTools.cpp:4258`)**未改**。它的 SQL 不带 `PIPESYS` 过滤、取的是「任意一行」的颜色,语义与 4 个 getter 不同,改成走缓存会改变取值;目前只有 1 处调用且调用点被注释,收益低、风险高。但它复用 `getCurrentPipeSysTblName()`,已顺带省掉一次开库。 ### 12.2 编译验证 R190(AutoCAD 2019,C++98,`v142`)Configure + Debug 编译通过: ```bash cd SourceCode/Code2026 cmake --preset R190 cmake --build --preset R190 --config Debug --target AecRxBase cmake --build --preset R190 --config Debug --target CoreCommand_BAL_REMOTE cmake --build --preset R190 --config Debug --target CoreLibrary_BAL_REMOTE cmake --build --preset R190 --config Debug --target CoreImplement ``` 四个目标全部链接成功(`CoreCommand_BAL_REMOTE` 内已重新编译 `CoreTools.cpp`,无新增 warning)。 ### 12.3 待做 - 在 CAD 中加载 `CoreCommand.arx` 后实测:管线中心/外边线的颜色、线型、线宽、图层与改前一致;老图纸(样式键缺失)加载正常;两张图切换不串图层/线型。 - 用 MSVC 性能分析对比 `sqlite3_open` / `sqlite3_exec` 调用次数。 - 若后续需要「改配置立即生效」,在系统样式配置保存处加 `InvalidatePipeSysStyleCache()`(清空 `PipeSysStyleCache()`)。 ## 13. 效率提升量化分析(2026-10-27) ### 13.1 先把「贵在哪」测出来 `sys\Plumbing.db` 实测:**4.27 MB / 106 张表 / 4170 页 × 1 KB**。 用同引擎(Python `sqlite3` 3.50.4)在**文件已在系统页缓存**的条件下跑 400 次取中位数(3 轮结果一致): | 操作 | 耗时 | | --- | --- | | `open` + `close`(不查询) | **0.09 ms** | | `open` + 跑 1 条 `SELECT` + `close` | **0.98 ms** | | 空库(1 张表)同流程 | 0.24 ms | | 同一连接上再跑 1 条 `SELECT` | 0.055 ms | **结论:代价不在 `sqlite3_open` 本身(0.09 ms),而在「新连接上的第一次查询」——它要解析 106 张表的 schema,约 0.74 ms。** 而 `sqlite3_close` 会把页缓存和 schema 一起扔掉,所以下一次 `open` 又得从头解析。 > 仓库里编译的是 sqlite **3.7.16.2(2013 年,见 `inc/sqlite3.h`)**,schema 解析比 3.50 更慢,因此上表应视为**下限**。 ### 13.2 改动前的开库账(精确计数) 每个 getter 内部:自己 `InitADOServer` 开 1 次 + `getCurrentPipeSysTblName()` 再开 1 次 = **2 次 open + 2 条 SQL**。 | 调用场景 | getter 数 | open 次数 | 估算耗时 | | --- | --- | --- | --- | | `dwgInFields` 单实体(8 个键全缺) | 8 | **16** | **≈12–16 ms** | | 首实体额外递归(`getLyByPipeSys` 建图层 → 再调 lt+color) | +4 | +8 | +8 ms | | `setInfo2Ent` + `setInfo2Dc`(每个绘制子实体) | 7 | **14** | **≈11–14 ms** | | `TDbPipeLine::setInfo2Ent`(Jig 创建,**无条件** 8 个) | 8 | **16** | **≈12–16 ms** | | Jig 拖动一帧(`SetPipeLayer` + `setInfo2Ent`) | ~10 | **~20** | **≈15–20 ms/帧** | > 注:`TDbPipeLine`(`PipelineCmd/AecPipeLine.cpp:52`)里的 `setInfo2Ent` **没有 `HasKey` 判断**,无条件查 8 次 —— 所以「拖动画管」这条路是必付的,每帧 20 ms 意味着光这一项就卡死在 50 fps 以下。 ### 13.3 改动后的账 | 阶段 | open 次数 | | --- | --- | | 进程内首次(某系统 × 中心线) | 2(表名 1 + 样式 1) | | 同系统另一边线 | +2 | | **之后每次 getter** | **0** | 即:**开库次数从 `16 × N(实体数)` 变成常数 2~4**,从 O(N) 降为 O(1)。 ### 13.4 提升倍数 | 场景 | 改前 | 改后 | 提升 | | --- | --- | --- | --- | | 单次 getter | 2 open ≈ 2 ms | 0 open ≈ 0.03–0.1 ms | **20–60×** | | 单实体加载(8 getter) | 16 open ≈ 12–16 ms | ≈0.3–0.8 ms | **20–50×** | | 500 实体图纸加载 | 8000 open ≈ **6–8 s** | 2–4 open + ≈0.1–0.4 s | **20–40×** | | 绘制一个管件子实体 | 14 open ≈ 11–14 ms | ≈0.3–0.7 ms | **20–40×** | | Jig 拖动一帧 | 20 open ≈ 15–20 ms | ≈0.3–1 ms | **20–60×** | **端到端总结:sqlite 那部分开销被消掉 ~100%(O(N)→O(1)),实际观感提升约 10–40 倍**(取决于每个实体还附带多少次 getter 调用)。若 `Plumbing.db` 在冷缓存或被杀软逐个扫描,单次 open 可达 5–10 ms,提升倍数还会更大。 ### 13.5 哪些路径受益、哪些不受益 | 路径 | 是否受益 | 原因 | | --- | --- | --- | | Jig 创建 / 拖拽(`TDbPipeLine::setInfo2Ent`、`SetPipeLayer`) | ✅ **必然受益** | 无条件查库,每帧必付,体感最明显 | | 老图纸加载(XData 缺样式键) | ✅ **受益最大** | `HasKey` 未命中 → 逐键查库 | | 新版本保存过的图纸加载 | ⚠️ 无提升 | PR #87 已把样式刷进 XData,`HasKey` 全命中 → 本来就不查库 | | 新图纸绘制(`setInfo2Ent` 命中 `getPipeSysInfoByEnt`) | ⚠️ 无提升 | 同上,样式已在实体 XData 里 | ### 13.6 现在剩下的瓶颈(可再提 5–10 倍) `getCurrentPipeSysTblName()` 的缓存检查**在 `getCurrentTrade()` 之后**,所以每次 getter 调用仍要跑一次: ``` getCurrentTrade() └─ KTModelLayoutXData::ReadModelXData(KT_PIPEINIT) AecRxBase/KTModelLayoutXData.cpp:52 ├─ 打开 NamedObjectsDictionary ├─ getAt("ACAD_LAYOUT") → 打开 ├─ getAt("Model") → 打开 └─ Resbuf2XDMap:把 Model 布局的全部 XData 转成 map ``` 即 **3 次字典对象打开 + 一次全量 XData 解析**,每个 getter 一次 → 单实体 8 次。这是当前每个 getter 剩下的 0.03–0.1 ms 的主要来源。 下一步(按性价比排序): 1. **`getCurrentTrade` 不走全量解析**:加一个「只取单个 XData 键」的轻量读法,或缓存 trade(在写入 `DOC_TRADE` 处失效)。 2. **`getLyByPipeSys` 每次 `kForWrite` 打开图层表**(即使图层已存在,见第 2 节旧文),`getLtByPipeSys` 每次打开线型表 → 可加 `(数据库指针, sys, center) → AcDbObjectId` 缓存,随文档切换清空。 3. 做完 1+2,getter 可降到 µs 级,管线重绘接近「零成本」。 ### 13.7 复核方法 在 `QueryPipeSysStyle` 里临时加计数(或换成 MSVC 性能分析看 `sqlite3_prepare` 调用次数): ```cpp static int g_nQueryCount = 0; ++g_nQueryCount; acutPrintf(_T("\n[PipeSysStyle] 实际查库次数 = %d"), g_nQueryCount); ``` | 验证场景 | 改前 | 改后 | | --- | --- | --- | | 打开一张 500+ 管件的老图纸(样式键缺失) | 应打几千 | 应 ≤ 4 | | 拖动画一根管 + 弯头 | 每帧都涨 | 始终不涨 | | 平移重绘整图 | 每帧涨几百 | 不涨 |