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
615 lines
32 KiB
Markdown
615 lines
32 KiB
Markdown
# 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
|