From 7038a984410ff17e0d44cb386f4e834769594e89 Mon Sep 17 00:00:00 2001 From: company-desktop-computer Date: Wed, 28 Oct 2026 21:28:00 +0800 Subject: [PATCH] =?UTF-8?q?diag:=20=E7=94=A8=20PDB=20=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E6=A0=88=E5=B8=A7=EF=BC=8C=E5=AE=9A=E4=BD=8D=E5=88=B0=20AecDbE?= =?UTF-8?q?wPipeBase::copyFrom=20=E9=87=8C=E7=9A=84=E5=A4=9A=E4=BD=99?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版只给了"模块+RVA"的栈,我据此先后推断成 AecDbCurveBase::subGetTransformedCopy 和 AecDbEntityCommon::base_DeepClone,两者都是错的 (用户在那里下断点不命中;新加的 PerfTraceMark 打点也没有触发)。本次用符号解析定死。 本次改动: - 新增 inc/PerfTraceMark.h:inline 的"某函数被调用过"打点,Debug 且 AEC_WRITEBACK_TRACE=1 才生效。AecRxBase.arx 不链接 CoreLibrary(静态库只被 CoreCommand 链接),拿不到 SectionPerf, 所以做成 header-only。 - 在 5 个候选入口打点:AecDbCurveBase::subGetTransformedCopy / subDeepClone、 AecDbEntityBase::subDeepClone、AecDbEntityCommon::base_DeepClone、CKTCadUtility::CloneObject。 跑一次开图,5 个都没触发 —— 这些候选被排除。 - 新增 tools/resolve_rva.ps1:用 dbghelp 的 SymLoadModuleEx / SymFromAddr 配本地 PDB, 把"模块+RVA"解析成函数名。Windows 没有自带 symbolizer,这个脚本很轻,可反复用。 解析结果(CoreCommand.arx): 0x58f051 AecDbEwPipeBase::copyFrom+0x41 0x7e1300 AecDbZLineStair::DrawGuarBar3d+0x2c0 0x7e09d0 AecDbZLineStair::DrawSteep3d+0xbe0 0x7ddcef AecDbZLineStair::Draw3d+0x34f 真实成因:Z 型楼梯画护栏(DrawGuarBar3d)时 copyFrom 一份管道实体, 而 AecDbEwPipeBase::copyFrom 用通用的 CopyObject(inc/AecDbEntityCommon.h:331) 做"整实体 dwgOutFields/dwgInFields 往返"来拷字段,于是截面被写成临时 SAT 文件再读回来。 而且这次序列化是多余的:copyFrom 随后把所有字段又用内存级方式拷了一遍 (曲线 clone/copyFrom、截面 clone/copyFrom、XData CopyFrom、m_arUserInfs append、 标志位/图层/线型/Breaks 逐项赋值),WriteUserInterface 写的也只是 m_arUserInfs。 所以最小修改是把那句 CopyObject 去掉,并补上源为 NULL 时清空目标的分支 (目前这个"清空"是 CopyObject 顺带完成的)。 本次只做定位与记录(分析写进 pipe-section-region-performance-plan.md 并纠正了上一版的误判), 不改 copyFrom 的行为——它影响所有管系实体的复制粘贴/块内拷贝,需要单独验证。 --- .../Code2026/AecRxBase/AecDbCurveBase.cpp | 3 + .../Code2026/AecRxBase/AecDbEntityBase.cpp | 2 + .../Code2026/AecRxBase/AecDbEntityCommon.cpp | 2 + .../Code2026/CoreLibrary/KTCadUtility.cpp | 2 + SourceCode/Code2026/inc/PerfTraceMark.h | 58 +++++++++++ pipe-section-region-performance-plan.md | 62 +++++++----- tools/resolve_rva.ps1 | 96 +++++++++++++++++++ 7 files changed, 201 insertions(+), 24 deletions(-) create mode 100644 SourceCode/Code2026/inc/PerfTraceMark.h create mode 100644 tools/resolve_rva.ps1 diff --git a/SourceCode/Code2026/AecRxBase/AecDbCurveBase.cpp b/SourceCode/Code2026/AecRxBase/AecDbCurveBase.cpp index 0c433e8..da97550 100644 --- a/SourceCode/Code2026/AecRxBase/AecDbCurveBase.cpp +++ b/SourceCode/Code2026/AecRxBase/AecDbCurveBase.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include #include "AecDbCurveBase.h" +#include "PerfTraceMark.h" #include "AcGiViewDraw.h" #include "AcGiExplodeDraw.h" #include "simpleproperty.h" @@ -306,6 +307,7 @@ Acad::ErrorStatus AecDbCurveBase::subTransformBy(const AcGeMatrix3d& xform) Acad::ErrorStatus AecDbCurveBase::subGetTransformedCopy(const AcGeMatrix3d& xform,AcDbEntity*& pEnt) const { + PerfTraceMark("AecDbCurveBase::subGetTransformedCopy"); assertReadEnabled(); AcDbEntity *pCloned = (AcDbEntity *)clone(); pCloned->setLayer(layerId());// @@ -324,6 +326,7 @@ Acad::ErrorStatus AecDbCurveBase::subDeepClone( AcDbObject* pOwner, AcDbIdMapping& idMap, Adesk::Boolean isPrimary) const { + PerfTraceMark("AecDbCurveBase::subDeepClone"); // 具体实现与 AecDbEntityBase 共用 AecDbEntityCommon::base_DeepClone, // 这里只保留 AcDbObject 虚函数这一侧的入口。 return AecDbEntityCommon::base_DeepClone(pOwner, pClonedObject, idMap, isPrimary); diff --git a/SourceCode/Code2026/AecRxBase/AecDbEntityBase.cpp b/SourceCode/Code2026/AecRxBase/AecDbEntityBase.cpp index b79d2d7..e811f2d 100644 --- a/SourceCode/Code2026/AecRxBase/AecDbEntityBase.cpp +++ b/SourceCode/Code2026/AecRxBase/AecDbEntityBase.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "AecDbEntityBase.h" +#include "PerfTraceMark.h" #include "AcGiViewDraw.h" #include "AcGiExplodeDraw.h" #include "simpleproperty.h" @@ -280,6 +281,7 @@ Acad::ErrorStatus AecDbEntityBase::subDeepClone( AcDbObject* pOwner, AcDbIdMapping& idMap, Adesk::Boolean isPrimary) const { + PerfTraceMark("AecDbEntityBase::subDeepClone"); return AecDbEntityCommon::base_DeepClone(pOwner, pClonedObject, idMap, isPrimary); } diff --git a/SourceCode/Code2026/AecRxBase/AecDbEntityCommon.cpp b/SourceCode/Code2026/AecRxBase/AecDbEntityCommon.cpp index 37ad00b..0c760c9 100644 --- a/SourceCode/Code2026/AecRxBase/AecDbEntityCommon.cpp +++ b/SourceCode/Code2026/AecRxBase/AecDbEntityCommon.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "AecDbEntityCommon.h" +#include "PerfTraceMark.h" #include "AecDbObjXDataMap.h" #include "AcGiViewDraw.h" #include "dbobjptr.h" @@ -1234,6 +1235,7 @@ Acad::ErrorStatus AecDbEntityCommon::base_DeepClone( AcDbObject* pOwner, AcDbIdMapping& idMap, Adesk::Boolean isPrimary) const { + PerfTraceMark("AecDbEntityCommon::base_DeepClone"); // You should always pass back pClonedObject == NULL // if, for any reason, you do not actually clone it // during this call. The caller should pass it in diff --git a/SourceCode/Code2026/CoreLibrary/KTCadUtility.cpp b/SourceCode/Code2026/CoreLibrary/KTCadUtility.cpp index b76e13b..8fdc914 100644 --- a/SourceCode/Code2026/CoreLibrary/KTCadUtility.cpp +++ b/SourceCode/Code2026/CoreLibrary/KTCadUtility.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" //#include "Resource.h" #include "KTCadUtility.h" +#include "PerfTraceMark.h" #include #include #include "Kingtouch.h" @@ -427,6 +428,7 @@ Acad::ErrorStatus CKTCadUtility::AddToCurrentSpace(AcDbEntity* pEnt, AcDbObjectI // 在相同Database中,拷贝实体, 仅适合简单实体, 且复制对象没有加入数据库 Acad::ErrorStatus CKTCadUtility::CloneObject(AcDbObject*& pObjDest, const AcDbObject* pObjSrc) { + PerfTraceMark("CKTCadUtility::CloneObject"); ASSERT(pObjSrc != NULL); BOOL bCreate = FALSE; diff --git a/SourceCode/Code2026/inc/PerfTraceMark.h b/SourceCode/Code2026/inc/PerfTraceMark.h new file mode 100644 index 0000000..1851237 --- /dev/null +++ b/SourceCode/Code2026/inc/PerfTraceMark.h @@ -0,0 +1,58 @@ +// PerfTraceMark.h —— 极轻量的"某函数被调用过"打点,用于在没有调试器时定位调用路径 +// +// 用法:在候选函数入口写 PerfTraceMark("函数名"), +// 每次进程内该 tag 只记录第一次,追加到 %TEMP%\aec_writeback_trace.txt。 +// 只在 Debug(NDEBUG 未定义)且环境变量 AEC_WRITEBACK_TRACE=1 时生效,发布版本零开销。 +// +// 为什么不用 SectionPerf:AecRxBase.arx 不链接 CoreLibrary(静态库只被 CoreCommand 链接), +// 所以 AecRxBase 的文件里拿不到 SectionPerf 的符号。本头文件是 inline 的,哪个模块都能用。 + +#ifndef PERF_TRACE_MARK_H +#define PERF_TRACE_MARK_H + +#include +#include +#include +#include +#include + +inline void PerfTraceMark(const char* pszTag) +{ +#ifndef NDEBUG + static bool s_bInit = false; + static bool s_bOn = false; + static std::set s_seen; + + if (!s_bInit) + { + s_bInit = true; + const char* psz = ::getenv("AEC_WRITEBACK_TRACE"); + s_bOn = (psz != NULL && psz[0] != '0'); + } + if (!s_bOn || pszTag == NULL) + return; + + const std::string sTag(pszTag); + if (s_seen.find(sTag) != s_seen.end()) + return; + s_seen.insert(sTag); + + char szTmp[MAX_PATH + 1]; + ::memset(szTmp, 0, sizeof(szTmp)); + if (::GetTempPathA(MAX_PATH, szTmp) == 0) + return; + std::string sPath(szTmp); + sPath += "aec_writeback_trace.txt"; + + FILE* fp = NULL; + if (fopen_s(&fp, sPath.c_str(), "ab") == 0 && fp != NULL) + { + fprintf(fp, "[mark] %s\n", pszTag); + fclose(fp); + } +#else + (void)pszTag; +#endif +} + +#endif // PERF_TRACE_MARK_H diff --git a/pipe-section-region-performance-plan.md b/pipe-section-region-performance-plan.md index 8b0ed25..1627759 100644 --- a/pipe-section-region-performance-plan.md +++ b/pipe-section-region-performance-plan.md @@ -322,46 +322,60 @@ pClone->dwgIn(&filer); // → dwgInFields → acisIn 读回来 (`AecDbX3dPartEntity.cpp:757` 直接 `clone()`),但**尚未证实它就是本次的调用者**, 需要用 PDB 解析上面三个 RVA 才能钉死。 -**哪个类在回写**(按实体类统计): +**哪个类在回写**: ``` ---- write-backs by entity class ---- 1140 AecDbEwPipeBase ``` -全部 1140 次都是 **`AecDbEwPipeBase` 基类实例**(不是 `AecDbEwPipe`,也不是任何具名子类)。 -它与属性 key 统计里 `AecDbEwPipeBase` 1172 次几乎一一对应 → -图纸里有约 1172 个基类实例,绘制期每个被深拷贝一次。 -(这些实例由 `AecDbAssembleWell.cpp` 的装配井代码创建,多为矩形截面段。) +全部 1140 次都是 **`AecDbEwPipeBase` 基类实例**(不是 `AecDbEwPipe`,也不是具名子类), +与属性 key 统计里 `AecDbEwPipeBase` 的 1172 次几乎一一对应。 +这些实例由 `AecDbAssembleWell.cpp` 的装配井代码创建,多为矩形截面段。 -**完整调用链**(文件:行): +**完整调用链(已用 PDB 解符号确认)** + +前期用 `AEC_WRITEBACK_TRACE=1` 抓到栈,但帧里的 RVA 当时没解析,先后误判成 +`AecDbCurveBase::subGetTransformedCopy` 与 `AecDbEntityCommon::base_DeepClone`—— +两者都已被证伪(断点不命中、`PerfTraceMark` 打点也没触发)。 +用 `tools/resolve_rva.ps1`(dbghelp + 本地 PDB)解析后,真实链路是: | 层次 | 位置 | | --- | --- | -| 视口绘制 | `AecDbEntityCommon::base_ViewportDraw` → `AecDbCurveBase::subViewportDraw` | -| 取变换副本 | `AecDbCurveBase::subGetTransformedCopy`(`AecRxBase/AecDbCurveBase.cpp:307`) | -| ↑ 其中的 `clone()` | 触发深拷贝 | -| 深拷贝实现 | `AecDbEntityCommon::base_DeepClone`(`AecRxBase/AecDbEntityCommon.cpp:1232`,`dwgOut` 在 :1265) | +| Z 型楼梯绘制 | `AecDbZLineStair::Draw3d` → `DrawSteep3d` → **`DrawGuarBar3d`** | +| 拷贝管道实体 | `AecDbEwPipeBase::copyFrom`(`CoreLibrary/AecDbEwPipeBase.cpp:1605`) | +| ↑ 调用 | `CopyObject(*pBase,*this)`(同文件 `:1627`) | +| **CopyObject 模板** | **`inc/AecDbEntityCommon.h:331`**:`AcDbDeepCloneFiler` + `src.dwgOutFields()` + `dest.dwgInFields()` | | 写字段 | `AecDbEwPipeBase::dwgOutFields`(`CoreLibrary/AecDbEwPipeBase.cpp:1440`) | -| **写截面** | **`AecDbEwPipeBase::RegionAcisoutDwgOutFields`(`CoreLibrary/AecDbEwPipeBase.cpp:1275`)** | +| **写截面** | **`AecDbEwPipeBase::RegionAcisoutDwgOutFields`(同文件 `:1275`)** | -即:`getTransformedCopy` → `clone()` → 全实体序列化 → 截面走 `acisOut` 写临时 SAT 文件。 +即:**楼梯画护栏时会 `copyFrom` 一份管道实体,而 `copyFrom` 用"整实体序列化"来拷字段, +于是截面被写成一个临时 SAT 文件再读回来。** -注意 `AecDbEwPipeBase::copyFrom`(:1605)里复制截面用的是 -`m_pRegion->copyFrom(...)`(内存级 ACIS 拷贝),比"写文件再读回"便宜得多。 -所以最局部的修法可能是:给管系实体加一个 `subGetTransformedCopy` 重载, -用 `isA()->create()` + `copyFrom()` 代替 `clone()`,绕开整条序列化链路。 -**但 clone 与 copyFrom 的语义并不完全等价,需要先核对**(扩展字典、XData、数据库驻留属性等)。 +**关键点:那次序列化是多余的。** `copyFrom` 在 `CopyObject` 之后又把所有字段用内存级方式 +拷了一遍: -可选修法(都需要动 `base_DeepClone` 的契约,属于结构性改动,建议单独立项): - -| 方案 | 做法 | 风险 | +| 字段 | `CopyObject`(序列化往返) | `copyFrom` 后面手工做的 | | --- | --- | --- | -| A | 用 `pFiler->isKindOf(AcDbDeepCloneFiler::desc())` 识别深拷贝,`dwgOutFields` 只写占位符不落盘;克隆完成后由 `base_DeepClone` 用 `m_pRegion->clone()` 在内存里补 | 要给 `AecDbEntityCommon` 加一个"重数据拷贝"的虚钩子,影响面大 | -| B | 缓存里同时保存每份 SAT 的原文,`dwgOutFields` 直接写缓存文本,省掉 `acisOut` | 若 `m_pRegion` 在 restore 之后被就地修改过,写出去的是旧几何——必须先审计全库对 `m_pRegion` 的写操作 | -| C | 维持现状 | 每次开图多 ~2.4 s(占 98 s 的 2.4%) | +| 曲线 | 序列化 | `m_pBaseCurve->clone()` / `copyFrom()` | +| 截面 | **acisOut → 临时文件 → acisIn** | `m_pRegion->clone()` / `copyFrom()` | +| spec XData | 序列化 | `m_mapSpecXData.CopyFrom()` | +| 用户界面数据 | 序列化 | `m_arUserInfs.append()` | +| 标志位 / 图层 / 线型 / Breaks | 序列化 | 逐项赋值 | -**A 与 B 都需要先做代码审计**,不适合和本缓存改动混在一起。 +`WriteUserInterface` 只写 `m_arUserInfs`,同样已被手工拷贝。 +所以最小改动是:**把 `copyFrom` 里那句 `CopyObject(*pBase,*this)` 去掉**, +再补上源对象为 NULL 时清空目标的分支(目前这个"清空"是 `CopyObject` 顺带完成的)。 + +预计收益:省掉 1140 次 `acisOut`(2.38 s)及其对应的读回; +`copyFrom` 本身也从"全实体序列化"变成纯内存拷贝。 + +风险:`copyFrom` 是所有管系实体(管道/管件/井…)的公共拷贝路径,涉及复制粘贴、块内拷贝等, +必须实测验证(复制粘贴后截面、XData、GUID、图层线型是否都正确)。 + +> 诊断手法记录:`AEC_WRITEBACK_TRACE=1` + `SectionPerf::TraceCallStack()` 出栈, +> 再用 `tools/resolve_rva.ps1` 把"模块+RVA"解析成函数名。 +> 单靠栈不解析符号很容易误判(本次就误判了两次)。 ### Phase 2:临时目录治理(可选,止血) diff --git a/tools/resolve_rva.ps1 b/tools/resolve_rva.ps1 new file mode 100644 index 0000000..d045526 --- /dev/null +++ b/tools/resolve_rva.ps1 @@ -0,0 +1,96 @@ +# resolve_rva.ps1 - resolve module+RVA to function names using dbghelp + local PDB +# usage: powershell -NoProfile -ExecutionPolicy Bypass -File resolve_rva.ps1 ` +# -Module "C:\...\CoreCommand.arx" -Rva 0x58f051,0x7ddcef,0x7e09d0,0x7e1300 + +param( + [Parameter(Mandatory=$true)][string]$Module, + [Parameter(Mandatory=$true)][string[]]$Rva, + [string]$SymbolPath = '' +) + +$ErrorActionPreference = 'Stop' +$mod = (Resolve-Path $Module).Path +$dir = Split-Path $mod -Parent +if (-not $SymbolPath) { $SymbolPath = $dir } + +$cs = @' +using System; +using System.Runtime.InteropServices; + +public static class Dbg { + [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)] + public struct SYMBOL_INFO { + public uint SizeOfStruct; + public uint TypeIndex; + public ulong Reserved1; + public ulong Reserved2; + public uint Index; + public uint Size; + public ulong ModBase; + public uint Flags; + public ulong Value; + public ulong Address; + public uint Register; + public uint Scope; + public uint Tag; + public uint NameLen; + public uint MaxNameLen; + [MarshalAs(UnmanagedType.ByValArray, SizeConst=1024)] + public byte[] Name; + } + + [DllImport("dbghelp.dll", SetLastError=true)] + public static extern bool SymInitialize(IntPtr hProcess, string UserSearchPath, bool fInvadeProcess); + [DllImport("dbghelp.dll", SetLastError=true)] + public static extern ulong SymLoadModuleEx(IntPtr hProcess, IntPtr hFile, string ImageName, string ModuleName, + ulong BaseOfDll, uint DllSize, IntPtr Data, uint Flags); + [DllImport("dbghelp.dll", SetLastError=true)] + public static extern bool SymFromAddr(IntPtr hProcess, ulong Address, out ulong Displacement, ref SYMBOL_INFO Symbol); + [DllImport("dbghelp.dll", SetLastError=true)] + public static extern bool SymCleanup(IntPtr hProcess); + [DllImport("dbghelp.dll")] + public static extern uint SymSetOptions(uint SymOptions); + + public const uint SYMOPT_UNDNAME = 0x00000002; + public const uint SYMOPT_LOAD_LINES = 0x00000010; + public const uint SYMOPT_DEBUG = 0x80000000; +} +'@ +Add-Type -TypeDefinition $cs -Language CSharp | Out-Null + +[Dbg]::SymSetOptions([Dbg]::SYMOPT_UNDNAME) | Out-Null +$proc = [System.Diagnostics.Process]::GetCurrentProcess() +$h = $proc.Handle +if (-not [Dbg]::SymInitialize($h, $SymbolPath, $false)) { + Write-Output ("SymInitialize failed: " + [ComponentModel.Win32Exception]::new([Runtime.InteropServices.Marshal]::GetLastWin32Error()).Message) + exit 1 +} + +$base = [Dbg]::SymLoadModuleEx($h, [IntPtr]::Zero, $mod, $null, 0, 0, [IntPtr]::Zero, 0) +if ($base -eq 0) { + Write-Output ("SymLoadModuleEx failed: " + [ComponentModel.Win32Exception]::new([Runtime.InteropServices.Marshal]::GetLastWin32Error()).Message) + [Dbg]::SymCleanup($h) | Out-Null + exit 1 +} +Write-Output ("module : " + $mod) +Write-Output ("base : 0x{0:x}" -f $base) +Write-Output ("sympath : " + $SymbolPath) +Write-Output "" + +foreach ($r in (($Rva -join ',').Split(',') | Where-Object { $_ -ne '' })) { + $off = [Convert]::ToUInt64($r, 16) + $addr = $base + $off + $sym = New-Object Dbg+SYMBOL_INFO + $sym.SizeOfStruct = 88 + $sym.MaxNameLen = 1024 + $sym.Name = New-Object byte[] 1024 + [uint64]$disp = 0 + if ([Dbg]::SymFromAddr($h, $addr, [ref]$disp, [ref]$sym)) { + $name = [Text.Encoding]::ASCII.GetString($sym.Name, 0, [int]$sym.NameLen) + Write-Output ("0x{0:x} {1}+0x{2:x} {3}" -f $off, $name, $disp, "") + } else { + Write-Output ("0x{0:x} (err {1})" -f $off, [Runtime.InteropServices.Marshal]::GetLastWin32Error()) + } +} + +[Dbg]::SymCleanup($h) | Out-Null