Files
envi-code/sqlite_query_cache.md
T
company-desktop 5167e298ef docs: 补充管线系统样式缓存的效率提升量化分析
新增第 13 节,把「这次改动到底快了多少」量化下来:

- 实测 Plumbing.db(106 张表)新建连接 + 首条查询 + 关闭 ≈ 0.98 ms,
  其中 schema 解析占 ~0.74 ms;仅 open + close 只有 0.09 ms。说明代价
  不在 sqlite3_open 本身,而在新连接的首次查询。
- 按此折算改动前的开库账:dwgInFields 单实体 16 次 open ≈12~16 ms,
  绘制子实体 14 次,Jig 拖动每帧 ~20 次 ≈15~20 ms(TDbPipeLine 里的
  setInfo2Ent 没有 HasKey 判断,是无条件必付)。
- 改动后开库次数从 16×N 变成常数 2~4,O(N) 降为 O(1);端到端估算
  提升 20~60 倍,500 实体图纸加载 6~8 s → 0.1~0.4 s。
- 同时写明哪些路径受益、哪些不受益(新图纸已由 PR #87 刷过 XData,
  本来就不查库),避免高估收益。
- 补当前残留瓶颈与复核方法:剩余代价主要是 getCurrentTrade 触发的
  ReadModelXData 全量 XData 解析,以及图层/线型表每次打开;附计数
  器验证法与三个场景的预期数字。
2026-10-28 10:19:06 +08:00

19 KiB
Raw Blame History

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

两段代码几乎一模一样:

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() 改成按行业缓存:

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. 实施与验证

实施顺序:

  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 编译通过:

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 调用次数):

static int g_nQueryCount = 0;
++g_nQueryCount;
acutPrintf(_T("\n[PipeSysStyle] 实际查库次数 = %d"), g_nQueryCount);
验证场景 改前 改后
打开一张 500+ 管件的老图纸(样式键缺失) 应打几千 应 ≤ 4
拖动画一根管 + 弯头 每帧都涨 始终不涨
平移重绘整图 每帧涨几百 不涨