diag: 统计回写的实体类,定位到 AecDbEwPipeBase 基类实例 #103

Merged
gjm merged 1 commits from company-desktop/company-desktop-computer/diag-writeback-copyfrom AGit into main 2026-10-28 21:30:50 +08:00
7 changed files with 201 additions and 24 deletions

No files matched your search

@@ -1,6 +1,7 @@
#include "stdafx.h"
#include <sstream>
#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);
@@ -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);
}
@@ -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
@@ -1,6 +1,7 @@
#include "stdafx.h"
//#include "Resource.h"
#include "KTCadUtility.h"
#include "PerfTraceMark.h"
#include <dbidmap.h>
#include <acutil.h>
#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;
+58
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <set>
#include <string>
inline void PerfTraceMark(const char* pszTag)
{
#ifndef NDEBUG
static bool s_bInit = false;
static bool s_bOn = false;
static std::set<std::string> 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
+38 -24
View File
@@ -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:临时目录治理(可选,止血)
+96
View File
@@ -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} <no symbol> (err {1})" -f $off, [Runtime.InteropServices.Marshal]::GetLastWin32Error())
}
}
[Dbg]::SymCleanup($h) | Out-Null