Files
envi-code/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp
T
company-desktop b981fe930a perf: 护栏/爬梯等构件的管道副本改为"只存参数",消除开图期 1140 次截面回写
现象:开图期间 AecDbEwPipeBase::RegionAcisoutDwgOutFields 被调用 1140 次、耗时 2.38 s,
把截面写成临时 SAT 文件。用 AEC_WRITEBACK_TRACE 抓栈 + tools/resolve_rva.ps1 解符号,
定位到 AecDbZLineStair::DrawGuarBar3d 里的 pipe.clone():
clone 走 AcDbObject → AecDbEwPipeBase::copyFrom → CopyObject(整实体 dwgOutFields/dwgInFields
往返),于是截面被写成临时文件再读回。

这些 clone 的唯一用途是存进 m_pipes,供 ToJson() 导出 __pipes__,而截面参数在
SetSection 时已经写进 spec XData(KEY_SECSHAPE / KEY_SIZE1 / KEY_SIZE2 / PIPE_THICK /
KEY_SECCEN),完全可以按参数重建。因此把"建几何"推迟到导出时:

- AecDbEwPipeBase::BuildSectionFromXData():按 XData 参数重建截面(圆形/矩形;
  自定义截面返回 NULL,那种只能存在于 SAT 里)
- AecDbEwPipeBase::CloneParamsOnly():生成只带参数(XData、放样线、实体属性、GUID、
  Breaks)、不含截面几何的副本,不触发任何序列化
- AecDbEwPipeBase::ToJson():GetSection() 为空时回退到 BuildSectionFromXData(),
  保证 __section__ / __section2__ 仍然导出
- 30 处 m_pipes.push_back((AecDbEwPipe*)pipe.clone()) 改为 pipe.CloneParamsOnly():
  ClimbStair(10)、ZLineStair(6)、BossHolder(4)、MRPLineStair(4)、Hanger(3)、Holder(3)

过程:先只改楼梯两个文件,回写从 1140 降到 960;再抓栈解析出剩余 960 全部来自
AecDbClimbStair::DrawLadderH3d,于是把其余 20 处一并改掉,最终归零。

实测(big1.dwg / AutoCAD 2020 / R230 Debug):
  开图墙钟        初始 108~113 s  -> 缓存后 98 s  -> 本次 93 s
  开图期回写      1140 次/2.38 s  -> 1140 次       -> 0
  RegionAcisIn    3082 次         -> 3082 次       -> 1942 次
  临时文件        6773/9.34MB     -> 2694/3.34MB   -> 1733/2.34MB
  截面恢复总耗时  22.4 s          -> 9.87 s        -> 9.4 s

另外把 SectionPerf::TraceCallStack 的输出改写到独立的 aec_writeback_trace.txt,
不再写进 ENVIPERFDUMP 的报告文件(否则脚本会把"栈写出来了"误判成"开图完成")。

验证:R230 Debug 编译通过并部署;用 tools/run_acad_perf.ps1 自动开图取数。
待验证:JSON 导出。m_pipes 里现在是不含几何的参数副本,ToJson 靠
BuildSectionFromXData() 重建,需要实际触发一次导出,确认 __pipes__ 里每条仍有
__section__ / __section2__ 且与改动前一致。
2026-10-28 21:57:31 +08:00

462 lines
12 KiB
C++
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.
#include "stdafx.h"
#include "SectionPerfCounter.h"
#include "SectionRegionCache.h"
#include <windows.h>
#include <stdlib.h>
#include <string>
#include <map>
#include <set>
namespace
{
// 单个计数器:次数 + 累计耗时(µs)
struct Entry
{
Entry() : nCount(0), nTotalUs(0), nMaxUs(0) {}
unsigned long long nCount;
unsigned long long nTotalUs;
unsigned long long nMaxUs;
};
// 唯一 hash 表的上限:正常图纸几十到几百个唯一截面,
// 万一文本各不相同(说明缓存没意义)也不让内存无限涨
const size_t kMaxUniqueHash = 50000;
// 单个属性 key 的统计(按实体类分开看,管道与管件的命中率往往不一样)
struct KeyStat
{
KeyStat() : nTotal(0), nUnique(0), nHits(0), bOverflow(false) {}
unsigned long long nTotal;
size_t nUnique;
unsigned long long nHits;
bool bOverflow;
std::set<unsigned long long> seen;
};
struct Stats
{
Stats()
: bInited(false), bEnabled(false)
, nRegionTexts(0), nUniqueHashes(0), nSimulatedHits(0), bHashTableOverflow(false)
, nTempFiles(0), nTempFileBytes(0)
, nWriteBacks(0)
{
}
bool bInited;
bool bEnabled;
std::map<std::string, Entry> entries; // tag -> 次数/耗时
unsigned long long nRegionTexts; // 一共恢复过多少份 SAT 文本
size_t nUniqueHashes; // 其中不同内容有多少
unsigned long long nSimulatedHits; // 若做内容缓存,能命中多少次
bool bHashTableOverflow;
std::set<unsigned long long> hashSeen;
unsigned long long nTempFiles; // 临时文件写入次数
unsigned long long nTempFileBytes; // 临时文件累计字节
unsigned long long nWriteBacks; // Region -> SAT 文本 次数
// 属性 key:class 名 -> 唯一/命中统计
std::map<std::string, KeyStat> attrStats;
// 回写次数:实体类 -> 次数
std::map<std::string, unsigned long long> writeBackByClass;
};
// 故意用函数内 static:AGENTS.md 禁止文件级 static。
// CoreLibrary 是静态库且只被 CoreCommand 链接,
// 所以这里的实例在 CoreCommand.arx 里是唯一的一份。
Stats& GetStats()
{
static Stats s_stats;
if (!s_stats.bInited)
{
s_stats.bInited = true;
#ifndef NDEBUG
const char* psz = ::getenv("AEC_PERF_COUNTER");
s_stats.bEnabled = (psz != NULL && psz[0] != '\0' && psz[0] != '0');
#else
s_stats.bEnabled = false;
#endif
}
return s_stats;
}
long long NowTicks()
{
LARGE_INTEGER li;
::QueryPerformanceCounter(&li);
return (long long)li.QuadPart;
}
unsigned long long TicksToUs(long long nTicks)
{
static long long nFreq = 0;
if (nFreq == 0)
{
LARGE_INTEGER li;
if (!::QueryPerformanceFrequency(&li) || li.QuadPart == 0)
nFreq = 1;
else
nFreq = (long long)li.QuadPart;
}
if (nTicks <= 0)
return 0;
return (unsigned long long)((nTicks * 1000000LL) / nFreq);
}
// FNV-1a 64
unsigned long long Hash64(const char* pszTxt, size_t nLen)
{
unsigned long long h = 1469598103934665603ULL;
for (size_t i = 0; i < nLen; ++i)
{
h ^= (unsigned char)pszTxt[i];
h *= 1099511628211ULL;
}
return h;
}
Entry& GetEntry(const char* pszTag)
{
Stats& s = GetStats();
std::string key(pszTag ? pszTag : "?");
return s.entries[key];
}
std::string TempReportPath()
{
char szTmp[MAX_PATH + 1];
::memset(szTmp, 0, sizeof(szTmp));
DWORD n = ::GetTempPathA(MAX_PATH, szTmp);
if (n == 0 || n >= MAX_PATH)
return std::string("aec_section_perf.txt");
std::string s(szTmp);
s += "aec_section_perf.txt";
return s;
}
}
namespace SectionPerf
{
bool Enabled()
{
return GetStats().bEnabled;
}
Scope::Scope(const char* pszTag)
: m_pszTag(pszTag), m_nStart(0), m_bActive(false)
{
if (!GetStats().bEnabled)
return;
m_bActive = true;
m_nStart = NowTicks();
}
Scope::~Scope()
{
if (!m_bActive)
return;
const unsigned long long nUs = TicksToUs(NowTicks() - m_nStart);
Entry& e = GetEntry(m_pszTag);
++e.nCount;
e.nTotalUs += nUs;
if (nUs > e.nMaxUs)
e.nMaxUs = nUs;
}
void NoteRegionText(const char* pszTxt, size_t nLen)
{
Stats& s = GetStats();
if (!s.bEnabled)
return;
if (pszTxt == NULL || nLen == 0)
return;
++s.nRegionTexts;
const unsigned long long h = SectionRegionCache::HashSatText(pszTxt, nLen);
if (s.hashSeen.find(h) != s.hashSeen.end())
{
++s.nSimulatedHits;
return;
}
if (s.hashSeen.size() < kMaxUniqueHash)
{
s.hashSeen.insert(h);
++s.nUniqueHashes;
}
else
{
s.bHashTableOverflow = true;
}
}
void NoteTempFile(size_t nBytes)
{
Stats& s = GetStats();
if (!s.bEnabled)
return;
++s.nTempFiles;
s.nTempFileBytes += (unsigned long long)nBytes;
}
void NoteWriteBack(const char* pszTag)
{
Stats& s = GetStats();
if (!s.bEnabled)
return;
++s.nWriteBacks;
(void)pszTag;
}
AttrKey::AttrKey(const TCHAR* pszClass)
: m_nHash(1469598103934665603ULL), m_bActive(GetStats().bEnabled)
{
if (!m_bActive)
return;
// CStringA 按当前代码页做宽窄转换;非 UNICODE 构建下直接构造
CStringA sCls(pszClass != NULL ? pszClass : _T("?"));
m_sClass = (LPCSTR)sCls;
Add(m_sClass.data(), m_sClass.size());
}
AttrKey::~AttrKey()
{
if (!m_bActive)
return;
// 与 NoteRegionText 相同口径:首次出现记唯一,重复出现记一次“命中”
KeyStat& st = GetStats().attrStats[m_sClass];
++st.nTotal;
if (st.seen.find(m_nHash) != st.seen.end())
{
++st.nHits;
return;
}
if (st.seen.size() < kMaxUniqueHash)
{
st.seen.insert(m_nHash);
++st.nUnique;
}
else
{
st.bOverflow = true;
}
}
void AttrKey::Add(const void* pData, size_t nLen)
{
if (!m_bActive || pData == NULL || nLen == 0)
return;
const unsigned char* p = (const unsigned char*)pData;
for (size_t i = 0; i < nLen; ++i)
{
m_nHash ^= (unsigned long long)p[i];
m_nHash *= 1099511628211ULL;
}
}
void NoteWriteBackClass(const char* pszClass)
{
Stats& s = GetStats();
if (!s.bEnabled)
return;
std::string key(pszClass != NULL ? pszClass : "?");
++s.writeBackByClass[key];
}
void Dump()
{
Stats& s = GetStats();
if (!s.bEnabled)
{
acutPrintf(_T("\n[SectionPerf] not enabled: set AEC_PERF_COUNTER=1 before starting CAD (Debug build only)."));
return;
}
// 报告正文全 ASCII:控制台用 %hs 直出,落文件也直接可读
std::string out;
char szLine[1024];
out += "\n================ SectionPerf (Phase 0) ================\n";
sprintf_s(szLine, sizeof(szLine), "RegionTexts=%llu UniqueHashes=%llu SimulatedHits=%llu HitRate=%.1f%%\n",
s.nRegionTexts, (unsigned long long)s.nUniqueHashes, s.nSimulatedHits,
s.nRegionTexts ? (100.0 * (double)s.nSimulatedHits / (double)s.nRegionTexts) : 0.0);
out += szLine;
if (s.bHashTableOverflow)
out += " (hash table capped: UniqueHashes is under-counted, HitRate over-estimated)\n";
sprintf_s(szLine, sizeof(szLine), "TempFiles=%llu TempBytes=%llu WriteBacks=%llu\n",
s.nTempFiles, s.nTempFileBytes, s.nWriteBacks);
out += szLine;
if (!s.writeBackByClass.empty())
{
out += "---- write-backs by entity class ----\n";
out += " count class\n";
for (std::map<std::string, unsigned long long>::const_iterator it = s.writeBackByClass.begin();
it != s.writeBackByClass.end(); ++it)
{
sprintf_s(szLine, sizeof(szLine), " %-8llu %s\n", it->second, it->first.c_str());
out += szLine;
}
}
if (!s.attrStats.empty())
{
out += "---- attr key (by class, would-be cache hit rate) ----\n";
out += " total unique hits hit_rate class\n";
for (std::map<std::string, KeyStat>::const_iterator it = s.attrStats.begin();
it != s.attrStats.end(); ++it)
{
const KeyStat& k = it->second;
const double rate = k.nTotal ? (100.0 * (double)k.nHits / (double)k.nTotal) : 0.0;
sprintf_s(szLine, sizeof(szLine), " %-8llu %-9llu %-6llu %7.1f%% %s%s\n",
k.nTotal, (unsigned long long)k.nUnique, k.nHits, rate,
it->first.c_str(), k.bOverflow ? " (capped)" : "");
out += szLine;
}
}
out += "---- timing (by tag) ----\n";
out += " count total_ms avg_us max_us tag\n";
for (std::map<std::string, Entry>::const_iterator it = s.entries.begin();
it != s.entries.end(); ++it)
{
const Entry& e = it->second;
const double avgUs = e.nCount ? ((double)e.nTotalUs / (double)e.nCount) : 0.0;
sprintf_s(szLine, sizeof(szLine), " %-9llu %10.1f %10.1f %10llu %s\n",
e.nCount, (double)e.nTotalUs / 1000.0, avgUs, e.nMaxUs, it->first.c_str());
out += szLine;
}
out += "=======================================================\n";
// 逐行输出:%hs 把窄字符串塞进 TCHAR 格式串,不涉及宽字符转换
const char* psz = out.c_str();
while (*psz != '\0')
{
const char* pszEnd = strchr(psz, '\n');
const size_t nSeg = (pszEnd != NULL) ? (size_t)(pszEnd - psz) : strlen(psz);
if (nSeg > 0 && nSeg < sizeof(szLine))
{
memcpy(szLine, psz, nSeg);
szLine[nSeg] = '\0';
acutPrintf(_T("\n%hs"), szLine);
}
if (pszEnd == NULL)
break;
psz = pszEnd + 1;
}
// 同时落文件,方便事后拿去做分析
const std::string sPath = TempReportPath();
FILE* fp = NULL;
if (fopen_s(&fp, sPath.c_str(), "a") == 0 && fp != NULL)
{
fprintf(fp, "%s", out.c_str());
fclose(fp);
}
}
void TraceCallStack(const char* pszTag, int nSkip)
{
typedef USHORT(WINAPI* PFN_CaptureStack)(ULONG, ULONG, PVOID*, PULONG);
static PFN_CaptureStack s_pfn = NULL;
static bool s_bTried = false;
if (!s_bTried)
{
s_bTried = true;
HMODULE hNtdll = ::GetModuleHandleA("ntdll.dll");
if (hNtdll != NULL)
s_pfn = (PFN_CaptureStack)::GetProcAddress(hNtdll, "RtlCaptureStackBackTrace");
}
if (s_pfn == NULL)
return;
void* frames[24];
::memset(frames, 0, sizeof(frames));
const USHORT nGot = s_pfn((ULONG)(nSkip > 0 ? nSkip : 0), 24, frames, NULL);
std::string out = "\n[stack] ";
out += (pszTag != NULL ? pszTag : "?");
out += "\n";
char szLine[1024];
for (USHORT i = 0; i < nGot; ++i)
{
const DWORD64 nAddr = (DWORD64)frames[i];
HMODULE hMod = NULL;
if (!::GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCWSTR)nAddr, &hMod) || hMod == NULL)
continue;
WCHAR szPath[MAX_PATH];
::memset(szPath, 0, sizeof(szPath));
::GetModuleFileNameW(hMod, szPath, MAX_PATH);
const WCHAR* pszBase = wcsrchr(szPath, L'\\');
pszBase = (pszBase != NULL) ? pszBase + 1 : szPath;
char szMod[MAX_PATH * 2];
::memset(szMod, 0, sizeof(szMod));
::WideCharToMultiByte(CP_ACP, 0, pszBase, -1, szMod, sizeof(szMod) - 1, NULL, NULL);
sprintf_s(szLine, sizeof(szLine), " %-28s +0x%llx\n", szMod, (unsigned long long)(nAddr - (DWORD64)hMod));
out += szLine;
}
// 写到独立文件:不打扰 ENVIPERFDUMP 的报告文件(脚本靠它判断就绪)
{
char szTmp[MAX_PATH + 1];
::memset(szTmp, 0, sizeof(szTmp));
if (::GetTempPathA(MAX_PATH, szTmp) != 0)
{
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, "%s", out.c_str());
fclose(fp);
}
}
}
}
void AppendReport(const char* pszText)
{
if (pszText == NULL)
return;
const std::string sPath = TempReportPath();
FILE* fp = NULL;
if (fopen_s(&fp, sPath.c_str(), "a") == 0 && fp != NULL)
{
fprintf(fp, "%s", pszText);
if (pszText[0] != 0 && pszText[strlen(pszText)-1] != '\n')
fprintf(fp, "%s", "\n");
fclose(fp);
}
}
void Reset()
{
Stats& s = GetStats();
s.entries.clear();
s.hashSeen.clear();
s.nRegionTexts = 0;
s.nUniqueHashes = 0;
s.nSimulatedHits = 0;
s.bHashTableOverflow = false;
s.nTempFiles = 0;
s.nTempFileBytes = 0;
s.nWriteBacks = 0;
s.attrStats.clear();
s.writeBackByClass.clear();
}
}