perf: 增加属性 key 命中率统计(Phase 0 补充)

意图:内容 hash 只看到 54% 命中率,但业务上"同数据的管子/管件截面必然相同",
所以要用实体属性做 key 再量一次,决定 Phase 1 到底用哪种缓存键。

新增 SectionPerf::AttrKey(RAII):
- 构造时按类名(isA()->name())初始化 hash 与统计桶,析构时记一次
- Add() 增量喂字节,调用方决定哪些字段参与 key
- 统计按实体类分开(total / unique / hits / hit_rate)

接入 AecDbEwPipeBase::RegionAcisInDwgInFields:key = 类名 + 全部 spec XData,
但排除编号("No.",每根管子唯一)并跳过 Id / 点 / 向量 / 矩阵 / 二进制类型
(与截面无关或随实体变化)。只在 AEC_PERF_COUNTER=1 时构建,Release 无开销。

实测(big1.dwg,AutoCAD 2020 / R230 Debug,113 s 开图):

  属性 key 合计 3082 次 / 2672 命中 = 86.7%
    AecDbEwPipe       1088 次  173 唯一  84.1%
    AecDbEwPipeBase   1172 次    3 唯一  99.7%
       (其余类 43%~99%)
  内容 hash 54.0%

结论:属性 key 显著优于内容 hash,但只覆盖 3082/5641 次恢复
(另 2559 次走 CoreTools::StringToRegion,拿不到 spec 数据,只能用内容 hash)。
混合策略预计省 14.9 s / 22.6 s 恢复开销,约占 113 s 开图的 13%。

数据已写入 pipe-section-region-performance-plan.md,并标注一个未验证点:
属性 key 命中时两份 SAT 文本是否真的相同(决定 Phase 1 是否需要 textHash 兜底校验)。

验证:R230 Debug 编译通过并部署,用 tools/run_acad_perf.ps1 自动跑图取数。
This commit is contained in:
company-desktop committed 2026-10-28 20:06:06 +08:00
1 parent 6559cdf6f5
commit 764e544db4
3 files changed
+143

No files matched your search

@@ -23,6 +23,18 @@ namespace
// 万一文本各不相同(说明缓存没意义)也不让内存无限涨
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()
@@ -49,6 +61,9 @@ namespace
unsigned long long nTempFileBytes; // 临时文件累计字节
unsigned long long nWriteBacks; // Region -> SAT 文本 次数
// 属性 key:class 名 -> 唯一/命中统计
std::map<std::string, KeyStat> attrStats;
};
// 故意用函数内 static:AGENTS.md 禁止文件级 static。
@@ -199,6 +214,53 @@ namespace SectionPerf
(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();
@@ -225,6 +287,22 @@ namespace SectionPerf
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();
@@ -277,5 +355,6 @@ namespace SectionPerf
s.nTempFiles = 0;
s.nTempFileBytes = 0;
s.nWriteBacks = 0;
s.attrStats.clear();
}
}