Files
envi-code/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp
T
company-desktop 6d6f9ddaff diag: 统计回写的实体类,定位到 AecDbEwPipeBase 基类实例
上一版只抓到"绘制期深拷贝"的调用栈,还没回答"是哪个类的实体在回写"。
现在 SectionPerf 增加按类统计的写回计数(NoteWriteBackClass),
在 RegionAcisoutDwgOutFields 里记一次 isA()->name()。

实测(big1.dwg,开图一次):

  ---- write-backs by entity class ----
    1140     AecDbEwPipeBase

全部 1140 次都是 AecDbEwPipeBase 基类实例,不是 AecDbEwPipe 也不是具名子类,
与属性 key 统计里 AecDbEwPipeBase 的 1172 次几乎一一对应——
说明图纸里约有 1172 个基类实例(由 AecDbAssembleWell.cpp 的装配井代码创建,
多为矩形截面段),绘制期每个被深拷贝一次。

完整链路(已写入计划文档):
  base_ViewportDraw -> AecDbCurveBase::subGetTransformedCopy
  (AecRxBase/AecDbCurveBase.cpp:307 里的 clone())
  -> AecDbEntityCommon::base_DeepClone(AecDbEntityCommon.cpp:1232,dwgOut 在 1265)
  -> AecDbEwPipeBase::dwgOutFields(AecDbEwPipeBase.cpp:1440)
  -> AecDbEwPipeBase::RegionAcisoutDwgOutFields(AecDbEwPipeBase.cpp:1275)

顺带发现:AecDbEwPipeBase::copyFrom(:1605)复制截面用的是 m_pRegion->copyFrom(),
是内存级 ACIS 拷贝,比"写文件再读回"便宜得多。所以最局部的修法可能是给管系实体
加 subGetTransformedCopy 重载,用 isA()->create()+copyFrom() 代替 clone(),
绕开整条序列化链路;但 clone 与 copyFrom 语义不完全等价,需先核对。
本次只记录分析,不改行为。

验证:R230 Debug 编译通过并部署,脚本自动开图一次得到上述分类统计。
2026-10-28 21:06:22 +08:00

447 lines
11 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;
}
AppendReport(out.c_str());
}
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();
}
}