新增 sqlite_query_cache.md:定位 AecDbEwPipe / AecDbEwFitting 的 dwgInFields 回填样式键时每个实体最多 16 次 sqlite3_open 的成因, 给出 4 个 getter 合并 SQL + 进程级缓存的方案、收益对比、风险取舍 与实施记录。
13 KiB
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:getColorByPipeSysgetLtByPipeSysgetLwByPipeSysgetLyByPipeSys
- 构造函数很轻(只
SetAt几个 XData 键,不碰库);dxfInFields直接返回eOk;GetPipeParam/GetFittingName等也只读内存里的m_mapSpecXData。
2. 初始化热点在 dwgInFields
AecDbEwPipe 和 AecDbEwFitting 在加载实体时,dwgInFields 里有一段补样式键的代码:
AecDbEwPipe::dwgInFields——CoreLibrary/AecDbEwPipe.cpp:990-1074AecDbEwFitting::dwgInFields——CoreLibrary/AecDbEwFitting.cpp:609-680
两段代码几乎一模一样:
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)每个都:
new CUIISQLiteServer(构造里CoInitialize)InitADOServer(...\Plumbing.db)→sqlite3_open- 调
getCurrentPipeSysTblName()(CoreTools.cpp:4912)——又新建一个 server、又 open 一次库,再SELECT [TABLENAME] FROM WPipeCfg - 自己再跑一条
SELECT [COLOR|LINETYPE|LINEWIDTH|LAYNAME] FROM ... - 析构 →
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() 改成按行业缓存:
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 里加:
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 改造成读缓存
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. 实施与验证
实施顺序:
- 先做第 7 节第一层(
CoreTools.cpp一处改动):覆盖 192 处调用点里的全部热点,dwgInFields和绘制两条路径一起解决。 - 用 MSVC 性能分析对比改前/改后的
sqlite3_open调用次数。 - 功能回归:
- 管线中心线 / 外边线的颜色、线型、线宽、图层与改之前一致;
- 画一根带法兰的管段 + 弯头 + 三通,实体显示正常;
- 打开两张图纸各画一段,确认图层/线型不串图纸;
- 打开老版本图纸(样式键缺失)加载正常。
- 编码自检:
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 编译通过:
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())。