Files
envi-code/SourceCode/Code2026/CoreLibrary/SectionRegionCache.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

345 lines
9.0 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 "SectionRegionCache.h"
#include "AecDbObjXDataMap.h"
#include "SectionPerfCounter.h"
#include <windows.h>
#include <stdlib.h>
#include <string>
#include <map>
#include <list>
namespace
{
// 每个通道的条目上限。实测大图纸唯一 SAT 文本约 2600 条,2048 会把热条目挤掉,
// 所以默认给 4096;可用 AEC_SECTION_CACHE_MAX 覆盖(内存吃紧时调小)。
const size_t kDefaultMaxEntries = 4096;
size_t GetMaxEntries()
{
static size_t s_nMax = 0;
if (s_nMax == 0)
{
const char* psz = ::getenv("AEC_SECTION_CACHE_MAX");
s_nMax = (psz != NULL) ? (size_t)atoi(psz) : 0;
if (s_nMax < 16)
s_nMax = kDefaultMaxEntries;
}
return s_nMax;
}
static const char* kChannelName[] = { "text", "attr", "param" };
struct ChannelData
{
ChannelData() : nHits(0), nMisses(0), nEvicted(0) {}
std::map<unsigned long long, AcDbRegion*> map;
std::list<unsigned long long> fifo;
unsigned long long nHits;
unsigned long long nMisses;
unsigned long long nEvicted;
void ReleaseAll()
{
for (std::map<unsigned long long, AcDbRegion*>::iterator it = map.begin(); it != map.end(); ++it)
{
AcDbRegion* p = it->second;
if (p != NULL)
delete p;
}
map.clear();
fifo.clear();
nEvicted = 0;
}
};
struct State
{
State() : bInited(false), bEnabled(true) {}
bool bInited;
bool bEnabled;
ChannelData channels[SectionRegionCache::kChannelCount];
CRITICAL_SECTION cs;
};
// 函数内 static:CoreLibrary 是静态库且只被 CoreCommand 链接,
// 所以 CoreCommand.arx 内只有这一份缓存。
// 注意:不在析构里 delete 母体(DLL 卸载时 modeler 可能已先关,
// 析构顺序不安全);由 ClearAll() 在 kUnloadAppMsg 里显式释放。
State& GetState()
{
static State s_state;
if (!s_state.bInited)
{
::InitializeCriticalSection(&s_state.cs);
const char* psz = ::getenv("AEC_SECTION_CACHE");
s_state.bEnabled = !(psz != NULL && psz[0] == '0');
s_state.bInited = true;
}
return s_state;
}
struct AutoLock
{
explicit AutoLock(CRITICAL_SECTION* pcs) : m_pcs(pcs) { ::EnterCriticalSection(m_pcs); }
~AutoLock() { ::LeaveCriticalSection(m_pcs); }
CRITICAL_SECTION* m_pcs;
};
unsigned long long HashAdd(unsigned long long h, const void* pData, size_t nLen)
{
if (pData == NULL || nLen == 0)
return h;
const unsigned char* p = (const unsigned char*)pData;
for (size_t i = 0; i < nLen; ++i)
{
h ^= (unsigned long long)p[i];
h *= 1099511628211ULL;
}
return h;
}
}
namespace {
// 诊断用:AEC_SAT_SAMPLE=n 时把前 n 个"未命中的 SAT 文本"导出,便于比对差异来源
void DumpSample(const void* pData, size_t nLen)
{
static int s_nRemain = -1;
if (s_nRemain < 0)
{
const char* psz = ::getenv("AEC_SAT_SAMPLE");
s_nRemain = (psz != NULL) ? atoi(psz) : 0;
}
if (s_nRemain <= 0)
return;
--s_nRemain;
char szTmp[MAX_PATH + 1];
::memset(szTmp, 0, sizeof(szTmp));
if (::GetTempPathA(MAX_PATH, szTmp) == 0)
return;
std::string sPath(szTmp);
sPath += "aec_sat_samples.txt";
FILE* fp = NULL;
if (fopen_s(&fp, sPath.c_str(), "ab") == 0 && fp != NULL)
{
fprintf(fp, "===== SAMPLE len=%llu =====\n", (unsigned long long)nLen);
fwrite(pData, 1, nLen, fp);
fprintf(fp, "\n===== END =====\n");
fclose(fp);
}
}
}
namespace SectionRegionCache
{
bool Enabled()
{
return GetState().bEnabled;
}
unsigned long long HashBytes(const void* pData, size_t nLen)
{
return HashAdd(1469598103934665603ULL, pData, nLen);
}
unsigned long long HashSatText(const void* pData, size_t nLen)
{
if (pData == NULL || nLen == 0)
return 1469598103934665603ULL;
const char* p = (const char*)pData;
const char* pEnd = p + nLen;
// 找第 2 行(SAT 的 16 号记录:Autodesk 版本 + 存盘时间戳)
const char* pNL1 = (const char*)memchr(p, '\n', nLen);
if (pNL1 != NULL && pNL1 + 1 < pEnd)
{
const char* pLine2 = pNL1 + 1;
const char* pNL2 = (const char*)memchr(pLine2, '\n', (size_t)(pEnd - pLine2));
if (pNL2 != NULL)
{
// 只在第 2 行确实是 Autodesk 头时才跳过,避免误伤别的格式
bool bHeader = false;
for (const char* q = pLine2; q + 9 <= pNL2; ++q)
{
if (_strnicmp(q, "Autodesk", 8) == 0)
{
bHeader = true;
break;
}
}
if (bHeader)
{
unsigned long long h = HashBytes(p, (size_t)(pNL1 - p));
return HashAdd(h, pNL2 + 1, (size_t)(pEnd - (pNL2 + 1)));
}
}
}
return HashBytes(pData, nLen);
}
unsigned long long BuildSpecAttrHash(const AecDbObjXDataMap& map, const TCHAR* pszClass)
{
unsigned long long h = 1469598103934665603ULL;
// 类名参与:不同实体类即使数据相同也绝不能共用截面
CStringA sCls(pszClass != NULL ? pszClass : _T("?"));
h = HashAdd(h, (LPCSTR)sCls, strlen((LPCSTR)sCls));
// std::map 按 key 排序,遍历顺序稳定
for (AecDbObjXDataMap::It it = const_cast<AecDbObjXDataMap&>(map).Begin();
it != const_cast<AecDbObjXDataMap&>(map).End(); ++it)
{
const CString& sKey = it->first;
const AecXData& xd = it->second;
// 编号每根管子唯一,必须排除
if (sKey.CompareNoCase(_T("No.")) == 0)
continue;
h = HashAdd(h, sKey.GetString(), (size_t)sKey.GetLength() * sizeof(TCHAR));
h = HashAdd(h, &xd.nVt, sizeof(xd.nVt));
switch (xd.nVt)
{
case AecXData::kTypeShort:
h = HashAdd(h, &xd.iVal, sizeof(xd.iVal));
break;
case AecXData::kTypeInt:
case AecXData::kTypeInt64:
h = HashAdd(h, &xd.nVal, sizeof(xd.nVal));
break;
case AecXData::kTypeDouble:
h = HashAdd(h, &xd.dVal[0], sizeof(double));
break;
case AecXData::kTypeString:
if (xd.pszVal != NULL)
h = HashAdd(h, xd.pszVal, _tcslen(xd.pszVal) * sizeof(TCHAR));
break;
default:
// Id / 点 / 向量 / 矩阵 / 二进制:与截面无关或随实体变化,跳过
break;
}
}
return h;
}
AcDbRegion* Acquire(Channel ch, unsigned long long nKey, const void* pSampleData, size_t nSampleLen)
{
State& s = GetState();
if (!s.bEnabled || ch < 0 || ch >= kChannelCount)
return NULL;
AutoLock lock(&s.cs);
ChannelData& cd = s.channels[ch];
std::map<unsigned long long, AcDbRegion*>::iterator it = cd.map.find(nKey);
if (it == cd.map.end() || it->second == NULL)
{
++cd.nMisses;
if (ch == SectionRegionCache::kText && pSampleData != NULL)
DumpSample(pSampleData, nSampleLen);
return NULL;
}
++cd.nHits;
SectionPerf::Scope hitScope("cache.hit.clone");
return (AcDbRegion*)it->second->clone();
}
AcDbRegion* Put(Channel ch, unsigned long long nKey, AcDbRegion* pMaster)
{
State& s = GetState();
if (pMaster == NULL)
return NULL;
if (!s.bEnabled || ch < 0 || ch >= kChannelCount)
return pMaster; // 关闭时原样返回,调用方照旧拥有
AcDbRegion* pForCaller = NULL;
{
SectionPerf::Scope putScope("cache.miss.clone");
pForCaller = (AcDbRegion*)pMaster->clone();
}
AutoLock lock(&s.cs);
ChannelData& cd = s.channels[ch];
std::map<unsigned long long, AcDbRegion*>::iterator it = cd.map.find(nKey);
if (it != cd.map.end())
{
// 已有同 key(并发或重复入库):丢弃新的母体
delete pMaster;
return pForCaller;
}
// 超出上限先淘汰最旧的
while (cd.map.size() >= GetMaxEntries() && !cd.fifo.empty())
{
const unsigned long long nOld = cd.fifo.front();
cd.fifo.pop_front();
std::map<unsigned long long, AcDbRegion*>::iterator itOld = cd.map.find(nOld);
if (itOld != cd.map.end())
{
AcDbRegion* pOld = itOld->second;
cd.map.erase(itOld);
delete pOld;
++cd.nEvicted;
}
}
cd.map[nKey] = pMaster;
cd.fifo.push_back(nKey);
return pForCaller;
}
void Dump()
{
State& s = GetState();
std::string out;
char szLine[512];
out += "\n---- section region cache ----\n";
out += " channel entries hits misses hit_rate evicted\n";
for (int i = 0; i < (int)kChannelCount; ++i)
{
const ChannelData& cd = s.channels[i];
const unsigned long long nTotal = cd.nHits + cd.nMisses;
const double rate = nTotal ? (100.0 * (double)cd.nHits / (double)nTotal) : 0.0;
sprintf_s(szLine, sizeof(szLine), " %-9s %-9llu %-7llu %-7llu %7.1f%% %llu\n",
kChannelName[i], (unsigned long long)cd.map.size(),
cd.nHits, cd.nMisses, rate, cd.nEvicted);
out += szLine;
}
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;
}
// 同时写进与 SectionPerf 相同的报告文件,便于脚本回收
SectionPerf::AppendReport(out.c_str());
}
void ClearAll()
{
State& s = GetState();
AutoLock lock(&s.cs);
for (int i = 0; i < (int)kChannelCount; ++i)
s.channels[i].ReleaseAll();
}
}