Files
envi-code/sqlite_query_cache.md
T
company-desktop b3923d3861 docs: 记录管线/管件初始化 sqlite 访问的分析与缓存方案
新增 sqlite_query_cache.md:定位 AecDbEwPipe / AecDbEwFitting 的
dwgInFields 回填样式键时每个实体最多 16 次 sqlite3_open 的成因,
给出 4 个 getter 合并 SQL + 进程级缓存的方案、收益对比、风险取舍
与实施记录。
2026-10-28 10:09:26 +08:00

285 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AecDbEwPipe / AecDbEwFitting 初始化时的 sqlite 访问分析与修复方案
> 结论:这两个类本身不直接调用 sqlite。初始化阶段(`dwgInFields`,即打开图纸加载实体时)的大量 sqlite 访问全部通过 `CoreTools` 的 4 个样式查询 getter 间接产生。
>
> 状态(2026-10-27):第 7 节方案已落地到 `CoreCommand/CoreTools.cpp`,R190 Debug 下 `AecRxBase` / `CoreCommand_BAL_REMOTE` / `CoreLibrary_BAL_REMOTE` / `CoreImplement` 四个工程编译通过。详见第 12 节。
---
## 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<CString, PipeSysStyle> s_map;
CString key;
key.Format(_T("%s\x01%s\x01%d"),
(LPCTSTR)CoreTools::getCurrentPipeSysTblName(), pszSys, bCenter ? 1 : 0);
std::map<CString, PipeSysStyle>::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()`)。