将 AecDbSlab-analysis.md 与 AecDbSlab-performance.md 合并为 AecDbSlab-review.md:第 1~6 章为内存泄漏/潜在 Bug/运行逻辑,第 7~12 章为运行效率(调用链、E1~E11、改造顺序、验证方法),并补充 GetRotation(Z) 恒返回 0 等跨章发现。
40 KiB
AecDbXSlab(AecDbSlab)审查:内存泄漏 / 潜在 Bug / 运行逻辑 / 运行效率
范围:
SourceCode/Code2026/CoreLibrary/AecDbSlab.cpp(5115 行)、SourceCode/Code2026/inc/AecDbSlab.h(172 行)。 注意:文件名是 AecDbSlab,类名是 AecDbXSlab(ACRX_DXF_DEFINE_MEMBERS(AecDbXSlab, AecDbBDPipeBase, ...)),全局搜索时两种名字都要试。 继承链:AecDbXSlab→AecDbBDPipeBase→AecDbEwPipeBase→AecDbCurveBase→AecDbEntityBase。 分析基线:main @ d1bdf28。下文行号均为分析时的 AecDbSlab.cpp。 合并说明:本文由原AecDbSlab-analysis.md(第 16 章)与12 章)合并而成。AecDbSlab-performance.md(第 7
目录: 1 数据模型 · 2 内存泄漏 · 3 潜在 Bug · 4 运行逻辑问题 · 5 已核对确认不是问题 · 6 缺陷修复顺序 7 效率结论速览 · 8 一帧调用链 · 9 逐项分析 E1~E11 · 10 效率改造顺序 · 11 顺带发现 · 12 验证方法
1. 数据模型(读代码前先建立的三个前提)
一个板实体同时持有四份"几何真相",任何一处不同步就会表现为画错/夹点错/属性错:
| 成员 | 作用 | 关键约定 |
|---|---|---|
m_pBaseCurve(基类,mutable AcDbCurve*) |
拉伸路径 / 类墙体板的定位线 | 必须是真实位置;长度即板厚(类墙体板则是截面高度) |
m_pRegion(基类,AcDbRegion*) |
截面 | 一般在原点(SetSection(pReg, ptCen) 只记中心,不做平移) |
m_mapSpecXData / GetSpecialtyData() |
参数(KEY_SHAPE/KEY_ORGPT/KEY_BOMTHICK/KEY_POINTS…) | GetSpecialtyData() 返回的就是 &m_mapSpecXData,两者等价 |
m_arX3DPartPtrs(AecDbBDPipeBase) |
附属 3D 体(水池堰墙) | 裸指针数组,析构时 delete;AddX3dPart 接管所有权 |
ESection 决定后续所有分支:eRect / eCircle / eEllispe / eUserDef / eXShape(截面拉伸)。
板还有两种"身份"由 CEntityProperty::getSlabType() 区分:eDB(水池底板)、eLB(楼板)、eGB(盖板)、eXDB(斜底板),属性面板与夹点逻辑都按它分叉。
调用面
- 创建:
SetRectSlab/SetCircleSlab/SetSectorSlab/SetRegionSlab/SetRectBasin/SetCircleBasin/SetEllipseBasin/SetFenceBasin/SetExtrudeBasin; - 交互:
subGetGripPoints/subMoveGripPointsAt/subGetStretchPoints/subMoveStretchPointsAt; - 显示:
Draw2d/Draw3d/DrawHatch/DrawXShape2d/PreprocessForHlr; - 交换:
dwgIn/OutFields、subDeepClone/subWblockClone、ToJson/ByJson、GetProperties/SetProperties。
2. 内存泄漏
L1 严重·高频:GetOffsetCurve 失败路径整批泄漏(:147-206)
第一个重载在 arCurOff1.length()==1 && arCurOff2.length()==1 不成立时直接 return NULL;,两个 AcDbVoidPtrArray 里的 offset 曲线(getOffsetCurves 产出、调用方拥有)全部泄漏。第二个重载(:209-257)在同样失败时调了 FreeBuffer,但成功分支 return pCurve1/pCurve2 时另一个数组元素已 delete,逻辑正确——两个重载行为不一致,说明第一个是漏写的。
修法:第一个重载失败分支补 FreeBuffer(0,arCurOff1); FreeBuffer(0,arCurOff2);。
L2 严重·每次拆墙:CreatePipeRegion 成功路径漏内环 region(:259-299)
AcDbRegion* pRegIns = (AcDbRegion*)arRegPtrs.first();
if (Acad::eOk == pRegOut->booleanOper(AcDb::kBoolSubtract,pRegIns))
return pRegOut; // pRegIns 从未释放
return NULL; // 失败时 pRegOut 也漏
booleanOper 不消耗第二个操作数(本仓库 AecDbEwPipeBase.cpp、AecDbEwFitting.cpp 都遵循"成功后手动 delete 第二个操作数"的惯例)。椭圆水池每次 SetEllipseBasin 建墙都漏一个 region。
L3 严重·每次命中判断:IsPointInSide 漏 polyline(:1760-1785)
polyline 分支算完 bIn 后直接 return bIn>0,pShapeCurve(GetBasinShapeCurve 新建的 polyline)没有 delete,代码里还留着 //delete pPl; 的注释。圆/椭圆分支各自 delete 了,只有 polyline 分支漏——异形板的点选/包含判断每次漏一条曲线。
L4 严重·每次重绘:Draw2d 圆板漏 circle(:2354-2390)
AcDbCircle* pCir = AcDbCircle::cast(pShape); // :2362
if (pCir != NULL) { ... dc.DrawEntity(pPl); Add2DBuffer(pPl); } // pShape 既不进 buffer 也不 delete
Add2DBuffer 接管所有权(Clean2DBuffer 会 delete),但这条分支只把新建的 pPl 加进 buffer,原始 pShape(circle)被丢弃。带出图宽度(KEY_PLOTW>1e-3)的圆板每次重绘漏一个 AcDbCircle。:2293 那个"无 KEY_ORGPT"分支同理:非 polyline 的 pShape 不进 buffer 也没释放。
L5 中·每次重设异形板:SetRegionSlab(LPCTSTR) explode 碎片全漏(:922-946)
pPl->explode(arCurPtrs); // 产出 N 个 line/arc
AcDbRegion::createFromCurves(arCurPtrs,arRegPtrs);
...
delete pPl; // 只删了 polyline,碎片既不 close 也不 delete
每次重设异形板漏 N 个曲线实体(N = 顶点数)。
L6 中:SetEllipseBasin 两处泄漏(:1273、:1330)
pEllipse->getOffsetCurves(dWallWidth,arCurPtrs)产出的 offset 曲线只用于建 region,之后从不释放;arRegPtrs多于 1 个时只取first(),其余泄漏;- 叠加 L1 的
GetOffsetCurve泄漏(pOutSide只删了成功返回的那个)。
L7 中:PeelBasinWall 拆墙后碎片泄漏(:1405-1550)
pBaseCrv 为 polyline 时 explode(arPtrs) 得到碎片,逐个 pWall->SetBaseCurve(pBaseCrv)(内部 clone)后 碎片全部不释放;多个墙体 part 时 pBaseCrv 被覆盖,前一个曲线也漏。拆一次墙漏 N 个实体。
L8 轻:SetProperties eXDB 分支漏 pXYPoly(:5095-5101)
AcDbPolyline *pXYPoly = CoreTools::StringToPolyline(sProfile);
if (pXYPoly) { ... SetSection(pXYPoly, ptCen); } // SetSection(AcDbPolyline*) 不接管所有权
对比 SetSection(AcDbRegion*) 内部会 clone()——polyline 版本只 createFromCurves,指针用完就该删。
L9 轻:DrawHatch 早退漏 clone 组(:4640-4660)
pEnt->isA()->create() + CopyObject 造的一组 clone 交给 ConvertBoundCurve,正常路径由 FreeBuffer(0,arRelt) 释放;但 if (!pCurve) return; 在 FreeBuffer 之前,此时 arEntClnPtrs 里的 clone 全部泄漏。
3. 潜在 Bug(崩溃 / 死循环 / 未定义行为)
B1 严重:FreeBuffer 遇到 NULL 元素死循环(:56-70)
while(arEntPtrs.length() > n)
{
pEnt = (AcDbEntity*)arEntPtrs.last();
if (pEnt != NULL) { pEnt->erase(); pEnt->close(); arEntPtrs.removeLast(); delete pEnt; }
}
removeLast() 在 if 内部——只要数组里有一个 NULL,length() 永远不减,CAD 直接卡死。同一文件里 DrawHatch、Disperse、GetOffsetCurve、DrawHatch 的 arRelt 都走它。
另外 erase()/close() 对 非数据库常驻实体(explode/offset 产出的)没有意义,本仓库 AecDbBDPipeBase.cpp:48 的同名 FreeBuffer 就只 DELETE_PTR——同名不同义,容易误改。
修法:removeLast() 提到 if 外;删掉 erase()/close(),与基类版本保持一致。
B2 严重:ClosestPointToCen 无判空解引用 m_pRegion(:1868、:1906)
两个分支都是 m_pRegion->clone(),没有 if (!m_pRegion)。而 dwgInFields(:2499)在 m_pRegion == NULL 时的重建逻辑里写了 if (shape == eCircle) break;——圆板 region 丢失时直接放弃重建,之后任何一次 ClosestPointToCen 都会崩。这个兜底注释写的是"除了圆板,其他都重建",恰恰把最容易丢的圆板排除了。
修法:m_pRegion 为空时按 KEY_SHAPE 现场重建(圆板用 KEY_RAD 造 AcDbCircle → region),或至少让 ClosestPointToCen 返回 false。
B3 严重:GetBasinShapeCurve eUserDef 分支空指针(:2118-2124)
AcDbPolyline* pPl = PointsScrToPolyline(sPoint2dsTop);
if (pPl != NULL) { pShapeCrv = pPl; }
pPl->setClosed(Adesk::kTrue); // ← 判空之后仍然无条件解引用
pPl->setConstantWidth(dPlotW);
KEY_POINTS 为空/格式损坏(旧图、被外部程序改过)时必崩,且这条路径被 IsPointInSide、subGetStretchPoints、subMoveStretchPointsAt、Draw2d 全都调用。
B4 中:SetRegionSlab(AcGePoint2dArray&) 未判空数组(:858)
arPts.last() / arPts.first() 直接用,调用方传空数组即崩。
B5 中:subGetGripPoints 三处未校验(:2594-2680)
gripPoints.at(0)(bitflags>=1394分支)没判空,ImpGetGripPoints返回空时越界;m_pGripAppData[i]以i < gripPoints.length()索引,数组容量GRIP_LENGTH=256(EarThwormDef.h:162)——异形板顶点 > 250 时越界写,memset(...,sizeof(AecGripData))一次踩上百字节;_tcscpy(m_pGripAppData[i].szID, m_sGUID)与szTip无长度校验(GUID 定长,暂时安全,但szTip是拼接出来的中文提示)。
B6 中:PreprocessForHlr clone 未判空(:4491-4493)
AcDbHatch *pNewH = (AcDbHatch*)pHatch->clone(); pNewH->setElevation(...) —— clone 失败即空指针解引用。
B7 中:SetCircleSlab(..., FALSE) 把基曲线留在了原点(:4062)
CircleImpMoveGripPointsAt 拖半径后调 SetCircleSlab(dR, dBomThick, ptOrg.z, ptOrg, FALSE)。bCenptAsMove=FALSE 时 SetCircleSlab 不会把 ptCenPos 加到基曲线上,基曲线停在 (0,0,dBomEl)。这违反头文件里"板的拉伸线必须是真实位置"的约定——后果是 3D 拉伸体跑到原点、2D/3D 错位,且 GetElBom() 仍然对(读的是 z),很难一眼发现。
B8 中:SetProperties 把"堰墙壁厚"写进了 KEY_HEIGHT(:4921-4922)
properties.findAndSet<double>(_T("堰墙高度"), pData, KEY_HEIGHT);
properties.findAndSet<double>(_T("堰墙壁厚"), pData, KEY_HEIGHT); // 应为 KEY_THICK
面板上改"堰墙壁厚"会覆盖"堰墙高度",壁厚本身不变——属于静默数据损坏。
B9 中:SetCircleBasin / SetRectBasin / SetFenceBasin 只在有墙时清旧墙(:1013、:1106、:1235)
RemoveAllX3dPart() 写在 if (dWallHeight>1e-3 && dWallWidth>1e-3) 内部,而 SetEllipseBasin 根本不调用。后果:
- 先建"带堰墙"的池,再以 0 壁厚重建 → 旧墙体 part 残留,属性显示无墙但 3D 有墙;
- 椭圆池每次改长轴/短轴/壁厚都
new AecDbX3dPart并AddX3dPart,墙体 part 不断累积(拖动长轴夹点几下就能叠出十几个)。
B10 轻:PeelBasinWall 标高在循环内累加(:1444、:1461、:1485)
三个分支都写 ptCenPos.z += dBomThick;,而 ptCenPos 是循环外变量。多个墙体 part 时第 2 个起标高就多抬一个板厚。
4. 运行逻辑问题
S1 半径/尺寸口径三处不一致(圆池)
| 位置 | 用的半径 | 结果 |
|---|---|---|
SetCircleBasin(:1184) |
SetSection((dR+0.5w)*2),KEY_RAD=dR |
截面外径 = 2dR+w |
GetBasinShapeCurve(:2094) |
dR += dWallWidth |
2D 轮廓半径 = dR+w(比截面大 0.5w) |
PeelBasinWall(:1457) |
dR = KEY_RAD-0.5w,之后 KEY_RAD += 0.5w |
墙中心线 = dR-0.5w |
三处各自自洽、合起来不自洽:2D 轮廓永远比 3D 截面大半个壁厚,IsPointInSide 的容差也随之偏大;KEY_RAD 的语义(外径?内径?)在读写之间来回切换,任何一次属性面板改写都会让三者漂移。建议:固定 KEY_RAD = 外半径、截面 2*KEY_RAD、墙中心线 KEY_RAD-0.5w,GetBasinShapeCurve 不再加壁厚。
S2 矩形池与圆形池的"尺寸含义"不同(:1046 vs :1184)
SetRectBasin 的截面是 dL×dW(不含墙),墙沿 ±dL/2 向外长;SetCircleBasin 的截面已经把半个壁厚算进去。同一套 UI 里输入的"长/宽/直径"含义不一致。
S3 类墙体板:SetRectSlab 把 dBomEl 硬编码成 0(:388-397)
SetBaseCurve(&ln); // z = dBomEl
SetAsWallData(dW,dThick,0.0); // ← dBomEl 传 0
SetAsWallData 里 m_mapSpecXData.SetAt(PIPE_EL, dBomEl) → PIPE_EL 恒为 0;GetAsWallData().dBomEl 永远是 0,只有基曲线 z 是对的。另外 SetAsWallData 内部 ptGe[0].y += (dBomEl + 0.5*dH); 之后又 += 0.5*dH;,等价于 y = dBomEl + dH——一旦有人传非 0 的 dBomEl,截面会整体偏一个 dH。
S4 类墙体板忽略 SetAsWallData 的失败返回(:388-397)
SetAsWallData 在 dThick<1e-3 || dH<1e-3 时返回 false,SetRectSlab 不检查、照样 return true。此时实体处于"有基曲线、无截面、无 KEY_*"的半初始化状态,后续 GetThick 走 KEY_SIZE2 兜底返回 0,Draw2d 走"无 KEY_ORGPT"分支。
S5 扇形板被记成圆板(:807)
SetSectorSlab 里 SetAt(KEY_SHAPE, eCircle)。之后 GetBasinShapeCurve 返回整圆(半径 dR),夹点、面积、体积、IsPointInSide 全按整圆算——扇形板的 1/4 或 1/2 面积凭空变大。应新增 eSector 或在 KEY_* 里存起止角并让 GetBasinShapeCurve 生成 AcDbArc + 两条半径。
S6 每次 MOVE/ROTATE 都让 KEY_POINTS 多一个顶点(:633-666 + CoreTools.cpp:3235)
subTransformBy 把 KEY_POINTS 解析成 polyline → setClosed(kTrue) → PolylineToString 回写。而 PolylineToString 对已闭合的 polyline 会再补一次首点,于是每变换一次顶点数 +1(首点重复)。板被反复移动后 KEY_POINTS 不断膨胀,region 重建(UserDefImpMoveGripPointsAt、subMoveStretchPointsAt)越来越慢,重复点还可能让 createFromCurves 失败。
修法:回写前 pPl->setClosed(kFalse)(或改用不追加首点的重载),只在写入时保持"末点=首点"。
S7 subTransformBy 在基类之前早退(:637)
if (!m_pRegion) return Acad::eOk; // 基类 subTransformBy 还没调用
AecDbBDPipeBase::subTransformBy(xform);
截面丢失的板 MOVE/ROTATE 完全无效(基曲线、KEY_ORGPT、PIPE_EL 都不动),且没有任何提示。应先调基类,再按需处理 region。
S8 夹点"改厚度"方向判断依赖 Y 分量为 0(:3530、:3719)
AcGeVector3d vtZOffset = vtOffset.orthoProject(AcGeVector3d::kXAxis); // 得到的是 YZ 分量,不是 Z 分量
...
if (vtZOffset.isCodirectionalTo(AcGeVector3d::kZAxis)) dThick += dLen; else dThick -= dLen;
orthoProject(kZAxis) 才是"垂直于 Z",所以上面这行本意是取 Z 分量却传了 kXAxis。只要拖动向量带 Y 分量(3D 视图、非正交拖动),方向判断恒为"减薄",改标高夹点也会斜着跑。修法:vtZOffset = vtOffset.dotProduct(AcGeVector3d::kZAxis) * AcGeVector3d::kZAxis;。
S9 多夹点拉伸被静默丢弃(:3415)
if (gripAppData.length() > 1) return Acad::eOk;——框选多个夹点一起拉,板一动不动,也不报错。要么支持批量,要么至少 acutPrintf 提示。
S10 subMoveGripPointsAt 形状夹点处理完不 return(:3465-3500)
if (pAppData->nIndex > 3) { switch(eShape){...} } 之后没有 return,继续落入下面的 switch(nGripIndex)(case 0/1/2/3 + default 空)。目前靠 default 兜住,一旦有人给 default 加逻辑,形状夹点就会被二次变换。
S11 SubAddDimPoints 闭合段标注是死代码(:2736、:2883)
for (ii = 0; ii < pPl->numVerts()-1; ii++) 内部又判断 if (ii == pPl->numVerts()-1)——恒为 false,最后一条边的尺寸永远不标注。
S12 两种厚度来源并存(:361、:1585、:2925)
GetThick():有 KEY_ORGPT 时读基曲线长度,否则读 KEY_SIZE2(类墙体板的截面高度);
SetElBom():用 ptEnd.distanceTo(ptSta) 当"长度"来反推顶面;
KEY_BOMTHICK:属性面板与 GetBimInfoTip 用的厚度。
三者不一致时(例如基曲线被夹点改过、KEY_BOMTHICK 未同步),提示框、属性面板、3D 实体会各说各话。
S13 Draw2d 圆板出图宽度的 polyline 几何不对(:2362-2385)
只建 3 个顶点(右→左→右),前两点带 tan(π/4) bulge,第三点回起点,实际画出来是"半圆+直线"而不是圆环线宽表达;且这条 pPl 进了 2D buffer,后续 DrawHatch 会把它当边界参与 ConvertBoundCurve。
S14 Draw2d 拖动时仍走完整重建(:2325)
if (dc.IsDragging()) { //return Acad::eOk; } 是空实现,拖动预览每一帧都重建 2D 缓存并 Add2DBuffer,大板拖拽时明显掉帧。
S15 ComparedTo 漏比形状与长短轴比(:4404)
只比 m_dBomThick / m_dBomEl / m_dData[0..2](L、W、R),不比 m_nShape 与 m_dData[3](radiusRatio)。长宽相同的矩形板和椭圆板会被判为"相同",EQUAL 类命令可能误合并。
S16 DXF 往返丢数据(:2584-2592)
dxfInFields / dxfOutFields 都是 return Acad::eOk; 空实现。存成 DXF 再打开,板的所有私有数据(含 region、KEY_*)全丢,只剩基类能恢复的部分。
S17 dwgInFields v1 分支不校验长度(:2479-2488)
pFiler->readUInt32(&nLen) 后直接 for (i<nLen) 读软指针,坏文件/恶意文件可让循环跑几十亿次。
S18 调试残留会污染图纸(:247-252)
GetOffsetCurve 失败时 acutPrintf("\nOffset error:%d") + 克隆一条红色曲线 AddToModelSpace。这是开发期排查手段,生产环境表现为"用户图里莫名多一条红曲线"。
S19 文案与键名错误(多处)
- 材质硬编码
"钢筋混土"(应为"钢筋混凝土")出现在 8 处,属性面板与材料统计都会显示错别字; - 墙体 part 的键名
_T("POIINTS")(双 I)与KEY_POINTS不是同一个键:写端/读端(PeelBasinWall)一致所以能工作,但拆墙后无法复用KEY_POINTS,走 eRect 分支重建时轮廓来源会变; subList(:2217)acutPrintf(_T("\n体积: 立方"), GetVolume()/1.e9)缺%f,体积永远不打印。
5. 已核对、确认不是问题的点
避免后续重复排查:
SetProperties里AcDbLine* pLn = AcDbLine::cast(GetBaseCurve()); ... SetBaseCurve(pLn); delete pLn;—— 不是 double free。AecDbEwPipeBase::GetBaseCurve()(AecDbEwPipeBase.cpp:2140)返回m_pBaseCurve->clone(),SetBaseCurve(const AcDbCurve*)(:4842)内部再 clone 一次并删旧指针,删的是克隆,正确。SetSection(pReg, ptCen)后紧跟delete pReg—— 正确,SetSection(AcDbRegion*)(AecDbEwPipeBase.cpp:5174)内部pReg->clone()。- 板内
subTransformBy变换m_pRegion—— 不是重复变换,基类AecDbEwPipeBase::subTransformBy(:1789)只动基曲线和 KEY_ORGPT/PIPE_EL。 GetSpecialtyData()与m_mapSpecXData是同一对象(AecDbEwPipeBase.cpp:6297返回&m_mapSpecXData),混用只是风格问题。m_pRegion由基类序列化(SectionRegionDwgInFields/DwgOutFields),正常存盘的圆板 region 不为空;B2 只在数据损坏/旧版本图纸时触发。SetRegionSlab写入的[标高;法向]后缀,CoreTools::StringToPolyline(CoreTools.cpp:3420-3437)确实会解析回 elevation/normal,PolylineToString(:3255)也会写回,不丢。m_pGripAppData容量 256,常规矩形(8 个夹点)、圆形(5 个)远低于上限。
6. 缺陷修复顺序
| 优先级 | 项 | 理由 |
|---|---|---|
| P0 | B1(FreeBuffer 死循环)、B2(m_pRegion 判空)、B3(eUserDef 空指针)、B8(KEY_HEIGHT 覆盖)、S7(早退)、S6(KEY_POINTS 膨胀) | 卡死/崩溃/静默数据损坏,改动小、可独立验证 |
| P1 | L1~L7(高频泄漏)、B7(基曲线回原点)、B9(旧墙残留/累积) | 连续布板、反复拖夹点的场景下累积明显 |
| P2 | S1~S5、S8、S9(口径与交互一致性) | 需要与产品确认尺寸语义,改动面较大 |
| P3 | L8、L9、B10、S10~S19(清理与文案) | 低风险,可随手带上 |
验证方式:本仓库无单测,改完按 CODEBUDDY.md 的约定编 Debug + R190(cmake --preset R190 && cmake --build --preset R190 --config Debug --target CoreLibrary_BAL_REMOTE),在 CAD 里加载后依次走:建矩形板/圆板/异形板/水池底板 → 拖各类夹点 → MOVE/ROTATE → 复制/镜像 → 存盘重开 → 拆堰墙 → 出图(HATCH)→ 消隐剖面。效率类改动的度量方法与基准图见第 12 章。
以下 6 章(第 7
12 章)为运行效率部分:先给热点结论,再拆一帧内的调用链,然后逐项分析 E1E11,最后给效率改造顺序与验证方法。行号同样基于 main @ d1bdf28,缺陷编号(L/B/S)沿用第 2~4 章。
7. 效率结论速览
| # | 热点 | 触发频率 | 单次代价 | 影响面 |
|---|---|---|---|---|
| E1 | AssertWriteEnabled 每次写使能清空 2D/3D 缓存 |
每个 setter、拖动每帧 | 释放全部缓存实体 + 下一帧全量重建 | 全局,最大 |
| E2 | DrawHatch 每帧克隆全部 2D 实体 + 布尔求边界 + evaluateHatch + 逐个打开洞口对象 |
每次重绘 | O(顶点数) 堆分配 + ACIS 布尔 + N 次 DB 打开 | 重绘/拖动 |
| E3 | 拖半径/长轴夹点时 SetCircleBasin/SetEllipseBasin 全量重建 region 与 3D 墙体 |
拖动每帧 | 1 次 createFromCurves + 1 次 AcDb3dSolid::Create + 1 次 kBoolUnite |
交互手感 |
| E4 | GetBasinShapeCurve 每次 new 曲线 + 连做 3 次 transformBy |
重绘、夹点查询、点选、拉伸 | 1 次堆分配 + 3×O(n) 顶点变换 | 全局 |
| E5 | KEY_POINTS 字符串 ⇄ polyline 反复解析;gripPoints.find O(n²) |
夹点查询 / 拖动 / 变换 | O(n) 字符串切分,异形板 O(n²) 去重 | 异形板 |
| E6 | IsPointInSide 里 Disperse(*pPl,50,...) + 模型空间逐实体调用 |
每次点选/拾取 | explode + 每弧段 50 份 + 2 个 float 数组 | 大图拾取 |
| E7 | 夹点与动态标注数量随顶点线性增长(dim grip ≈ 顶点数) | 每次选中/悬停 | 每夹点 new AcDbGripData + 256B memset |
大异形板 |
| E8 | GetBimInfoTip 经 GetArea()/GetVolume() 触发 3D 重建 |
每次悬停提示 | 一次完整 3D 建模 | 悬停 |
| E9 | 热路径上几十次 GetAt(_T("...")):临时 CString + std::map 查找 |
每帧 | 每次 1 次小堆分配 + O(log n) 宽字符比较 | 全局 |
| E10 | Add2DBuffer 线性去重 |
每次加入缓存 | O(缓存长度) 指针比较 | 轻微 |
一句话:这个类的性能问题不在算法,而在"每次写使能就把所有缓存扔掉"这条基类约定,叠加"重绘时现算填充、拖动时现建 ACIS 实体"。
8. 一帧到底跑了什么
8.1 拖动半径夹点(最坏路径)
subMoveGripPointsAt(3410)
├─ AssertWriteEnabled() → AecDbEwPipeBase.cpp:4755
│ ├─ Clean2DBuffer() → 释放上一帧全部 2D 实体
│ ├─ Clean3DBuffer() → 释放上一帧全部 3D 实体(含 3D solid)
│ └─ FreeBuffer(0, m_ar2DDrableBufPtrs2)
└─ CircleImpMoveGripPointsAt(4013)
└─ SetCircleBasin(1184)
├─ AssertWriteEnabled() → 再清一次缓存
├─ SetSection(2R, 0) → AecDbEwPipeBase.cpp:4924,createFromCurves(ACIS)
├─ SetBaseCurve(&ln) → AecDbEwPipeBase.cpp:4842,clone + 再清缓存
├─ SetAt(KEY_ORGPT/KEY_RAD/...) ×7 → map 写入
├─ RemoveAllX3dPart()
├─ new AecDbX3dPart + new AcDbX3dSolid
└─ Create(kPipe) + AddX3dSolid(kBoolUnite) → ACIS 建模 + 布尔
下一帧重绘又要把上面清掉的 2D/3D 全部重建一遍。拖一次半径 = 每帧一次 ACIS 建模 + 一次 region 布尔 + 一次 3D 实体布尔。
8.2 静止重绘
Draw2d(2251)
├─ DrawDTYG2D(dc) → AecDbBDPipeBase.cpp:278,有缓存则直接画
├─ Has2DBuffer()(2336) 为假时:
│ ├─ GetBasinShapeCurve(TRUE)(1954) → new 曲线 + 3 次 transformBy
│ ├─ dc.DrawEntity + Add2DBuffer
│ └─ DrawHatch(4621)
│ ├─ for 每个 2D 实体: isA()->create() + CopyObject() + SetZeroEl
│ ├─ ConvertBoundCurve(...) → 内部 region 布尔
│ ├─ ::DrawHatch(dc, sHatch, pPl) → 新建 hatch
│ ├─ for 每个关联洞口: AcDbObjectPointer 打开 + 解析 Points 字符串 + appendLoop
│ └─ evaluateHatch() → 图案求交
└─ 有缓存时:只遍历缓存 DrawEntity
静止场景靠缓存还能撑住;只要发生任何写操作(拖夹点、改属性、MOVE),缓存全废,下一帧走完整路径。
8.3 选中 / 悬停
subGetGripPoints(2594)
├─ ImpGetGripPoints(3109) → 异形板:解析 KEY_POINTS + getGeomExtents + 每顶点 find 去重
├─ 每个夹点:memset(256B) + new AcDbGripData + 3 个回调赋值
└─ SubAddDimPoints(2681) → 异形板:**再解析一次** KEY_POINTS + 每段 getFirstDeriv + 每段一个 dim grip
8.4 点选(外部驱动)
Pool3dCmd/pool3dcmdCmd.cpp:1678 在模型空间迭代里对每个可见 AecDbXSlab 调 IsPointInSide:
IsPointInSide(1712)
└─ GetBasinShapeCurve(true) → 又一次 new + 3 次 transformBy
└─ 折线分支:Disperse(*pPl,50,...)(1757)
├─ poly.explode(arCurSegs) → 每个段一个实体(矩形板也要 explode)
├─ 每段弧按 50 份采样
└─ 两个 float 数组 + 逐点 getPointAt
9. 逐项分析(E1~E11)
E1(最高优先级)写使能即清缓存
AecDbEwPipeBase::AssertWriteEnabled(CoreLibrary/AecDbEwPipeBase.cpp:4755)无条件执行:
Clean2DBuffer();
Clean3DBuffer();
FreeBuffer(0, m_ar2DDrableBufPtrs2);
AecDbCurveBase::AssertWriteEnabled(autoUndo, recordModified);
而 slab 的每个 setter、每次夹点移动都会走到它。后果:
- 拖动时 2D 缓存每帧失效,
Draw2d里Has2DBuffer()的早退分支形同虚设(AecDbSlab.cpp:2336); - 3D 缓存同样每帧失效,
GetArea()(AecRxBase/AecDbCurveBase.cpp:1156,缓存空时先Draw3d)、GetVolume()(AecDbEwPipeBase.cpp:6369)以及 HLR 预处理都要重建; PreprocessForHlr(4461)依赖 2D 缓存取 hatch,缓存一空就现场Draw2d,等于消隐时再跑一遍 1.2 的全流程。
改法:把清缓存从 AssertWriteEnabled 移到真正改几何的入口(SetBaseCurve、SetSection、subTransformBy、各 SetXxxSlab/SetXxxBasin),或者加一个 m_bBatchEdit 在拖动会话期间抑制清理、松开时清一次。
风险:这是基类行为,AecDbEwPipe、AecDbEwFitting、AecDbECFitting 等 20+ 处依赖它(仓库里有 20 余处显式 Clean2DBuffer() 调用,说明本来就有"显式清理"的先例)。改动需要逐类回归,建议先只给 slab 加抑制开关,不动基类。
E2 DrawHatch 每帧全量重建
AecDbSlab.cpp:4621。每次重绘都:
- 遍历 2D 缓存,对每个实体
pEnt->isA()->create()+CopyObject造克隆(4633-4647); KTCADUtilityEx::ConvertBoundCurve求外边界(内部是 region 布尔);::DrawHatch新建AcDbHatch;- 遍历
m_arAssocPartEntIds,每个洞口AcDbObjectPointer<AecDbX3dPartEntity>打开一次数据库对象,再解析Points字符串、appendLoop; evaluateHatch()。
一个带 3 个洞口的水池底板,重绘一次 = 3 次 DB 打开 + 3 次字符串解析 + 1 次布尔 + 1 次图案求交。
改法:
- 把
AcDbHatch*结果缓存成成员,几何 dirty 才重建(配合 E1,dirty 标志天然可用); - 拖动期间(
dc.IsDragging())直接跳过洞口 loop 与evaluateHatch,只画外轮廓; - 洞口
AcDbObjectId与解析后的AcDbPolyline缓存在实体上,避免每帧打开对象。
顺带:DELETE_PTR(pCurve) 在 4660 与 4732 出现两次(宏会置空,不崩,但说明收尾逻辑没理顺);pPl 与 pCurve 是同一指针,4660 之后 pPl 已悬空,目前恰好没再用到。
E3 拖动时全量重建 region 与 3D 墙体
CircleImpMoveGripPointsAt(4013)与 EllispeImpMoveGripPointsAt(4068)在每次鼠标移动时直接调 SetCircleBasin / SetEllipseBasin:
SetCircleBasin(1184):SetSection(2R,0)→createFromCurves;SetBaseCurve→ clone;带墙时RemoveAllX3dPart+new AcDbX3dPart+new AcDbX3dSolid+Create(kPipe)+kBoolUnite;SetEllipseBasin(1262):额外getOffsetCurves+ region 布尔(并有泄漏,见 2 章 L6)。
拖 60 帧 = 60 次 ACIS 建模 + 60 次布尔。这是"拖半径时明显卡顿、内存上涨"的直接原因。
改法:拆成两条路径——
- 轻量:只改
KEY_RAD/KEY_RRATIO+ 基曲线 + 对已有 region 施加缩放矩阵(圆/椭圆 region 支持transformBy),不重建; - 重量:松开夹点(或
bitflags表明非拖动)时才SetCircleBasin全量重建。 墙体AecDbX3dPart复用已有 part,只改其参数,不new。
E4 GetBasinShapeCurve 的分配与三次变换
AecDbSlab.cpp:1954。每次调用:
new AcDbPolyline/new AcDbCircle/new AcDbEllipse(调用方负责 delete,多数分支确实删了,异形板分支漏删见 L3);- 末尾固定做三次
transformBy(2137-2148):X 轴、Y 轴、Z 轴各一次,即使m_dRotate[]全为 0 也照做。
对 n 顶点异形板,一次调用 = 3n 次顶点矩阵乘 + 3 次曲线内部缓存失效。而这个函数在重绘、IsPointInSide、subGetStretchPoints、subMoveStretchPointsAt、SubAddDimPoints 路径上都会被调。
改法:
- 三个角度都接近 0 时整体跳过;否则用
AcGeMatrix3d相乘合成一个矩阵,只transformBy一次; - 对圆/椭圆且仅 Z 旋转时,直接改 center/majorAxis,不必变换整条曲线。
E5 KEY_POINTS 字符串往返 + O(n²) 去重
异形板的顶点以字符串存在 KEY_POINTS(PointsScrToPolyline,108)。热点在于解析次数:
| 场景 | 解析次数 |
|---|---|
选中一次(subGetGripPoints) |
2 次(ImpGetGripPoints 3240 一次、SubAddDimPoints 2867 一次) |
| 拖动一个顶点 | 1 次解析 + 1 次序列化(UserDefImpMoveGripPointsAt 4151) |
| MOVE/ROTATE | 1 次解析 + 1 次序列化(subTransformBy 633) |
| 重绘(缓存失效时) | 1 次(GetBasinShapeCurve 2118) |
解析本身是 CoreTools::StringToPolyline(失败再退化到 SplitStrintToArray + _ttof),O(n) 次 CString::Find + 子串分配;序列化 PolylineToString 也是逐点 Format 拼接。
另外 ImpGetGripPoints 里:
for (int i = 0;i < pPl->numVerts();i++) {
...
if (gripPoints.find(ptGeT) == -1) // :3240,AcGePoint3dArray 线性查找
gripPoints.append(ptGeT);
}
顶点越多越慢,是 O(n²);而夹点顺序本来就对应顶点顺序,这个去重没有必要(重复点只会来自数据损坏)。
改法:
- 在实体上加
mutable AcGePoint2dArray m_arCachePts; mutable bool m_bPtsDirty;,解析一次后复用;SetAt(KEY_POINTS)时置 dirty; - 去掉
find去重(或改用std::unordered_set<int64>存量化坐标); subTransformBy只在真正发生变换时序列化(当前无条件回写,且因 S6 的闭合点重复,串会越写越长,代价随时间上升)。
E6 IsPointInSide 的分化与调用位置
AecDbSlab.cpp:1712。折线分支(1757):
AcDbPolyline polyBoundry;
Disperse(*pPl,50,polyBoundry); // explode + 每弧段 50 份
int nPts = polyBoundry.numVerts();
float* pfPtx = new float[nPts]; // 两个临时数组
float* pfPty = new float[nPts];
Disperse(1628)第一件事就是 poly.explode(arCurSegs)——矩形板(无 bulge)也照 explode 一遍,纯浪费。50 份/弧 的精度是写死的,与当前视图缩放无关。
调用方在 Pool3dCmd/pool3dcmdCmd.cpp:1678:模型空间迭代中对每个可见板调用一次。50 块板的图,一次拾取 = 50 次曲线构造 + 50 次 explode。
改法:
- 先用包围盒粗筛(
KEY_ORGPT+KEY_Length/Width/RAD,或缓存 extents),不相交直接返回 false; - 矩形/圆用解析判断(当前圆分支已经是解析的),只有带 bulge 的异形板才走
Disperse; nCirDiv按当前视口像素尺寸自适应,而不是固定 50;- 用
AcGePoint2dArray+ 直接遍历,避免两个 float 数组(顺带解决 float 精度问题,见 11.3)。
E7 夹点与动态标注的规模
subGetGripPoints(2594)每个夹点都:
memset(&m_pGripAppData[i],0,sizeof(AecGripData)); // 256 项数组里的一项
pGripData = new AcDbGripData; // 每夹点一次堆分配
pGripData->setGripPoint/setAppData/setWorldDraw/setToolTipFunc
_tcscpy(szTip,...); _tcscpy(szID,...)
SubAddDimPoints 又为每段加一个 dim grip,于是 n 顶点异形板的夹点数 ≈ 3 + n + n。200 顶点的板选中一次 = 400+ 次 new AcDbGripData + 400 次 256B memset + 400 次回调设置,之后宿主还要逐个绘制夹点。
改法:dim grip 做 LOD——顶点数超过阈值(如 32)时只标注外包矩形,或按 curViewUnitSize 决定是否出标注。这是显示增强,不是核心交互,砍掉不影响编辑。
E8 BIM 提示触发 3D 重建
GetBimInfoTip(2225)调 GetArea() 与 GetVolume():
GetArea()(AecDbCurveBase.cpp:1156)在!Has3DBuffer()时先AcGiViewDraw dc; Draw3d(dc);,再遍历 3D 实体求面积;GetVolume()(AecDbEwPipeBase.cpp:6369)非 body 时转AecDbCurveBase::GetVolume,同样依赖 3D 缓存。
而 3D 缓存又被 E1 每次写使能清掉。结果是:鼠标在板上悬停 → 提示框 → 一次完整 3D 建模。
改法:面积/体积用参数直接算——KEY_Length × KEY_Width、π × KEY_RAD²、× KEY_BOMTHICK;异形板用缓存的 AcGePoint2dArray 鞋带公式。只有用户显式查询时才走精确 3D。
E9 键字面量与 map 查找
KEY_* 全是 #define KEY_Length _T("len")(inc/EarThwormDef.h:1367),因此每次
GetSpecialtyData()->GetAt(KEY_Length, dL);
都会构造一个临时 CString(非空串走堆分配),再到 std::map<CString, AecXData>(inc/AecDbObjXDataMap.h:284)里做 O(log n) 的宽字符比较。
单次开销不大,但 ImpGetGripPoints、SubAddDimPoints、GetBasinShapeCurve、GetProperties 每个函数都有 5-15 次这种调用,乘上每秒几十次的调用频率就不可忽略。
改法:
- 把
KEY_*改成static const CString(或inline constexpr wchar_t[]+GetAt的LPCTSTR重载),消除临时对象; - 每个函数开头
const AecDbObjXDataMap* pData = GetSpecialtyData();,一次取多个键; - 更高阶:把 slab 热路径需要的 6-8 个参数缓存在成员里,写时同步(当前每次都要回 map 查)。
E10 Add2DBuffer 的线性去重
AecRxBase/AecDbEntityCommon.cpp:104:
if (pEnt && m_ar2DDrableBufPtrs_.find(pEnt) == -1)
m_ar2DDrableBufPtrs_.append(pEnt);
每次加入都线性扫描。slab 的缓存通常只有几条(外轮廓 + hatch),影响很小;但 DrawXShape2d(2427)会把 OrthoProjected3DSolid 产出的所有投影曲线逐条加入,截面复杂时是 O(n²)。优先级低,顺手可改。
E11 其他小项
GetRotation(Z)在 1995/2713/3163/3916/4037/4099 共 6 处调用,本身是数组读,不贵——但它恒返回 0(见 11.1),导致"按旋转方向计算夹点"这段逻辑白算;GetProperties(4735)每次刷新做 10+ 次GetAt+SimpleProperty::makeAndInsert(后者内部有CString格式化和容器插入),属性面板打开/切页时是主要开销,但不在绘制循环里,优先级低;SetProperties(4899)改板厚时也会把KEY_POINTS解析→改标高→序列化→再写回(eLB/eGB 分支),纯字符串往返,可只改KEY_ORGPT/KEY_BOMTHICK后置 dirty;GetBaseCurve()返回 clone,SetBaseCurve()再 clone 一次再删旧指针(见 5 章"已核对、确认不是问题的点"),每次属性写多两次分配,量级小。
10. 效率改造顺序
| 阶段 | 动作 | 预期收益 | 风险 |
|---|---|---|---|
| 1 | 拖动期间抑制 Clean2DBuffer/Clean3DBuffer(slab 局部开关),松开后清一次 |
拖动帧率提升最明显 | 低(不改基类) |
| 2 | Draw2d 在 dc.IsDragging() 时跳过 DrawHatch 与洞口 loop |
拖动/平移大幅减负 | 低 |
| 3 | GetBimInfoTip 改解析式算面积/体积 |
悬停不再触发 3D 建模 | 低 |
| 4 | 圆/椭圆拖夹点走轻量路径(只改参数 + 缩放 region) | 拖半径不再每帧 ACIS 建模 | 中(要保证 region 与参数同步) |
| 5 | GetBasinShapeCurve 合成单矩阵 + 零角度跳过 |
重绘/点选线性下降 | 低 |
| 6 | KEY_POINTS 内存缓存 + 去掉 O(n²) 去重 |
异形板夹点/拖动线性化 | 中(缓存失效要覆盖所有写点) |
| 7 | IsPointInSide 加包围盒粗筛 + 解析判断矩形/圆 |
大图拾取从 O(N·n) 降到 O(N) | 低 |
| 8 | 夹点/标注 LOD | 大板选中不卡 | 低(显示层) |
11. 顺带发现(跨章,含新发现的逻辑 bug)
11.1 GetRotation(Z) 恒返回 0
AecDbBDPipeBase.cpp:233:
if (nAxisIndex > -1 && nAxisIndex < 2) // 只放行 0、1
return m_dRotate[nAxisIndex];
return 0.0;
而 X=0, Y=1, Z=2(Arx/R19.0/inc/adsdef.h:62 的 enum { X = 0, Y = 1, Z = 2 })。所以 slab 里 6 处 GetRotation(Z) 全部拿到 0:旋转过的板,夹点方向、标注方向都按未旋转算,而 GetBasinShapeCurve 用的是 m_dRotate[Z](正确),于是画出来的轮廓转了、夹点没转。改 nAxisIndex < 3 即可(需确认其它实体的 0/1/2 语义一致)。
11.2 DrawHatch 重复 DELETE_PTR(pCurve)
4660 与 4732 各一次。宏内会置空,第二次是空操作,不崩,但 pPl(同一指针的 cast)在 4660 之后已悬空,后续若有人加代码用到 pPl 就是 use-after-free。
11.3 IsPointInSide 的 float 精度
1760-1770 把 double 坐标截断成 float 再交给 IsPointInPoly。坐标绝对值上百万(大坐标总图)时 float 只有约 7 位有效数字,误差可达 0.06~0.1 m,边界点判断会随机抖动。改成 double 数组或直接用 AcGePoint2d。
11.4 Disperse 对矩形也 explode
Disperse(1628)无条件 poly.explode(arCurSegs),之后才判断 bulge。矩形板、正交折线板全部白 explode 一遍(还要 FreeBuffer)。可先扫一遍 getBulgeAt,全为 0 时直接复制顶点。
12. 验证方法(本仓库无单测)
基准图:50 块板,其中 10 块异形板(200 顶点)、5 个带堰墙水池(3 个洞口)、5 块旋转 30° 的板。
| 指标 | 测法 | 期望 |
|---|---|---|
| 拖动半径夹点帧率 | 开 PERF 计时,累计 SetCircleBasin 次数与总耗时 |
阶段 1+2+4 后单帧 < 5 ms |
| 静止重绘 | 连续平移视图,统计 DrawHatch 调用次数 |
缓存命中时不应再进 DrawHatch |
| 悬停提示 | 鼠标扫过 50 块板,统计 Draw3d 次数 |
阶段 3 后应为 0 |
| 框选/拾取 | 拾取 50 块板中心点,统计 IsPointInSide 内 Disperse 次数 |
阶段 7 后仅异形板触发 |
| 内存 | 拖动 30 秒后看私有字节 | 不应随帧数线性增长(对照 2 章 L1~L7) |
临时埋点(改完删掉):
static LONGLONG s_t = 0; static int s_n = 0;
LONGLONG t0 = GetTickCount64();
... 被测代码 ...
s_t += GetTickCount64() - t0; ++s_n;
if (s_n % 100 == 0) acutPrintf(_T("\n%s: %d 次, 累计 %lld ms"), _T("DrawHatch"), s_n, s_t);
编译按 CODEBUDDY.md:cmake --preset R190 && cmake --build --preset R190 --config Debug --target CoreLibrary_BAL_REMOTE(只编 CoreLibrary,不跑全量)。