- 7.1 增加 E3 条目(PR #69); - 7.4 由“未处理”改为“确认不改”,逐条写明原因; - E2 重点说明:只在超长路径回退分支触发、需重构两个 DrawPipe 重载、 真正的瓶颈是后续 N-1 次 booleanOper 合并,收益不划算。
14 KiB
GiDrawUtility 分析:内存泄漏、效率与逻辑缺陷
目标:在动手改之前,把
GiDrawUtility(AecRxBase 公共几何工具库)的内存泄漏、性能热点和逻辑缺陷列清楚,给出可分批落地的修复顺序。范围:
SourceCode/Code2026/AecRxBase/GiDrawUtility.cpp(965 行)、SourceCode/Code2026/inc/GiDrawUtility.h。 关联:EarThwormDef.h(EQUAL_ZERO 等宏)、inc/memOper.h(DELETE_PTR 模板函数,经KTCad.h的using namespace MemOper;引入)。状态(2026-10-25):L1-L7、B1-B5、B7、B9、S1、E3 已修复并合并(PR #62 / #64 / #65 / #67 / #69);B6、B8、B10、B11、S2 与效率项 E2、E4、E5 确认不改;S3 复核为误判。详见第 7 节。文中行号为分析时的 main,修复后已偏移。
1. 背景与影响面
GiDrawUtility 是全库共用的 3D 几何构建工具(DrawPipe / DrawLoft / DrawRevolve / DrawCone / DrawBox 等),以 AECDLLIMPEXP 导出,直接被 14+ 个模块调用:
DrawPipe(ptFrom, ptTo, dRadius, dThick, nSegs):AecDbEwPipe、AecDbHvacDuct、AecDbECableTray、AecDbSludgePipe、EarthWormCmd/DGGLDlg.cpp、stakeOper.cpp,以及所有DrawCylinder(内部转调此函数);DrawPipe(pCurve, pRegion, ptCenInRgn):AecDbEwPipeBase(约 20 处)、AecDbEwFitting(约 40 处)、AecDbArcStair、AecDbLineStair、AecDbClimbStair、AecDbGuardBar、AecDbFlume等;DrawPipe(pCurve, dR):AecDb3dRoadAxisLine、AecDbEwFitting。
因此本文件的问题会随"每画一个管件/实体"重复触发,泄漏与效率问题在连续布管、explode 分解场景下会持续累积。
约定说明:DELETE_PTR(p) 宏定义为 if (p) { delete p; p = NULL; }(tsuperlist.h),delete 后置 NULL,故"DELETE_PTR 后 return 该指针"不会产生野指针;未 DELETE_PTR 的失败路径才是泄漏。booleanOper 成功后第二个操作数不被消耗、需调用方手动释放——本仓库 AecDbEwPipeBase.cpp、AecDbEwFitting.cpp 均循此例。
2. 内存泄漏
L1 严重·高频:DrawPipe 重复建 region 且丢弃第一次结果(GiDrawUtility.cpp:677-678)
if (!createRegion(&Cir1)) return NULL; // 返回值(new AcDbRegion)被直接丢弃 → 泄漏
AcDbRegion *pBig = createRegion(&Cir1); // 同样的几何又建了一遍
createRegion 返回 createFromCurves 产出的堆对象,第一次调用每次必漏一个 AcDbRegion。所有画管道/圆柱的路径(含 DrawCylinder)都踩中,画一根管子漏一个 region。
修法:删掉第一行,改为
AcDbRegion *pBig = createRegion(&Cir1);
if (!pBig) return NULL;
L2 严重·同函数:pSml 两处泄漏(GiDrawUtility.cpp:684-700)
dThick > dRadius时内圆负半径,createRegion(&Cir2)失败后return NULL,pBig未释放;booleanOper成功路径pSml从未释放(失败路径反而删了)。按仓库惯例成功后应DELETE_PTR(pSml)。
L3 DrawLoft(Curve*,Curve*,Curve*) 失败路径漏三个 clone(GiDrawUtility.cpp:238)
if (!pRegT || !pRegB) return NULL; // _pTop/_pBottom/_pLine 及已成功的 pRegT 全漏
DrawPyramid / DrawTydf / DrawCirTorus / DrawRectTorus 截面非 region 时走这条。
L4 DrawBox / DrawSphere 失败路径漏 pSolid(GiDrawUtility.cpp:432、449)
createBox / createSphere 返回非 eOk(负尺寸等)时 return NULL 前未 delete。
L5 DrawMesh 失败路径双重泄漏(GiDrawUtility.cpp:355-377)
createLoftedSolid 失败时 DELETE_PTR(pEnt) 后直接 return——new AcGePoint3d[columns](pSection)和 vecPLs 中已生成的所有 polyline 全部泄漏。成功路径反而清理完整,失败路径什么都没清。
L6 DrawPipe(pCurve, dR) 两平台分支不对称(GiDrawUtility.cpp:820-829)
closed polyline 走 createSweptSolid:ZWCAD 分支(866 行起)失败时有 DELETE_PTR(pReg),ARX 分支没有 → ARX 上扫掠失败即漏 pReg。
L7 模式性问题:createFromCurves 只取 [0](GiDrawUtility.cpp:130-135、645-648、817-819)
createRegion(AcDbEntity*)、DrawDish、DrawPipe(pCurve, dR) 均只取结果数组第一个 region,多 region 返回(自交多边形等)时其余全漏。圆/简单闭合环一般返回 1 个,概率低但存在。
3. 运行效率
| # | 位置 | 问题 |
|---|---|---|
| E1 | 677-678 | 即 L1:每次画管道多跑一次 createFromCurves(ACIS 级开销) |
| E2 | 830-860 | fallback 分段递归时每段重新建同一个圆 region(AcDbCircle + createFromCurves + sweep)。长路径(>100000)分十几段就重复十几遍。应段外建一次模板 region,段内 clone() + transformBy() |
| E3 | 145-160 | transfromRegion 对 region 做两次 transformBy(先旋转后平移)。region 是 ACIS 拓扑,每次变换触发几何重算;两条矩阵可合成一次。每条管道热路径 |
| E4 | 466-467、553-555 | DrawPyramid/DrawTydf 中 mat.setToTranslation(ptBottom - kOrigin) 在 ptBottom=原点 时是恒等变换,白做一次 transformBy;顶点可直接用 3D 点 addVertexAt 完全免掉 |
| E5 | 455-460 | DrawSphere 用 RotateAndMove(kZAxis, origin, kZAxis, ptCenter)——同轴 rotateTo 返回单位阵,纯平移即可 |
4. 执行逻辑
B1 逻辑错误:RotateAndMove 平移项不完整(GiDrawUtility.cpp:186)
mat = _vtOrig.rotateTo(_vtDist); // p' = R·p
mat.setTranslation(ptDist - ptOrig); // p' = R·p + (ptDist - ptOrig) ← 错
把 ptOrig 搬到 ptDist 的正确平移项是 ptDist - R·ptOrig。当前实现仅在 ptOrig 为原点时正确:
- 现有内部调用恰好都传
kOrigin,暂未暴露; - DrawTydf 的 bIsTfdy=TRUE 分支(508-518 行先把 ptBottom 平移到 (0,0,H),562 行再
RotateAndMove(kZAxis, ptBottom, vtMove, ptPos))ptOrig 非原点——vtMove 不是 Z 轴时台体落点偏移 R·ptOrig - ptOrig,是当前代码中唯一实际踩中的路径; - 该函数是 AECDLLIMPEXP 公开 API,未来调用方传非原点必错。
修法一行:
AcGePoint3d ptTmp(ptOrig);
ptTmp.transformBy(mat);
mat.setTranslation(ptDist - ptTmp);
B2 零向量防护是谎言(GiDrawUtility.cpp:180-183)
_vtOrig *= 1; // 保证长度不为0 是 no-op。输入零向量时 rotateTo 行为未定义,应显式判零并回退默认轴。
B3 DrawTydf 的 tfdy 分支 guide 线未随截面变换(GiDrawUtility.cpp:508-521)
line 在变换之前构造;tfdy 分支把 plBottom 上移 H、cir 下移 H,line 原地不动,不再连接两截面中心。loft 的 guide rail 直接影响曲面形状;非 tfdy 分支的 line 是连接两截面中心的,行为不一致,疑似 bug(需确认是否有意为之)。
B4 DrawMesh 无入参校验(GiDrawUtility.cpp:340-352)
pVertexList为 NULL 直接解引用;rows * columns == 0时pVertexList[rows * columns - 1]越界读 [-1];rows == 1时截面数组为空,createLoftedSolid失败又回到 L5 泄漏路径;first = 1, last = rows - 1中 rows 为 UInt32,rows=0 时 last 下溢为 -1(循环虽不执行,但上面 line 构造已先越界)。
B5 DrawRevolve 失败时往图纸里塞红线(GiDrawUtility.cpp:283-286)
if (!pReg) { _pC->setColorIndex(1); AddToModelSpace(_pC); return NULL; }
工具函数产生"往模型空间加实体"的副作用,调用方无法预期也无法关闭,疑似调试代码残留。且用的是匿名 namespace 里不判 pEnt 空的那个重载。
其余逻辑问题
| # | 位置 | 问题 |
|---|---|---|
| B6 | 673 | nSegs 死参数:签名带分段数,函数体完全没用,调用方以为能控制精度 |
| B7 | 794 | getStartPoint(ptS) 返回值未检查,未定义点传入 getFirstDeriv |
| B8 | 138 | create3DPolyline 无条件 makeClosed():开口折线被强制闭合,回转出闭合实体;len<=0 不校验 |
| B9 | 636 等 | 全局无参数校验:DrawDish 的 2*dH/dD 除零;DrawCirTorus/DrawRectTorus 在 minorRadius >= majorRadius 时构造负半径几何直接喂 ACIS;ptFrom==ptTo 零长路径扫掠 |
| B10 | 405-412 | DrawExclude(pCurve, vtPath) 用包围盒中心作拉伸起点,凹形截面时起点在区域外,需确认是否符合预期 |
| B11 | 10-84 | 匿名 namespace 里三个 AddToModelSpace 重载语义分裂:4 参版返回 ErrorStatus、2 参 bool 版返回 AcDbObjectId、3 参默认值版返回 ErrorStatus 且不判 pEnt 空,靠重载决议分流,维护陷阱 |
5. 规范问题
- S1
spliteCurve(722 行)、deleteContainerOfArx(770 行)是文件级static,违反 CODEBUDDY.md"文件内私有函数放匿名 namespace"约定——本文件已有匿名 namespace,应移入。 - S2
transfromRegion函数名拼写错误(transfrom → transform),公开 API 不宜直接改名,建议加正确拼写的 wrapper。 - S3
DELETE_PTR宏无do-while(0)包裹(if (ptr) { delete ptr; ptr = NULL; }),本文件现有用法均为独立语句暂无 bug,但与 if/else 搭配时是陷阱。
6. 建议修复顺序
- L1 + L2:改动约 3 行,消掉最高频的泄漏(每画一个管件一次),收益最大;
- B1:一行修复整类错位风险,需回归 DrawTydf 各分支及所有落点;
- L3 / L4 / L5 / L6:失败路径按触发概率补清理;
- B4 / B9:入参校验,防崩溃类;
- 其余(B2/B3/B5~B8/B10/B11、E 系列、S 系列)按需。
注意事项:GiDrawUtility 是 AecRxBase 公共库、DrawPipe 系列是全库管道实体绘制的公共入口,改动后建议小步提交,编译对应 preset 并在 CAD 中实测(画管、台体、回转体、loft 分解)。
7. 处理结果(2026-10-25)
7.1 已修复
| # | 内容 | 落地 |
|---|---|---|
| L1 / L2 | DrawPipe(ptFrom, ptTo) 重复建 region、pSml 成功后不释放 |
PR #62 |
| L3 ~ L6 | 各失败路径补释放:DrawLoft(curve,curve,curve)、DrawBox、DrawSphere、DrawMesh、DrawPipe(pCurve, dR) 的 ARX 分支 |
PR #62 |
| L7 | 新增 TakeFirstRegion(),createRegion(AcDbEntity*)、DrawDish、DrawPipe(pCurve, dR) 统一只取首个 region 并释放其余;DrawDish 顺带补上 DrawRevolve 失败时的释放 |
PR #65 |
| B1 | RotateAndMove 平移项改为 ptDist - R*ptOrig |
PR #64 |
| B2 | 零向量显式退化为 Z 轴,去掉 *= 1 空操作 |
PR #65 |
| B3 | DrawTydf 的 tfdy 分支引导线跟随变换后的截面中心 |
PR #65 |
| B4 | DrawMesh 入参校验(pVertexList、rows >= 2、columns >= 3) |
PR #65 |
| B5 | DrawRevolve 建 region 失败不再往模型空间塞红色曲线 |
PR #65 |
| B7 | DrawPipe(pCurve, dR) 检查 getStartPoint 返回值 |
PR #67 |
| B9 | DrawDish 除零、DrawCirTorus/DrawRectTorus 负半径、零长管道、非正半径 |
PR #67 |
| S1 | spliteCurve、deleteContainerOfArx 由文件级 static 移入匿名 namespace |
PR #67 |
| E3 | transfromRegion 把旋转与平移合成一个矩阵,region 只做一次 transformBy |
PR #69 |
E1 随 L1 一并消除(不再对同一几何重复调用 createFromCurves)。
7.2 确认不改
B6(nSegs 死参数,删除需改公开签名)、B8(create3DPolyline 强制闭合)、B10(DrawExclude 用包围盒中心作拉伸起点)、B11(三个 AddToModelSpace 重载为调试用函数)、S2(transfromRegion 拼写不改名,避免动公开 API)。
7.3 S3 结论:误判,不改宏
GiDrawUtility.cpp 里的 DELETE_PTR 不来自 tsuperlist.h 的宏,而是 inc/KTCad.h:565 的 using namespace MemOper; 引入的模板函数:
// inc/memOper.h
template <class T>
void DELETE_PTR(T *&ptr) { if (ptr != NULL) { delete ptr; ptr = NULL; } }
模板函数不存在悬垂 else 问题,本文件不受影响。inc/AdsXRx/tsuperlist.h、tg_shs/inc/geo/tsuperlist.h 的 DELETE_PTR 宏与 Dimension/superlist.h 的 _DELETE_PTR_ 宏仍是 if (ptr) { ... } 形式,但与本文件无关,保持原样。
7.4 效率项 E2 / E4 / E5:确认不改
| # | 位置 | 不改的原因 |
|---|---|---|
| E2 | DrawPipe(pCurve, dR) 与 DrawPipe(pCurve, pRegion, ptCenInRgn) 的分段回退分支 |
见下方说明 |
| E4 | DrawPyramid / DrawTydf 的 mat.setToTranslation(ptBottom - kOrigin) |
只有 ptBottom 恰在原点时才是恒等变换;要真正省掉得把四边形顶点改成直接 addVertexAt 传 3D 点,动的是台体截面构造,收益是省一次 4 顶点的 transformBy,不值得 |
| E5 | DrawSphere 的 RotateAndMove(kZAxis, kOrigin, kZAxis, ptCenter) |
同轴 rotateTo 返回单位阵,剩余的是纯平移;但这里只多算一个矩阵乘法,不是 ACIS 变换,收益可忽略 |
E2 为什么不做:
- 触发面很窄。只有
extrudeAlongPath失败时才进这个分支——即路径长超过 120000 或体量超过 12000(CAD 对拉伸/扫掠体的限制)。普通管段(几米)永远走不到,spliteCurve也就不会把路径切段。 - 改造成本不小。要复用截面,必须先把“用给定截面沿曲线扫掠”抽成一个不带重试逻辑的内部函数,然后把两个
DrawPipe重载里的两份近乎相同的回退代码一起改过去,属于结构性重构,不是局部优化。 - 收益比看上去小。省掉的是 N−1 次
createFromCurves;但每段仍要做clone+transformBy(同样是 ACIS 操作)+extrudeAlongPath,而该分支真正的大头是后面 N−1 次booleanOper(kBoolUnite)把各段合并成一个实体——E2 完全碰不到这一块。即使把 region 建一次,整体耗时也不会明显下降。
结论:收益/风险不划算。等真的遇到长路径布管卡顿,先用实测确认瓶颈落在 createFromCurves 还是 booleanOper,再决定是否重构;按目前的判断,大概率是后者。