Files
envi-code/GiDrawUtility-analysis.md
T
gjm 8bf914e6b6 更新分析文档:E3 已修复,补充 E2/E4/E5 不改的原因
- 7.1 增加 E3 条目(PR #69);
- 7.4 由“未处理”改为“确认不改”,逐条写明原因;
- E2 重点说明:只在超长路径回退分支触发、需重构两个 DrawPipe 重载、
  真正的瓶颈是后续 N-1 次 booleanOper 合并,收益不划算。
2026-10-25 19:20:00 +08:00

205 lines
14 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.
# 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)
```cpp
if (!createRegion(&Cir1)) return NULL; // 返回值(new AcDbRegion)被直接丢弃 → 泄漏
AcDbRegion *pBig = createRegion(&Cir1); // 同样的几何又建了一遍
```
`createRegion` 返回 `createFromCurves` 产出的堆对象,第一次调用**每次必漏一个 AcDbRegion**。所有画管道/圆柱的路径(含 DrawCylinder)都踩中,画一根管子漏一个 region。
**修法**:删掉第一行,改为
```cpp
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)
```cpp
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)
```cpp
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,未来调用方传非原点必错。
**修法**一行:
```cpp
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)
```cpp
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. 建议修复顺序
1. **L1 + L2**:改动约 3 行,消掉最高频的泄漏(每画一个管件一次),收益最大;
2. **B1**:一行修复整类错位风险,需回归 DrawTydf 各分支及所有落点;
3. **L3 / L4 / L5 / L6**:失败路径按触发概率补清理;
4. **B4 / B9**:入参校验,防崩溃类;
5. 其余(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;` 引入的模板函数:
```cpp
// 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 为什么不做**:
1. **触发面很窄**。只有 `extrudeAlongPath` 失败时才进这个分支——即路径长超过 120000 或体量超过 12000(CAD 对拉伸/扫掠体的限制)。普通管段(几米)永远走不到,`spliteCurve` 也就不会把路径切段。
2. **改造成本不小**。要复用截面,必须先把“用给定截面沿曲线扫掠”抽成一个不带重试逻辑的内部函数,然后把两个 `DrawPipe` 重载里的两份近乎相同的回退代码一起改过去,属于结构性重构,不是局部优化。
3. **收益比看上去小**。省掉的是 N−1 次 `createFromCurves`;但每段仍要做 `clone` + `transformBy`(同样是 ACIS 操作)+ `extrudeAlongPath`,而该分支真正的大头是后面 N−1 次 `booleanOper(kBoolUnite)` 把各段合并成一个实体——E2 完全碰不到这一块。即使把 region 建一次,整体耗时也不会明显下降。
结论:收益/风险不划算。等真的遇到长路径布管卡顿,先用实测确认瓶颈落在 `createFromCurves` 还是 `booleanOper`,再决定是否重构;按目前的判断,大概率是后者。