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

32 KiB
Raw Blame History

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

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:析构函数不释放几何单元

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&) 只释放数据块,不释放结构体

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() 删错元素,且泄漏

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,并写库

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:存盘时把组合设备的子单元清空

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 当布尔用

if (eSubType == eValve || sName.Find(_T("阀")) != -1 || sName.Find(_T("接头")))   // :2119

CString::Find 找不到返回 -1,非 0 即真,所以第三个条件恒真。整句等价于"永远是阀门",删除任何设备都会走合并管线那段逻辑(GetInterface + PushGripMessage)。应写成 != -1。

3.3 P1:DrawHandFlg 用 == 赋值

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 元素

for (; it != _preAr.end(); ++it)
{
    if (AcDbRegion::cast(it->first))
        _preAr.erase(it);        // :13133 迭代器失效,下一轮 ++it 是 UB
}

应改为 it = _preAr.erase(it);。这是典型的偶发崩溃点(容器元素少时不容易复现)。

3.5 P1:GetSectionDraw 返回值取反

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 的范围累加逻辑错

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 尺寸对不上

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 的"对面"被变换了两次

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:拖拽时缓存不清理,且每帧全量重建

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