Files
envi-code/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp
T
company-desktop f8d10d5a3b perf: 增加截面 Region 缓存,开图恢复耗时降 56%
一次开图要做 5641 次截面恢复(22.4 s),其中大量内容重复,
而每次恢复都是"写临时 SAT 文件 + 从盘 acisIn + 删文件",单次 2.7~5.5 ms,
还要过一次杀软。本提交加一级母体缓存,命中直接 clone 返回。

新增 CoreLibrary/SectionRegionCache.{h,cpp},三个通道按 key 的正确性强度分开:
- kParam:纯参数 hash,用于 AecDbEwFitting::GetSection 的两个重载
  (key = dD/dt/HLR 标志 或 W/H/dt/HLR 标志,同参数必同几何)
- kAttr:类名 + spec XData(排除编号,并跳过 Id/点/向量/矩阵/二进制类型),
  只给 AecDbEwPipe 用。AecDbEwPipeBase 是很多类的基类,其余派生类实例间的
  区别就是截面,不满足"同数据必同截面",所以一律走内容 hash
- kText:SAT 文本内容 hash,用于 RegionAcisInDwgInFields 的其它类
  与 CoreTools::StringToRegion 的两个重载

约定:母体由缓存持有、永不外泄,Acquire/Put 都返回 clone,调用方拥有返回值;
母体不入库;每条通道 4096 条上限(AEC_SECTION_CACHE_MAX 可调),超出按 FIFO 淘汰;
AEC_SECTION_CACHE=0 可整体关闭做 A/B;卸载时 ClearAll() 释放,避免 DLL 析构顺序问题。

关键发现:SAT 文本第 2 行带存盘时间戳
  16 Autodesk AutoCAD 19 ASM 219.0.0.5600 NT 24 Thu Oct 22 10:19:20 2026
同一几何存盘时间不同,整份文本就不同(采样里两份 2596 字节的文本只差
10:19:20 与 10:19:16),这正是内容 hash 命中率只有 54% 的原因,与截面无关。
因此新增 HashSatText():算 hash 时跳过第 2 行,仅当该行含 "Autodesk" 时才跳过。
计数器也改用同一口径,避免两套统计互相矛盾。

实测(big1.dwg / AutoCAD 2020 / R230 Debug,AEC_SECTION_CACHE 关 vs 开):
  开图墙钟        108~113 s      -> 98 s
  截面恢复总耗时  22.4 s         -> 9.87 s(-56%)
    StringToRegion  14.05 s      -> 7.73 s
    RegionAcisIn     8.38 s      -> 2.14 s
  临时文件        6773 次/9.34MB -> 2875 次/3.61MB
  命中率          text 65.5% / attr 84.1% / param 88.8%
  clone 开销      命中 58us、未命中 73us(合计 0.37 s,可忽略)

顺带:
- tools/run_acad_perf.ps1 增加 -CacheOff 开关,用于同一构建上的 A/B 对比
- SectionPerfCounter 增加 AppendReport(),让缓存统计也写进同一份报告文件
- 诊断口子 AEC_SAT_SAMPLE=n 可导出未命中的 SAT 原文,用于定位这类差异

验证:R230 Debug 编译通过并部署;用脚本自动开图各跑一次(缓存关/开),
数据已写入 pipe-section-region-performance-plan.md。
尚未做:R190 编译(本机缺 v100 工具集)、Release 构建的收益复测、
开图期间 1140 次 RegionAcisout 回写(与本缓存无关,需单独立项)。
2026-10-28 20:41:05 +08:00

376 lines
9.2 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;
};
// 故意用函数内 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 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.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 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();
}
}