Merge pull request 'docs: 补充管线系统样式缓存的效率提升量化分析' (#89) from company-desktop/company-desktop-computer/doc-pipe-style-cache-perf into main

Reviewed-on: #89
This commit was merged in pull request #89.
This commit is contained in:
gjm committed 2026-10-28 10:19:31 +08:00
commit a21860a632
1 file changed
+103 -1
+103 -1
View File
@@ -2,7 +2,7 @@
> 结论:这两个类本身不直接调用 sqlite。初始化阶段(`dwgInFields`,即打开图纸加载实体时)的大量 sqlite 访问全部通过 `CoreTools` 的 4 个样式查询 getter 间接产生。
>
> 状态(2026-10-27):第 7 节方案已落地到 `CoreCommand/CoreTools.cpp`,R190 Debug 下 `AecRxBase` / `CoreCommand_BAL_REMOTE` / `CoreLibrary_BAL_REMOTE` / `CoreImplement` 四个工程编译通过。详见第 12 节。
> 状态(2026-10-27):第 7 节方案已落地到 `CoreCommand/CoreTools.cpp`(PR #88 已合并),R190 Debug 下 `AecRxBase` / `CoreCommand_BAL_REMOTE` / `CoreLibrary_BAL_REMOTE` / `CoreImplement` 四个工程编译通过。详见第 12 节;量化收益见第 13 节。
---
@@ -282,3 +282,105 @@ cmake --build --preset R190 --config Debug --target CoreImplement
- 在 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 |
| 拖动画一根管 + 弯头 | 每帧都涨 | 始终不涨 |
| 平移重绘整图 | 每帧涨几百 | 不涨 |