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

387 lines
19 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`(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<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()`)。
## 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 |
| 拖动画一根管 + 弯头 | 每帧都涨 | 始终不涨 |
| 平移重绘整图 | 每帧涨几百 | 不涨 |