Files
envi-code/AecDbEwEquipment-analysis.md
T
company-desktop-computer 41397130e2 refactor: 统一 XPrimData::m_pData 为字节数组,releaseData 只保留 delete[]
releaseData() 原来按 m_eType 分两路(8 种 delete[],其余 delete),但同一类型
在内存里可能是对象也可能是字节数组:开图、JSON 导入、深拷贝都分配 new byte[],
而对话框里 setData<T> 的 default 分支和几处直接 new 分配的是对象。按类型判断
必然有一半场景不匹配。

- 13 处对象形式分配统一改成 new unsigned char[sizeof(T)],后续 (T*)m_pData
  的用法不变
- releaseData() 去掉 switch,只保留 delete[];~XPrimData() 改为先调 releaseData(),
  顺带修掉 m_Combins 子单元的 m_pData 泄漏
- AecDbEwEquipment::FreeSubPrimData() 取代 FreeBuffer(CPtrArray&),改成成员函数,
  不再需要参数
- 另外三个文件作用域的 FreeBuffer/FreeBuffer2/FreeBuffer3 改名为 FreeEntityArray/
  FreeComplexCurveArray/FreeFaceplanArray,并加 static 收回内部链接
- 删除 ReleaseXPrimData,直接 releaseData() + DELETE_PTR
2026-10-23 10:29:31 +08:00

615 lines
32 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.
# AecDbEwEquipment 分析:内存、缺陷与性能
> 目标:在动手改之前,把 `AecDbEwEquipment`(阀门/设备通用实体)的内存问题、潜在缺陷和性能热点列清楚,给出可分批落地的方案。
>
> 范围:`SourceCode/Code2026/inc/AecDbEwEquipment.h`(578 行)、`SourceCode/Code2026/CoreLibrary/AecDbEwEquipment.cpp`(14290 行)。关联的 `AecEqui::XPrimData`(`inc/EarThwormDef.h:2526`)、`AecDbEntityCommon` 的 2D/3D 缓存(`AecRxBase/AecDbEntityCommon.cpp`)只在必要时提及。
>
> 状态(2026-10-22):仅分析,未改代码。行号以当前 `main` 为准。
---
## 1. 类职责与数据模型
### 1.1 职责
`AecDbEwEquipment` 继承 `AecDbEntityBase` + `AecMInterfaces`,是"阀门、设备"这类**参数化拼装体**的宿主:
- 自身只存插入点 `m_ptInsert`、方向 `m_vtDir`、专业数据 `m_mapSpecXData`、两个图层软指针 `m_idDash` / `m_idCenter`、竖放标志 `m_bVert`
- 几何是 `m_arSubPrimPtrs`(`CPtrArray`,元素是堆上的 `AecEqui::XPrimData*`),每个单元描述一个基本体(box / cylinder / cone / revolve / nozzle / extrude / Revit 组合 …)
- 3D 显示时逐个 `Draw3dSolid()` 造 `AcDb3dSolid`,按 `m_bBoolSubtract` 分成并集/差集两组,做布尔后放进 3D 缓存
- 2D 显示时按阀门类型画符号(25 个 `Draw*2d`),或把 3D 单元投影到平面(`PrimPorjectToPlan`)
### 1.2 几何单元 `XPrimData`
```cpp
struct XPrimData {
short m_eType; // 基本体类型
PVOID m_pData; // 参数块(new byte[n] 或 new 具体结构体)
int m_nSize; // 参数块字节数
...
std::vector<XPrimData*> m_Combins; // 组合设备的子单元
};
```
两个关键点,后文大量引用:
| 事实 | 位置 | 影响 |
| --- | --- | --- |
| `~XPrimData()` 只删 `m_Combins`,**不删 `m_pData`** | `EarThwormDef.h:2539` | 删除 `XPrimData` 前必须手动 `releaseData()`,否则参数块全泄漏 |
| `releaseData()` 按类型区分 `delete[]` / `delete` | `EarThwormDef.h:2544` | 释放方式与类型绑定,不能统一 `delete[]` |
### 1.3 数据流
```
开图 dwgInFields:1693 ──► dwgInFieldsForXPrim:172(递归读 m_Combins)
└─► subDMesh2NewSolid:3145(Revit 子网格 → 参数化单元)
存盘 dwgOutFields:1807 ──► dwgOutFieldsForXPrim:239(递归写 m_Combins)
绘制 Draw2d:2491 ──► MakeCurves:11063 ──► PrimPorjectToPlan:8314(逐单元投影)
Draw3d:3002 ──► Draw3dSolid:3905 ──► Draw3dSolid(XPrimData):3526(逐单元造体)
└─► 布尔并/减 ──► Add3DBuffer
导出 GetDraw2dables:2782 / GetDraw3dables:2928(克隆 + 投影/并体 + 写 BIM XData)
```
---
## 2. 内存问题
### 2.1 P0:`m_pGripAppData` 在 `dwgInFields` 里重复分配
| 位置 | 代码 |
| --- | --- |
| 构造 `:1682` | `m_pGripAppData = new AecGripData[GRIP_MAXNUM];` |
| `dwgInFields:1707` | `m_pGripAppData = new AecGripData[GRIP_MAXNUM];` ← 又分配一次,旧指针直接丢弃 |
| 析构 `:1690` | `delete [] m_pGripAppData;`(只删最后一块) |
`GRIP_MAXNUM` 为 256(`EarThwormDef.h:163`),`AecGripData` 含 `szTip[256]` + `szID[256]` + `szLockId[39]` + 3 个 `AcGePoint3d` + 1 个 `AcGeVector3d`,`sizeof` 约 1.2 KB。**每个设备实体开图即泄漏约 300 KB**,1000 个设备 ≈ 300 MB,且每次重开图纸累加。
同一处还有两个次要泄漏:
- `:1709` 读版本号失败、`:1712` 版本过高返回 `eMakeMeProxy` 时,新数组同样丢弃
- `:1720` `pFiler->readString(&pszTmp)` 分配的字符串没有 `acutDelString(pszTmp)`(对比 `AecDbEwFitting.cpp:639` 的正确写法),每次开图泄漏一个 GUID 长度的小块
**修复**:删掉 `:1707` 的重复分配(构造函数已经分配过);`readString` 用完补 `acutDelString`。改动 3 行,收益最大。
### 2.2 P0:析构函数不释放几何单元
```cpp
AecDbEwEquipment::~AecDbEwEquipment(void) // :1686
{
Clean3DBuffer();
Clean2DBuffer();
delete [] m_pGripAppData;
} // m_arSubPrimPtrs 完全没处理
```
`CPtrArray` 析构不会删除元素,`~XPrimData` 也不删 `m_pData`。所以**每个被销毁的设备实体,泄漏全部几何单元 + 参数块**。开图后关图、复制粘贴、`clone()` 的临时对象销毁,都走这条路。
同类 `AecDbEwFitting` 的析构(`:584`)同样没处理它的子单元数组,属于同一批遗留写法。
**修复**:析构里补 `FreeSubPrimData();`(成员函数,见 2.3 与 2.13)。
### 2.3 P0:`FreeBuffer(CPtrArray&)` 只释放数据块,不释放结构体
```cpp
void FreeBuffer(CPtrArray& arSubPrimPtrs) // :1520
{
for (...) {
AecEqui::XPrimData* pData = (AecEqui::XPrimData*)arSubPrimPtrs.GetAt(i);
if (pData != NULL) {
if (pData->m_nSize > 0)
delete[](byte*)pData->m_pData; // 只删 m_pData
}
}
arSubPrimPtrs.RemoveAll(); // 结构体本身没删
}
```
被 `copyFrom:2181` 调用,于是 `copyFrom` 每次都泄漏被覆盖的那批 `XPrimData` 结构体。修 2.2 时会连带放大这个问题。
另外这里无条件 `delete[](byte*)`,而 `releaseData()` 是按类型区分 `delete[]` / `delete` 的。
**核查 `m_pData` 的全部分配点后,结论是 `releaseData()` 的类型判断不可靠**——同一个类型在内存里可能是对象,也可能是字节数组:
| 来源 | 分配形式 |
| --- | --- |
| `dwgInFieldsForXPrim:206`(开图) | `new byte[n]`,所有类型 |
| `byJson:833`(JSON 导入) | `new PVOID[n]`,所有类型 |
| `copyXPrimData:150`(深拷贝) | `new byte[n]`,所有类型 |
| `CoreTools.cpp:2036` / `:4565` / `:4662` | `new byte[]` / `new char[]` |
| 各命令模块的 `setData<T>`(8 处) | `kExtrude`/`kRevolve` 用 `new byte[]`,其余 `new T` |
| `PipelineCmd/command.cpp:18670` 等 5 处 | `new AecEqui::XNozz` / `new AecEqui::XBOX` |
例如 `kBox` 从对话框创建时是 `new AecEqui::XBOX`,开图读回时却是 `new byte[]`。按 `m_eType` 判断,必然有一半场景不匹配(MSVC 下 POD 数组的 `delete` 与 `delete[]` 实际都走 `free`,所以一直没暴露)。
**改法:统一表示,再统一释放规则。**
1. 把 13 处对象形式的分配改成 `new unsigned char[sizeof(T)]`(`sizeof(T)` 与原来一致,后续 `(T*)m_pData` 的用法不变)
2. `releaseData()` 去掉 switch,只保留 `delete[] (unsigned char*)m_pData`
3. `~XPrimData()` 改为先调 `releaseData()`,顺带修掉 `m_Combins` 子单元的 `m_pData` 泄漏
这样 `AecDbEwEquipment.cpp` 里的 `ReleaseXPrimData` 就不需要了,直接 `pData->releaseData(); DELETE_PTR(pData);`。
### 2.4 P0:`subDMesh2NewSolid()` 删错元素,且泄漏
```cpp
void AecDbEwEquipment::subDMesh2NewSolid() // :3145
{
CPtrArray ar, subDs;
std::vector<int> idxs(0);
for (int i(0); i < m_arSubPrimPtrs.GetCount(); ++i) {
AecEqui::XPrimData *p = (AecEqui::XPrimData *)m_arSubPrimPtrs[i];
if (p->m_eType == AecEqui::kSubDMesh) {
idxs.push_back(i); // 记录的是"原始下标"
subDMesh2NewSolid(p, ar);
}
}
for (size_t i(0); i < idxs.size(); ++i)
m_arSubPrimPtrs.RemoveAt(i); // ← 用的是 i,不是 idxs[i]
...
}
```
两个错误叠加:
1. **删错元素**:`RemoveAt(i)` 删的是第 0、1、2… 个元素,而不是记录的 SubDMesh 下标。只要有一个 SubDMesh,就会把**第一个**正常单元删掉,SubDMesh 本身反而留着(后续 `Draw3dSolid` 对 `kSubDMesh` 是 `assert(0)`)。
2. **泄漏**:`RemoveAt` 只摘指针,被摘掉的 `XPrimData` 及其 `m_pData` 全部泄漏。
正确写法是按下标**从大到小**删,并先 `releaseData()` + `DELETE_PTR`。
这条只在"图纸里含 Revit 子网格单元"时触发,但一旦触发就是几何丢失,属数据正确性问题。
### 2.5 P1:`RemoveSubPrimObj` / `RemoveSubPrimObjByID` 不调 `releaseData()`
`:4276` 和 `:4291` 都是 `DELETE_PTR(pSolid)`,而 `~XPrimData` 不删 `m_pData`。删一个单元泄漏一个参数块(box/cylinder 几百字节,Revit 单元可能几十 KB)。
### 2.6 P1:`GetDraw2dables` 克隆后丢弃
`:2843` 对 2D 缓存里每个实体 `clone()`,然后:
- `AcDb3dSolid` → `OrthoProjected3DSolid` 后 `continue`(`:2850`)
- `AcDbSubDMesh` → `OrthoProjected3DSubMesh` 后 `continue`(`:2857`)
`OrthoProjected3DSolid` 只是把传入实体喂给 HLR 收集器(`CoreCommand/HlrCommands.cpp:214`),**不接管所有权**。这两条 `continue` 的克隆体全部泄漏,每次调用泄漏 N 个 solid。
### 2.7 P1:剖切相关的克隆泄漏
| 位置 | 问题 |
| --- | --- |
| `GetSectionDraw:12691` | `return OldGetSectionDraw(...)` 时没有 `delete pFit`(`:12677` 克隆的整份设备) |
| `OldGetSectionDraw:12620` | 逐个子实体 `clone()`,只对 `AcDb3dSolid` 做了投影,非 solid 的克隆体既不进结果也不释放 |
| `OldGetSectionDraw:12609` | `GetDraw3dables(arSubPtrs,false)` 产出的整组克隆从头到尾没释放 |
剖切是低频操作,但一次剖切可能泄漏几十个实体。
### 2.8 P1:`GetClosestPointToSUbPrim` 泄漏 region,并写库
```cpp
AcDbObjectId idSH = CoreTools::AddToModelSpace(pSolid); // :3986 把临时体写进模型空间
...
pFace->convertToRegion(regions); // :4032 新建 region
for (...) { ... } // regions 里的对象从不释放
delete pEnt; // :4049 只删了 explode 出来的本体
```
两个问题:
1. `convertToRegion` 产出的 region 全部泄漏
2. 为了算一个最近点,把临时 solid **加进当前图纸的模型空间**再 erase。这会触发数据库事务、undo 记录、反应器,是明显的性能与副作用问题
### 2.9 P2:`SortAecComplexCurves` 丢弃空曲线
`:11230` 先把非空曲线挑进 `std::vector`,然后 `arPrimCplPtrs.RemoveAll()` 再回填。**空曲线指针被直接丢掉,没有 delete**。`PrimPorjectToPlan:8507` 每次都调用,投影为空的单元越多,泄漏越多。
### 2.10 P2:`ByJson` 失败路径泄漏
`:12853` `new XPrimData`,`byJson` 返回 false 时不入数组也不删除,连带已解析出来的 `m_pData` 一起泄漏。另外 `ByJson` 没有先清空 `m_arSubPrimPtrs`,对已有几何的对象再导入会叠加重复单元。
### 2.11 P2:`MakeBlockRefSet` 失败路径泄漏
`:12540` `new AcDbBlockTableRecord`,若 `getBlockTable` 失败(`:12559`)或 `pTbl->add` 失败,`pRec` 既不关闭也不删除。`appendAcDbEntity` 失败时(`:12552`)实体也没关闭。
### 2.12 内存问题汇总
| 级别 | 问题 | 位置 | 触发频率 | 单次量级 | 状态 |
| --- | --- | --- | --- | --- | --- |
| P0 | 夹点数组重复分配 | `:1707` | 每个实体每次开图 | ~300 KB | 已修 |
| P0 | 析构不释放几何单元 | `:1686` | 每个实体销毁 | 全部参数块 | 已修 |
| P0 | `FreeBuffer` 不删结构体 | `:1520` | 每次 `copyFrom` | N 个结构体 | 已修 |
| P0 | `subDMesh2NewSolid` 删错+泄漏 | `:3159` | 含 SubDMesh 的图 | N 个单元 + 几何错乱 | 已修 |
| P1 | `RemoveSubPrimObj*` 漏 `releaseData` | `:4276` `:4291` | 编辑删除单元 | 1 个参数块 | 已修 |
| P1 | `GetDraw2dables` 克隆泄漏 | `:2850` `:2857` | 2D 导出/剖切 | N 个 solid | 已修 |
| P1 | 剖切克隆泄漏 | `:12691` `:12620` | 每次剖切 | N 个实体 | 已修 |
| P1 | `readString` 未释放 | `:1720` | 每次开图 | 小 | 已修 |
| P1 | `convertToRegion` 泄漏 | `:4032` | 最近点查询 | N 个 region | 已修 |
| P1 | `GetDraw3dables` 合并后不删入参 | `:2975` | 每次导出 3D | N 个 solid | 已修 |
| P2 | 空曲线丢弃 | `:11240` | 每次 2D 投影 | 小 | 已修 |
| P2 | `ByJson` / `MakeBlockRefSet` 失败路径 | `:12853` `:12540` | 异常路径 | 小 | 已修 |
“已修”的改动见 2.13。
### 2.13 第一批内存修复的实施记录
改动集中在 `CoreLibrary/AecDbEwEquipment.cpp`,不涉及头文件和存储格式。
| # | 实际改法 | 与原方案的差异 |
| --- | --- | --- |
| 1 | 删掉 `dwgInFields` 里的 `new AecGripData[GRIP_MAXNUM]`;`readString` 后补 `acutDelString(pszTmp)` | 同原方案 |
| 2 | 析构补 `FreeSubPrimData()` | 同原方案 |
| 3 | `FreeSubPrimData()` 成员函数:逐个 `releaseData()` + `DELETE_PTR`,最后 `RemoveAll` | 见 2.3,先统一了 `m_pData` 表示 |
| 4 | `subDMesh2NewSolid()` 按 `idxs` 倒序释放 + `RemoveAt` | 同原方案,另把 `size_t i` 改成 `int i` 倒序 |
| 5 | `RemoveSubPrimObj` / `RemoveSubPrimObjByID` 改用 `releaseData()` + `DELETE_PTR` | 同原方案 |
| 6 | `GetDraw2dables` 两个 `continue` 前补 `DELETE_PTR(pClnEnt)` | 同原方案 |
| 7 | `OldGetSectionDraw` 循环内补 `DELETE_PTR(pSubCln)`、循环后补 `FreeEntityArray(arSubPtrs)`;`GetSectionDraw` 两处 `return OldGetSectionDraw` 前补 `delete pFit` | 同原方案 |
| 8 | `convertToRegion` 的 `regions` 循环结束后逐个 `DELETE_PTR`;顺带把内层遮蔽的 `i` 改为 `j` | 同原方案 |
| 9 | `SortAecComplexCurves` 空曲线改为 `DELETE_PTR` | 同原方案,另补 `pCrv == NULL` 判断 |
| 10 | `ByJson` 循环前 `FreeSubPrimData()`,`byJson` 失败时 `releaseData()` + `DELETE_PTR` | 同原方案 |
| 11 | `MakeBlockRefSet` 的 `getBlockTable` / `pTbl->add` 失败时 `pRec->close()` 并返回 `false` | 同原方案 |
| 12 | `dwgInFields` 的 `m_arSubPrimPtrs.RemoveAll()` 改为 `FreeSubPrimData()` | 新增,原注释写着 FreeBufer 但没真释放 |
| 13 | `GetDraw3dables` 的 `booleanOper(kBoolUnite)` 之后补 `DELETE_PTR(pSolid)` | 新增,见 3.15 |
**函数命名整理**(`AecDbEwEquipment.cpp` 原来有 4 个文件作用域、外部链接的 `FreeBuffer*`,与其他 cpp 里的同名 `static` 函数容易混淆):
| 原名 | 现名 | 说明 |
| --- | --- | --- |
| `FreeBuffer(CPtrArray&)` | `AecDbEwEquipment::FreeSubPrimData()` | 只处理 `m_arSubPrimPtrs`,改成成员函数后参数不再需要 |
| `FreeBuffer(AcDbVoidPtrArray&)` | `static FreeEntityArray()` | 通用实体数组,加 `static` 收回内部链接 |
| `FreeBuffer2(CPtrArray&)` | `static FreeComplexCurveArray()` | `AecComplexCurve` 数组 |
| `FreeBuffer3(CPtrArray&)` | `static FreeFaceplanArray()` | `Aec3dFaceplan` 数组 |
**尚未处理**:
- 2.8 的第二个问题(为了算最近点把临时 solid 写进模型空间再 erase),属于副作用与性能,放在第三批
---
## 3. 潜在缺陷
### 3.1 P0:存盘时把组合设备的子单元清空
```cpp
void dwgOutFieldsForXPrim(AecEqui::XPrimData *pSolid, ...) // :239
{
...
if (CurrentVer >= 5)
{
// 做一下容错
if (pSolid->m_eType != AecEqui::kRevitCombination ||
pSolid->m_eType != AecEqui::kRevitCombination) // ← 恒真
pSolid->m_Combins.clear();
...
}
}
```
`x != A || x != A` 永远为真(第二个条件显然是想写 `kCombination`)。结果:
- **任何带 `m_Combins` 的单元,在存盘前子单元被清空**,写出去的就是 0 个
- 因为是就地 `clear()`,**当前内存里的对象也被破坏**,存完盘再画就没了
- 这段位于 `const` 的 `dwgOutFields` 调用链上,等于在"只读"函数里改数据
影响面取决于图纸里是否还有 `kCombination` 单元(Revit 组合走的是 `kRevitCombination`,数据在 `m_pData` 里,不受影响)。但这段逻辑本身是明确的错误,应先修成 `&&`,并且把"容错"挪到读入或创建时,不要在存盘路径里改数据。
### 3.2 P1:`subErase` 里 `Find` 当布尔用
```cpp
if (eSubType == eValve || sName.Find(_T("阀")) != -1 || sName.Find(_T("接头"))) // :2119
```
`CString::Find` 找不到返回 `-1`,非 0 即真,所以**第三个条件恒真**。整句等价于"永远是阀门",删除任何设备都会走合并管线那段逻辑(`GetInterface` + `PushGripMessage`)。应写成 `!= -1`。
### 3.3 P1:`DrawHandFlg` 用 `==` 赋值
```cpp
if (nVt == 1) sFlag = _T("M");
else if (nVt == 2) sFlag = _T("G");
else sFlag == _T("Y"); // :2656 应为 =
```
液动(3)时文字为空,画出来是空标注。另外这里直接取 `GetInterface(0)` / `GetInterface(1)`,接口不足 2 个时 `inf2` 是默认值,`distanceTo` 为 0、`normalize()` 得到零向量,后续 `angleTo` 结果不可信。
### 3.4 P1:`ProcessHlrResult` 在遍历中删 map 元素
```cpp
for (; it != _preAr.end(); ++it)
{
if (AcDbRegion::cast(it->first))
_preAr.erase(it); // :13133 迭代器失效,下一轮 ++it 是 UB
}
```
应改为 `it = _preAr.erase(it);`。这是典型的偶发崩溃点(容器元素少时不容易复现)。
### 3.5 P1:`GetSectionDraw` 返回值取反
```cpp
int nCount = arRegionPtrs.length();
GetSectionDraw(plan, vtTo, arRegionPtrs);
int nCoundPre = arRegionPtrs.length();
return (nCount - nCoundPre); // :12762 顺序反了
```
返回的是"调用前 − 调用后",恒为 0 或负数。若调用方用它判断"是否剖到东西",判断必然失败。
### 3.6 P1:`HLR_CALC` 标志会被存进图纸
`Draw3d:3009` 一旦读到 `HLR_CALC` 为真就直接返回,什么都不画。这个标志由 `PreprocessForHlr:13204` / `:13360` 置真、`ProcessHlrResult:13107` 复位。
但 `m_mapSpecXData` 的序列化(`AecRxBase/AecDbObjXDataMap.cpp:2034`)是**全量写**,没有任何过滤。所以只要在"置真之后、复位之前"存盘(剖切过程中保存、崩溃、取消),标志就会留在 DWG 里,**下次开图这个设备在 3D 里永久不可见**。
建议:`HLR_CALC` 这类纯运行时标志不要进 `m_mapSpecXData`,或序列化时按 key 白名单/黑名单过滤。
### 3.7 P1:`GetInterface` 两个重载单位不一致
| 重载 | 位置 | 是否乘 `dUnit`(米制 0.001) |
| --- | --- | --- |
| `GetInterface(int, AecInterface&)` | `:3285` | 是(`inf.dSize1 = pNoze->dVal1 * dUnit`) |
| `GetInterface(AcArray<AecInterface>&)` | `:3224` | **否**(直接 `pNoze->dVal1`) |
米制图纸里,同一份数据经两个入口读出来的接口尺寸差 1000 倍。`Draw*Vlv2d` 用下标版,`subErase` / `PreprocessForHlr` 用数组版,两边算出来的东西对不上。
### 3.8 P1:`copyFrom` 漏拷字段
`:2174` 只拷了 `m_mapSpecXData` / `m_nInfActive` / `m_ptInsert` / `m_vtDir` / 几何单元,**没有拷 `m_bVert`、`m_idDash`、`m_idCenter`**。复制粘贴一个竖放阀门,粘贴出来是横放的;两个图层软指针也丢了(虚线和中心线会跑到默认图层)。
### 3.9 P2:`MakeBlockRefSet` 的范围累加逻辑错
```cpp
for (...) {
pEnt = (AcDbEntity*)arEntPtrs.at(i);
if (!AcDbBlockReference::cast(pEnt))
pEnt->getGeomExtents(extAdd);
if (isValidExtents(ext)) // :12522 判断的是累加器,不是本次结果
ext.addExt(extAdd);
}
```
- 第一轮 `ext` 还是默认(空)状态,`isValidExtents(ext)` 为假 → **第一个实体的范围永远加不进去**
- 若 `pEnt` 是块参照,`extAdd` 保持上一轮的旧值,会被重复累加
- 若整个循环没加进任何范围,`ptIns` 由默认范围算出,块原点落在原点而不是设备位置
应改成 `if (isValidExtents(extAdd)) ext.addExt(extAdd);`,块参照分支直接 `continue`。
### 3.10 P2:`byJson` 的 `m_pData` 尺寸对不上
```cpp
p->m_nSize = it->value.GetInt(); // 字节数(来自 __nSize__)
GEO2JSON::binary br(it->value);
p->m_pData = new PVOID[br._len]; // 按 PVOID 个数分配
::memcpy(p->m_pData, br._data, sizeof(PVOID) * br._len); // :820
```
分配长度取决于 `br._len`,使用长度取决于 `m_nSize`,两者来自不同字段。JSON 被改过或版本不一致时就会越界读写。应按 `m_nSize` 分配并校验 `sizeof(PVOID)*br._len >= m_nSize`。
### 3.11 P2:开图读入没有边界校验
`dwgInFieldsForXPrim:172`:
- `readInt32(&nSize)` 后直接 `new byte[pSolid->m_nSize]`(`:193`),负值或超大值都没有检查(负值转 `size_t` 会申请巨大内存)
- `readBytes` 的返回值没有检查
- `m_Combins` 是**递归**读(`:227`),没有深度上限,损坏的图纸可以打爆栈
同类的 `AcDbX3dSolid` 阶段一、二已经补过类似的防护,这里可以照搬。
### 3.12 P2:`PreprocessForHlr` 的"对面"被变换了两次
```cpp
AcGeMatrix3d mat4(mat3.inverse()); // mat3 = 沿截面法向平移 -H/2
for (...) {
AcDbEntity *pNew = (AcDbEntity*)pEnt->clone();
pNew->transformBy(mat4);
pNew->transformBy(mat4); // :13353 又乘一次
_other.append(pNew);
}
```
原实体在 `-H/2` 位置,克隆体被移到 `+H` 位置(两次 `+H/2`),两侧不对称。除非这是刻意做的"厚度补偿",否则应只乘一次。建议拿一个剖切样例对照确认。
### 3.13 P2:`GetBodyBox` 恒返回 `TRUE`
`:4205` 无论有没有取到实体都 `return TRUE`;且 `ar3D` 引用在 `Draw3d` 之前就绑定了(`:4207`)。当前 `Get3DBuffer()` 返回成员引用,重新填充不会换对象,所以能工作;但如果哪天改成返回副本就会静默失效。返回值也应反映实际结果。
### 3.14 P2:`clearGeoBuffer()` 是空实现
`:12861` 函数体只有 `AssertWriteEnabled();`。`PipelineCmd/EquipmentMakeDlg.cpp` 有 8 处调用,本意应该是清掉 2D/3D 缓存(`Clean2DBuffer` / `Clean3DBuffer`)。现在是"调了没反应",改了参数后旧几何还在。
### 3.15 P2:`booleanOper` 的所有权写法在本文件里自相矛盾
同一文件里两种写法并存:
| 位置 | 写法 | 若 SDK 会删除入参 | 若 SDK 不删除入参 |
| --- | --- | --- | --- |
| `:3790` | `booleanOper(...); DELETE_PTR(p);` | 二次释放 | 正确 |
| `:3883` `:3980` | `booleanOper(...)` 后统一 `deleteArrayVoidPtrs` | 二次释放 | 正确 |
| `:14198` | `booleanOper(...); DELETE_PTR(pSolid);` | 二次释放 | 正确 |
| `:2975` | `booleanOper(kBoolUnite, pSolid)`,克隆体不再处理 | 正确 | 每次泄漏一个克隆 |
本文件 5 处调用里 4 处都主动释放入参,`:2975` 是唯一的例外。
**2026-10-23 结论**:按「`booleanOper` 不删除入参」处理,`:2975` 补 `DELETE_PTR(pSolid)`(见 2.13 #13)。依据是同一文件另外 4 处(含失败路径 `:2002` 的 `DELETE_PTR(pSolidInside)`)都按调用方负责释放来写;若 SDK 实际会删除入参,这些既有代码早就在正常流程里二次释放崩掉了。
另外 `CoreLibrary/AcDbX3dSolid.cpp:1998` 的 `booleanOper` 成功路径没有释放 `pSolidInside`,同一约定下也是一个泄漏点,不在本次范围。
### 3.16 其他
- `GRIP_MAXNUM` 在两处定义:`EarThwormDef.h:163` 是 256,`AecWPipe.h:231` 是 24。当前 `AecWPipe.h` 没有被包含,暂时没冲突;一旦谁把它引进包含链,`m_pGripAppData` 的分配与使用边界就会不一致
- `ReadFromFile:4843` 的 `Adesk::Int32 id = ::_time32(NULL);` 是死变量,实际用的是 `++pX->m_nID`,同一秒导入多个单元时 ID 可能重复
- `Draw2d:2498` 的 `if (dc.IsDragging()) { /*return*/ }` 是空壳,拖拽时仍然读旧 2D 缓存,显示滞后一帧
---
## 4. 性能
### 4.1 3D 绘制链路
`Draw3d:3002` 每次调用:
1. `DrawInfArrow` → `GetInterface(arInfs)` → 每个管嘴一次 `GetInfFaceInf`
2. 有 3D 缓存就直接画缓存(快)
3. 没有缓存 → `Draw3dSolid:3905`:逐单元 `Draw3dSolid(XPrimData)` 造体(每个单元一次 `acisIn`/`createXxx`),有差集时再做一轮并集 + 一轮差集
4. `BoolUnit3DBuffer(0.001)`:米制时把缓存里每个 solid 再 `transformBy` 一次
5. 逐个 `setTransparency`
6. `Draw3DBuffers`
成本主要在 3:N 个单元 = N 次造体 + (N−1) 次布尔,与 `AecDbX3dPart` 的结论一致。
### 4.2 P0:拖拽时缓存不清理,且每帧全量重建
```cpp
if (!dc.IsDragging()) // :3036
{
if (Has3DBuffer()) { 画缓存; return; }
}
Draw3dSolid(&dc, nColor); // :3062 拖拽时每帧都走到这里
```
而 `Add3DBuffer`(`AecDbEntityCommon.cpp:146`)只是 `append`,**不清理已有内容**。于是拖拽过程中:
- 每帧重新造 N 个 solid
- 每帧往 3D 缓存里**再追加** N 个
- `Draw3DBuffers` 把缓存里所有实体都画一遍
第 k 帧要造 N 个体、画 k×N 个体。内存随帧数线性涨、帧率随帧数线性掉,拖一次长距离夹点就能看到明显卡顿和内存上冲。
这是当前最值得修的性能问题。可选做法(按改动量排序):
1. 进入拖拽前 `Clean3DBuffer()`,拖拽结束后再清一次 —— 一行改动,先止住缓存膨胀
2. 拖拽时画包围盒/线框(用 `GetInfFaceInf` 那套参数直接算 bbox),不造 ACIS
3. 拖拽时只对缓存里的 solid 施加增量矩阵(`transformBy(delta)`),不重建
### 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 阀门手柄的方向,把所有单元造一遍体再做布尔**。
`Draw2d` 本来就是轻量路径(平面符号),这条依赖把它变成了重路径。设备数量多、缩放频繁时开销明显。
建议:给 `XPrimData` 增加一个按参数解析计算 bbox 的函数(box/cylinder/cone/nozz 这几种占绝大多数,参数都在 `m_pData` 里,`GetInfFaceInf` 已经在做类似的事),2D 路径只算参数 bbox,不碰 ACIS。
### 4.4 P1:`GetInterface` 每次 `acedGetVar` + 临时实体
- `:3237` 和 `:3300` 各调一次 `acedGetVar("INSUNITS")`,而 `GetInterface` 在绘制、夹点、osnap、剖切里共 67 处调用,很多在循环内。`INSUNITS` 是数据库级常量,可以缓存(`CoreTools::IsMetersUnit()` 已有类似封装)
- `GetInfFaceInf` 每个面调两次 `GetTransformbyXform`(`:12160` `:12225`),每次都要 `new`/构造临时 `AcDbLine` / `AcDbPoint` 再走 `TransformEntity`。一个 6 面 box = 12 个临时实体;夹点显示时每个管嘴都要算
- 建议:把"单元 → 世界矩阵"提成一个函数,一次算好,`GetInfFaceInf` / `GetTransformbyXform` 都复用它
### 4.5 P1:`GetDraw2dables` 每次建 `*U` 块 + 跑 HLR
`:2909` 调 `CKTCadUtility::CreateBlockTableRecord(ar, ptIns, NULL, ...)`,`pszBlk == NULL` 时会在当前图纸里**新建一个匿名块表记录**(`KTCadUtility.cpp:537`)。也就是说一次"取 2D 图形"的查询操作会往 DWG 里写数据:块表增长、undo 记录、文件变大。
同时 `OrthoProjected3DSolid` 走的是完整 HLR(`HlrCommands.cpp:214`),每个 solid 一次。
导出场景可以接受,但若被绘制路径复用就不合适。建议:
- 只读查询走"投影曲线直接返回",不建块
- 需要块时由调用方决定,函数只负责给实体
### 4.6 P1:开图路径
`dwgInFields:1791` 无条件 `subDMesh2NewSolid()`,对每个 `kSubDMesh` 单元做一次 `ParseRevitFamily::parseJson`(`:3142`)。Revit 族转来的设备,这一步是开图耗时的大头之一,而且结果只用于显示。
可以照 `AecDbX3dPart` 的做法改成惰性:开图只记标志,首次绘制/取几何时再解析。前提是先修 2.4(现在的删除逻辑会把数组搞错)。
### 4.7 度量建议
| 指标 | 位置 | 目的 |
| --- | --- | --- |
| `Draw3dSolid(XPrimData)` 调用次数 / 累计耗时 | `:3526` | 确认造体占比 |
| `booleanOper` 次数 / 累计耗时 | `:3867` `:3964` | 确认布尔占比 |
| 3D 缓存长度峰值 | `Draw3d:3036` | 验证 4.2 的膨胀 |
| `GetBodyBox` 触发的 `Draw3d` 次数 | `:4205` | 验证 4.3 |
| `GetDraw2dables` 建块次数 | `:2909` | 验证 4.5 |
| 开图总时长 / `subDMesh2NewSolid` 时长 | `:1791` | 验证 4.6 |
场景:单设备拖拽夹点、全图缩放、图层冻结后重生成、剖切、2D 导出。
---
## 5. 优化方案(分批)
### 5.1 第一批:内存与正确性(不改接口、不改格式)
拆成两批提交:**内存问题已做(见 2.13)**,逻辑缺陷待做。
| # | 改动 | 位置 | 风险 | 状态 |
| --- | --- | --- | --- | --- |
| 1 | 删掉 `dwgInFields` 里的重复分配,`readString` 补 `acutDelString` | `:1707` `:1720` | 低 | 已完成 |
| 2 | 析构补 `FreeSubPrimData()` | `:1686` | 低(依赖 #3) | 已完成 |
| 3 | `releaseData()` 统一 `delete[]`,`FreeSubPrimData()` 释放 `m_pData` 后删结构体 | `:1520` | 低 | 已完成(见 2.3、2.13) |
| 4 | `subDMesh2NewSolid` 按下标倒序删除并释放 | `:3159` | 中(涉及几何正确性,需样例图) | 已完成 |
| 5 | `RemoveSubPrimObj*` 补释放 `m_pData` | `:4276` `:4291` | 低 | 已完成 |
| 6 | `subErase` 的 `Find` 补 `!= -1` | `:2119` | 低(行为会变,需回归删除阀门) | 待做 |
| 7 | `DrawHandFlg` 的 `==` 改 `=` | `:2656` | 低 | 待做 |
| 8 | `ProcessHlrResult` 改用 `it = erase(it)` | `:13128` | 低 | 待做 |
| 9 | `copyFrom` 补 `m_bVert` / `m_idDash` / `m_idCenter` | `:2174` | 低 | 待做 |
| 10 | `GetSectionDraw` 返回值改 `nCoundPre - nCount` | `:12762` | 低(需确认调用方语义) | 待做 |
| 11 | `clearGeoBuffer` 实现为清 2D/3D 缓存 | `:12861` | 低 | 待做 |
另外补做的 4 处内存修复(2.6、2.7、2.9、2.10、2.11、`GetDraw3dables` 合并泄漏、`dwgInFields` 的 `RemoveAll`)见 2.13。
### 5.2 第二批:`m_Combins` 存盘清空(需要确认影响面)
| 步骤 | 内容 |
| --- | --- |
| 1 | 统计现有图纸里 `kCombination` 单元的实际使用(与阶段三那次一样,先看数据) |
| 2 | 修 `:279` 的恒真条件为 `!= kRevitCombination && != kCombination` |
| 3 | 把"容错清理"从 `dwgOutFields` 挪到 `dwgInFields` 读完之后 |
| 4 | 用含组合设备的样例做"存盘→重开→几何完整"验证 |
若确认图纸里已无 `kCombination`,可以只做 2、3,不做数据迁移。
### 5.3 第三批:性能(先量后改)
| 优先级 | 改动 | 预期 |
| --- | --- | --- |
| 高 | 拖拽时清缓存 + 简化显示(4.2) | 拖拽帧率、内存峰值 |
| 高 | 2D 路径改用参数 bbox,不触发 3D(4.3) | 2D 重生成、缩放 |
| 中 | `INSUNITS` 缓存 + 单元矩阵复用(4.4) | 夹点、osnap |
| 中 | `GetDraw2dables` 不建块(4.5) | 导出、剖切、DWG 体积 |
| 低 | `subDMesh2NewSolid` 惰性化(4.6) | 开图 |
### 5.4 需要先确认的事
1. ~~`booleanOper` 是否删除入参(3.15)~~ —— 2026-10-23 按「不删除入参」处理,理由见 3.15
2. `GetSectionDraw` 的返回值调用方到底期望什么(3.5)
3. `PreprocessForHlr` 的 `mat4` 双次变换是否刻意(3.12)
4. `kCombination` 单元的实际存量(5.2)
---
## 6. 风险与验证清单
| 改动 | 验证方式 |
| --- | --- |
| 夹点数组 / 析构释放 | 打开含 1000+ 设备的图纸,任务管理器看私有字节;关图后应回落 |
| `releaseData` 统一 `delete[]` | 各类单元(box/cylinder/cone/Revit/组合)各删一个,确认无崩溃;开图→改参数→存盘→重开,几何完整 |
| 剖切 / 2D 导出 | 对含阀门、法兰、普通设备的图各剖一次,反复开关剖切对话框,内存不持续上涨 |
| `subDMesh2NewSolid` | 含 SubDMesh 的样例图,开图后单元数量与类型正确,绘制不缺体 |
| `subErase` | 删除阀门/接头/普通设备各一次,确认只有阀门走合并管线 |
| `HLR_CALC` | 剖切过程中保存再重开,设备 3D 仍可见 |
| `copyFrom` | 复制粘贴竖放阀门,方向与图层保持 |
| 拖拽优化 | 拖夹点 10 秒,内存不涨、帧率稳定;松手后几何位置正确 |
| 2D bbox | 各阀门 2D 符号方向、手柄位置与改前一致 |
本仓库没有单元测试,验证只能靠"编译 + CAD 内实测"。编译范围按既有约定:Debug + R190,只编 `AecRxBase`、`CoreLibrary_BAL_REMOTE`、`CoreCommand_BAL_REMOTE`、`CoreImplement`。
---
## 7. 明确不做
- 不改 `XPrimData` 的存储布局和 `kCurrentVersionNumber`(会带来新老插件不兼容,与 `AcDbX3dSolid` 阶段三取消的理由一致)
- 不把 2D 符号系统(25 个 `Draw*2d`)重构成数据驱动,收益不明确、回归面太大
- 不动 `AecDbEwFitting` / `AecDbHvacFitting` 等同类实体,除非确认它们的同一批写法也要一起改
---
## 8. 实施顺序
1. ~~第一批 1~5(内存),单独一个 PR,可独立验证~~ —— 已完成,范围扩到 2.1~2.11 全部内存项,见 2.13
2. 第一批 6~11(逻辑),单独一个 PR
3. 确认 5.4 剩下的三个问题
4. 第二批(`m_Combins`),视存量决定是否做
5. 第三批按 4.7 度量结果排序,一项一个 PR