docs: 记录性能部分 4.2~4.6 的结论 #56

Merged
gjm merged 1 commits from company-desktop-computer/docs/ewequipment-perf-decisions AGit into main 2026-10-25 13:01:37 +08:00
+72 -33
View File
@@ -498,14 +498,14 @@ for (...) {
成本主要在 3:N 个单元 = N 次造体 + (N−1) 次布尔,与 `AecDbX3dPart` 的结论一致。 成本主要在 3:N 个单元 = N 次造体 + (N−1) 次布尔,与 `AecDbX3dPart` 的结论一致。
### 4.2 P0:拖拽时缓存不清理,且每帧全量重建 ### 4.2 P0:拖拽时缓存不清理,且每帧全量重建(已修复,PR #53)
```cpp ```cpp
if (!dc.IsDragging()) // :3036 if (!dc.IsDragging()) // 改前
{ {
if (Has3DBuffer()) { 画缓存; return; } if (Has3DBuffer()) { 画缓存; return; }
} }
Draw3dSolid(&dc, nColor); // :3062 拖拽时每帧都走到这里 Draw3dSolid(&dc, nColor); // 拖拽时每帧都走到这里
``` ```
而 `Add3DBuffer`(`AecDbEntityCommon.cpp:146`)只是 `append`,**不清理已有内容**。于是拖拽过程中: 而 `Add3DBuffer`(`AecDbEntityCommon.cpp:146`)只是 `append`,**不清理已有内容**。于是拖拽过程中:
@@ -514,44 +514,81 @@ Draw3dSolid(&dc, nColor); // :3062 拖拽时每帧都走到这里
- 每帧往 3D 缓存里**再追加** N 个 - 每帧往 3D 缓存里**再追加** N 个
- `Draw3DBuffers` 把缓存里所有实体都画一遍 - `Draw3DBuffers` 把缓存里所有实体都画一遍
第 k 帧要造 N 个体、画 k×N 个体。内存随帧数线性涨、帧率随帧数线性掉,拖一次长距离夹点就能看到明显卡顿和内存上冲。 第 k 帧要造 N 个体、画 k×N 个体。内存随帧数线性涨、帧率随帧数线性掉。
这是当前最值得修的性能问题。可选做法(按改动量排序): **2026-10-25 已修复(PR #53)**:去掉 `!dc.IsDragging()` 判断,拖拽时也直接走 3D 缓存。原先写这个判断的原因是「解开后崩溃」,实际测试并不崩溃——崩溃来自 4.2 的缓存无限追加,而不是拖拽本身。缓存长度不再随帧数增长,`Draw3dSolid` 拖拽期间一次都不调用。
1. 进入拖拽前 `Clean3DBuffer()`,拖拽结束后再清一次 —— 一行改动,先止住缓存膨胀 原计划的另外两个做法(拖拽时画线框、增量 `transformBy`)不需要了。
2. 拖拽时画包围盒/线框(用 `GetInfFaceInf` 那套参数直接算 bbox),不造 ACIS
3. 拖拽时只对缓存里的 solid 施加增量矩阵(`transformBy(delta)`),不重建
### 4.3 P1:2D 符号绘制触发 3D 布尔 ### 4.3 P1:2D 符号绘制触发 3D 布尔(暂缓)
11 处 `Draw*Vlv2d` 里调 `GetBodyBox(ext)`(`:5020` `:5189` `:5357` `:5512` `:5675` `:6307` `:6492` `:6654` `:6816` `:6996` `:7147`),而 `GetBodyBox:4205` 在没有 3D 缓存时会 `Draw3d` —— 也就是**为了画一个 2D 阀门手柄的方向,把所有单元造一遍体再做布尔**。 11 处 `Draw*Vlv2d` 里调 `GetBodyBox(ext)`(`:5020` `:5189` `:5357` `:5512` `:5675` `:6307` `:6492` `:6654` `:6816` `:6996` `:7147`),而 `GetBodyBox` 在没有 3D 缓存时会 `Draw3d` —— 为了画一个 2D 阀门手柄的方向,把所有单元造一遍体再做布尔。
`Draw2d` 本来就是轻量路径(平面符号),这条依赖把它变成了重路径。设备数量多、缩放频繁时开销明显。 **2026-10-25 核实:原文的「缩放频繁时开销明显」写错了。** `Draw2d` 开头 `if (Has2DBuffer())`(`:2545`)就直接画缓存并返回,根本走不到 `Draw*Vlv2d`。缩放、平移、重生成都不会清缓存,只有改参数(`clearGeoBuffer`)、图层变化等才会。
建议:给 `XPrimData` 增加一个按参数解析计算 bbox 的函数(box/cylinder/cone/nozz 这几种占绝大多数,参数都在 `m_pData` 里,`GetInfFaceInf` 已经在做类似的事),2D 路径只算参数 bbox,不碰 ACIS。 所以准确的触发条件是:**2D 缓存失效后的第一次绘制**,且此时 3D 缓存也为空(平面图场景下 3D 缓存一直是空的)。是一次性开销,不是每次绘制的持续开销。
### 4.4 P1:`GetInterface` 每次 `acedGetVar` + 临时实体 结论:暂缓。真要优化,收益只在「改完参数后立刻看平面图」这一下,不值得为此把 25 个 `Draw*2d` 的取尺寸逻辑改成参数 bbox。
- `:3237` 和 `:3300` 各调一次 `acedGetVar("INSUNITS")`,而 `GetInterface` 在绘制、夹点、osnap、剖切里共 67 处调用,很多在循环内。`INSUNITS` 是数据库级常量,可以缓存(`CoreTools::IsMetersUnit()` 已有类似封装) ### 4.4 P1:`GetInterface` 每次 `acedGetVar` + 临时实体(只做矩阵复用)
- `GetInfFaceInf` 每个面调两次 `GetTransformbyXform`(`:12160` `:12225`),每次都要 `new`/构造临时 `AcDbLine` / `AcDbPoint` 再走 `TransformEntity`。一个 6 面 box = 12 个临时实体;夹点显示时每个管嘴都要算
- 建议:把"单元 → 世界矩阵"提成一个函数,一次算好,`GetInfFaceInf` / `GetTransformbyXform` 都复用它
### 4.5 P1:`GetDraw2dables` 每次建 `*U` 块 + 跑 HLR - `acedGetVar("INSUNITS")` 每个重载各调一次。**2026-10-25 决定不缓存**:`acedGetVar` 读的是数据库级变量,开销很小,而且当前图纸单位可以在会话中途变化(切换图纸、`INSUNITS` 被改),缓存反而要处理失效
- `GetInfFaceInf` 每个面调两次 `GetTransformbyXform`,每次构造一个临时 `AcDbLine` / `AcDbPoint` 再走 `TransformEntity`。一个 6 面 box = 12 个临时实体;夹点显示时每个管嘴都要算
`:2909` 调 `CKTCadUtility::CreateBlockTableRecord(ar, ptIns, NULL, ...)`,`pszBlk == NULL` 时会在当前图纸里**新建一个匿名块表记录**(`KTCadUtility.cpp:537`)。也就是说一次"取 2D 图形"的查询操作会往 DWG 里写数据:块表增长、undo 记录、文件变大。 **「单元 → 世界矩阵」是什么**:`TransformEntity`(`:1552`)对实体做的是一串矩阵变换,顺序固定:
同时 `OrthoProjected3DSolid` 走的是完整 HLR(`HlrCommands.cpp:214`),每个 solid 一次。 ```cpp
rotation(m_ptRotation.x, kXAxis) // 绕 X 轴转
rotation(m_ptRotation.y, kYAxis) // 绕 Y 轴转
rotation(m_ptRotation.z, kZAxis) // 绕 Z 轴转
translation(m_vtMove) // 平移到单元自身的偏移
m_arMatrix[i] // 逐项累乘附加矩阵
rotation(按 vtDir 先绕 Z 再绕法向) // 转到插入方向
translation(ptIns) // 平移到插入点
rotation(90°, kXAxis, ptIns) // 竖放时再翻一次
```
导出场景可以接受,但若被绘制路径复用就不合适。建议: 这串变换只依赖 `(pData, ptIns, vtDir, bVert)`,与具体是什么实体无关。现在为了变换「一个点」或「一个方向」,要先把它们塞进一个临时实体、跑完这一串 `transformBy`、再把结果读出来——矩阵算了一遍又一遍。
- 只读查询走"投影曲线直接返回",不建块 所谓「提成世界矩阵」,就是把这串矩阵按同样顺序 `postMult` 合成一个 `AcGeMatrix3d M`,之后直接 `ptGe.transformBy(M)` / `vtDir.transformBy(M)`。`GetInfFaceInf` 里 12 次临时实体可以全部去掉,只剩 12 次向量乘法。
- 需要块时由调用方决定,函数只负责给实体
### 4.6 P1:开图路径 这条**尚未实现**,等 4.6 一起评估。
`dwgInFields:1791` 无条件 `subDMesh2NewSolid()`,对每个 `kSubDMesh` 单元做一次 `ParseRevitFamily::parseJson`(`:3142`)。Revit 族转来的设备,这一步是开图耗时的大头之一,而且结果只用于显示。 ### 4.5 P1:`GetDraw2dables` 每次建 `*U` 块 + 跑 HLR(暂缓)
可以照 `AecDbX3dPart` 的做法改成惰性:开图只记标志,首次绘制/取几何时再解析。前提是先修 2.4(现在的删除逻辑会把数组搞错)。 调 `CKTCadUtility::CreateBlockTableRecord(ar, ptIns, NULL, ...)`,`pszBlk == NULL` 时会在当前图纸里**新建一个匿名块表记录**(`KTCadUtility.cpp:537`)。也就是说一次"取 2D 图形"的查询操作会往 DWG 里写数据:块表增长、undo 记录、文件变大。同时 `OrthoProjected3DSolid` 走完整 HLR(`HlrCommands.cpp:214`),每个 solid 一次。
**2026-10-25 核实**:
- 剖切功能已经不再调用它(可以确认)
- 但它是 `AecDbEntityCommon` 的虚函数,`CoreCommand/KTEarthWormCmd.cpp` 里有十几处直接调用(`:6776` `:7884` `:12377` 等),`AecDbEntityCommon.cpp:1137` 的 `entitySet` 路径也在用,用途不止剖切
所以「剖切不用了」不等于「可以改语义」。暂不动,等确认其余调用方各自的期望。
### 4.6 P1:开图路径(待评估,未实施)
`dwgInFields` 里无条件 `subDMesh2NewSolid()`,对每个 `kSubDMesh` 单元做一次 `ParseRevitFamily::parseJson`(`parseFamilyForRevit.cpp:1234`:RapidJSON 解析 + 遍历生成 `NewSolidBase`)。Revit 族转来的设备,这一步是开图耗时的大头之一。
**2026-10-25 评估结论:不能照 `AecDbX3dPart` 的方式惰性化。** 两者的性质不同:
| | `AecDbX3dPart` | `AecDbEwEquipment` |
| --- | --- | --- |
| 懒加载的对象 | `m_pSolid`,布尔运算的**派生结果** | `m_arSubPrimPtrs`,**持久化数据本体** |
| 延迟的代价 | 首次绘制时重算一次 | 对象在解析前处于「半初始化」状态 |
| 数组内容 | 不变 | `kSubDMesh` 被替换成若干新单元,**长度和顺序都变** |
具体风险:
1. `m_arSubPrimPtrs` 在本文件有 **99 处引用、约 40 个读函数**(`GetInterface` ×2、`GetInterfaceCount`、`Draw3dSolid`、`ImpGetGripPoints`、`GetSubPrimObj*`、`ActiveInterface`、`NozzhasFlage`、13 个 `Draw*2d`、`PrimPorjectToPlan`、`ToJson` …),每一处都要在开头调 `EnsureSubDMeshParsed()`,漏一个就是「少管嘴 / 少几何 / 夹点错位」
2. **索引语义会变**:`GetSubPrimObj(n)`、`SetActiveSubPrimObj(n)`、`RemoveSubPrimObj(n)` 的下标在解析前后指向不同单元。用户如果在解析前拿到下标,解析后再用,就删错/选错
3. `copyFrom` / `ByJson` 拷的是「当前数组」,源对象没解析时目标也没解析,需要在拷贝时正确传递标志
4. 收益不确定:`AcDb` 开图后可能立刻调 `subGetGeomExtents`,那样惰性化等于没省
**建议的替代路径**(按优先级):
1. 先量:在真实图纸上统计「开图总耗时」与 `subDMesh2NewSolid` 的累计耗时。占比不到 20% 就不值得动
2. 若确实是大头,优先做「解析结果缓存」:以 `m_pData` 内容的 hash 为 key 缓存解析出的 `XPrimData` 数组,第二次遇到同一个族直接深拷贝。语义完全不变,风险最低
3. 最后才考虑惰性化,且必须把所有读入口一次改全
### 4.7 度量建议 ### 4.7 度量建议
@@ -608,13 +645,15 @@ Draw3dSolid(&dc, nColor); // :3062 拖拽时每帧都走到这里
### 5.3 第三批:性能(先量后改) ### 5.3 第三批:性能(先量后改)
| 优先级 | 改动 | 预期 | | 优先级 | 改动 | 预期 | 状态 |
| --- | --- | --- | | --- | --- | --- | --- |
| 高 | 拖拽时清缓存 + 简化显示(4.2) | 拖拽帧率、内存峰值 | | 高 | 拖拽时走缓存,不重建(4.2) | 拖拽帧率、内存峰值 | 已完成(PR #53) |
| 高 | 2D 路径改用参数 bbox,不触发 3D(4.3) | 2D 重生成、缩放 | | 高 | 2D 路径改用参数 bbox,不触发 3D(4.3) | 一次性开销,收益有限 | 暂缓 |
| 中 | `INSUNITS` 缓存 + 单元矩阵复用(4.4) | 夹点、osnap | | 中 | 单元矩阵复用,去掉临时实体(4.4) | 夹点、osnap | 待做 |
| 中 | `GetDraw2dables` 不建块(4.5) | 导出、剖切、DWG 体积 | | 中 | `GetDraw2dables` 不建块(4.5) | 导出、DWG 体积 | 暂缓,调用方未确认 |
| 低 | `subDMesh2NewSolid` 惰性化(4.6) | 开图 | | 低 | `subDMesh2NewSolid` 惰性化(4.6) | 开图 | 待量,不建议按原方案做 |
`INSUNITS` 缓存(4.4 的第一条)已决定不做。
### 5.4 需要先确认的事 ### 5.4 需要先确认的事
@@ -668,4 +707,4 @@ Draw3dSolid(&dc, nColor); // :3062 拖拽时每帧都走到这里
2. ~~第二批(`m_Combins` 存盘清空)~~ —— 已完成(PR #48),见 3.1、5.2 2. ~~第二批(`m_Combins` 存盘清空)~~ —— 已完成(PR #48),见 3.1、5.2
3. ~~第一批 6~11(逻辑)~~ —— 已完成,范围扩到 3.2~3.14 共 10 项,见 5.1 3. ~~第一批 6~11(逻辑)~~ —— 已完成,范围扩到 3.2~3.14 共 10 项,见 5.1
4. ~~确认 5.4 剩下的问题~~ —— 3.7 已改(见 3.7),3.12 确认是故意的、不改,3.6 暂缓 4. ~~确认 5.4 剩下的问题~~ —— 3.7 已改(见 3.7),3.12 确认是故意的、不改,3.6 暂缓
5. 第三批按 4.7 度量结果排序,一项一个 PR 5. 第三批:4.2 已完成(PR #53);4.4 的矩阵复用待做;4.3 / 4.5 / 4.6 按 4.x 的结论暂缓或待量