perf: 增加截面 Region 缓存,开图恢复耗时降 56% #100

Merged
gjm merged 1 commits from company-desktop/company-desktop-computer/perf-section-region-cache AGit into main 2026-10-28 20:45:59 +08:00
11 changed files with 594 additions and 2 deletions

No files matched your search

@@ -21,6 +21,7 @@
#include "gpc.h"
#include "ImpMD5.h"
#include "SectionPerfCounter.h"
#include "SectionRegionCache.h"
#include "breptools.h"
#include "DimScaleZoneDlg.h"
#include "simpleProperty.h"
@@ -3523,6 +3524,11 @@ bool CoreTools::CurveToString(const AcDbCurve * pCurve, CString & sScr2)
AcDbRegion* CoreTools::StringToRegion(const CString & sRegionScrp)
{
// 截面缓存:SAT 文本内容 hash(字节相同即几何相同,对所有实体类都成立)
SectionRegionCache::Channel chCache = SectionRegionCache::kText;
unsigned long long nCacheKey = 0;
const bool bCacheOn = SectionRegionCache::Enabled();
// Phase 0 计数:截面恢复耗时/次数/文本唯一性,见 pipe-section-region-performance-plan.md
SectionPerf::Scope perfScope("CoreTools::StringToRegion");
@@ -3546,6 +3552,14 @@ AcDbRegion* CoreTools::StringToRegion(const CString & sRegionScrp)
CString sFullPath = MakeTempSatPath();
if (sFullPath.IsEmpty()) return NULL;
if (bCacheOn)
{
nCacheKey = SectionRegionCache::HashSatText(sNewSrc.c_str(), sNewSrc.size());
AcDbRegion* pHit = SectionRegionCache::Acquire(chCache, nCacheKey, sNewSrc.c_str(), sNewSrc.size());
if (pHit != NULL)
return pHit;
}
SectionPerf::NoteRegionText(sNewSrc.c_str(), sNewSrc.size());
if (file.Open(sFullPath, CFile::modeCreate | CFile::modeWrite))
@@ -3586,11 +3600,20 @@ AcDbRegion* CoreTools::StringToRegion(const CString & sRegionScrp)
DELETE_PTR(arBodyPtrs[i]);
::DeleteFile(sFullPath);
if (bCacheOn)
return SectionRegionCache::Put(chCache, nCacheKey, pReg);
return pReg;
}
AcDbRegion *CoreTools::StringToRegion(Adesk::UInt64 nLen, char *pszReg)
{
// 截面缓存(同 CString 重载)
SectionRegionCache::Channel chCache = SectionRegionCache::kText;
unsigned long long nCacheKey = 0;
const bool bCacheOn = SectionRegionCache::Enabled();
SectionPerf::Scope perfScope("CoreTools::StringToRegion(buf)");
std::string sNewSrc;
@@ -3610,6 +3633,14 @@ AcDbRegion *CoreTools::StringToRegion(Adesk::UInt64 nLen, char *pszReg)
CString sFullPath = MakeTempSatPath();
if (sFullPath.IsEmpty()) return NULL;
if (bCacheOn)
{
nCacheKey = SectionRegionCache::HashSatText(sNewSrc.c_str(), sNewSrc.size());
AcDbRegion* pHit = SectionRegionCache::Acquire(chCache, nCacheKey, sNewSrc.c_str(), sNewSrc.size());
if (pHit != NULL)
return pHit;
}
SectionPerf::NoteRegionText(sNewSrc.c_str(), sNewSrc.size());
CFile file;
@@ -3650,6 +3681,10 @@ AcDbRegion *CoreTools::StringToRegion(Adesk::UInt64 nLen, char *pszReg)
DELETE_PTR(arBodyPtrs[i]);
::DeleteFile(sFullPath);
if (bCacheOn)
return SectionRegionCache::Put(chCache, nCacheKey, pReg);
return pReg;
}
@@ -20,6 +20,7 @@
#include "AecDbDimScaleZone.h"
#include "AecDbJoggedCamera.h"
#include "SectionPerfCounter.h"
#include "SectionRegionCache.h"
#include "breptools.h"
#include "HlrCutOption.h"
@@ -474,6 +475,7 @@ acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt)
// && g_lockState == BIT_SUCCESS)
// 验证是否成功,都应该自行注销
UnloadApplication();
SectionRegionCache::ClearAll();
//else
// return AcRx::kRetError;
#if ARX > 18
@@ -549,6 +551,7 @@ KTDocReactor *g_pDocReactor(NULL);
void envierpf_dump()
{
SectionPerf::Dump();
SectionRegionCache::Dump();
}
void envierpf_reset()
@@ -1,5 +1,7 @@
#include "stdafx.h"
#include "AecDbEwFitting.h"
#include "SectionPerfCounter.h"
#include "SectionRegionCache.h"
#include "dbgrip.h"
#include "AecGripCback.h"
#include "MFCDbDwgFiler.h"
@@ -11986,6 +11988,24 @@ void AecDbEwFitting::DrawInfCap3d(AcGiDraw &dc) const
//设置截面为管道外径和壁厚
AcDbRegion* AecDbEwFitting::GetSection(double dD,double dt) const
{
// 截面缓存:GetSection 是 (dD, dt, HLR标志) 的纯函数,同参数必同几何,
// 且它在绘制期被反复调用(createFromCurves 单次约 3.6 ms)。
SectionPerf::Scope perfScope("Fitting::GetSection(D,dt)");
SectionRegionCache::Channel chCache = SectionRegionCache::kParam;
unsigned long long nCacheKey = 0;
const bool bCacheOn = SectionRegionCache::Enabled();
if (bCacheOn)
{
const bool bHLR = isHLRCalc(this);
nCacheKey = SectionRegionCache::HashBytes(&dD, sizeof(dD));
nCacheKey = SectionRegionCache::HashBytes(&dt, sizeof(dt)) ^ (nCacheKey << 1);
nCacheKey ^= bHLR ? 0x9E3779B97F4A7C15ULL : 0ULL;
AcDbRegion* pHit = SectionRegionCache::Acquire(chCache, nCacheKey);
if (pHit != NULL)
return pHit;
}
assertReadEnabled();
AcDbVoidPtrArray arCurPtrs,arRegnPtrs;
@@ -12025,11 +12045,33 @@ AcDbRegion* AecDbEwFitting::GetSection(double dD,double dt) const
}
FreeBuffer(0,arCurPtrs);
}
if (bCacheOn)
return SectionRegionCache::Put(chCache, nCacheKey, pRegion);
return pRegion;
}
AcDbRegion * AecDbEwFitting::GetSection(double W, double H, double dt) const
{
// 截面缓存:矩形截面同理,key = (W, H, dt, HLR标志)
SectionPerf::Scope perfScope("Fitting::GetSection(W,H,dt)");
SectionRegionCache::Channel chCache = SectionRegionCache::kParam;
unsigned long long nCacheKey = 0;
const bool bCacheOn = SectionRegionCache::Enabled();
if (bCacheOn)
{
const bool bHLR = isHLRCalc(this);
nCacheKey = SectionRegionCache::HashBytes(&W, sizeof(W));
nCacheKey = SectionRegionCache::HashBytes(&H, sizeof(H)) ^ (nCacheKey << 1);
nCacheKey = SectionRegionCache::HashBytes(&dt, sizeof(dt)) ^ (nCacheKey << 1);
nCacheKey ^= bHLR ? 0x9E3779B97F4A7C15ULL : 0ULL;
AcDbRegion* pHit = SectionRegionCache::Acquire(chCache, nCacheKey);
if (pHit != NULL)
return pHit;
}
double x(W * .5), y(H * .5);
AcDbVoidPtrArray arCurPtrs, arRegnPtrs;
@@ -12069,6 +12111,9 @@ AcDbRegion * AecDbEwFitting::GetSection(double W, double H, double dt) const
}
FreeBuffer(0,arCurPtrs);
if (bCacheOn)
return SectionRegionCache::Put(chCache, nCacheKey, pRegion);
return pRegion;
}
@@ -1,6 +1,7 @@
#include "stdafx.h"
#include "encryption.h"
#include "AecDbEwPipeBase.h"
#include "AecDbEwPipe.h"
#include "KTCADUtilityEx.h"
#include "MFCDbDwgFiler.h"
#include "AecDb3dRoadAxisLine.h"
@@ -19,6 +20,7 @@
#include "AcGiViewDraw.h"
#include "AcDbX3dSolid.h"
#include "SectionPerfCounter.h"
#include "SectionRegionCache.h"
#ifndef EW_EARTHWORMINIT
#define EW_EARTHWORMINIT _T("EW_EARTHWORMINIT")
@@ -823,6 +825,26 @@ AcDbRegion* AecDbEwPipeBase::RegionAcisInDwgInFields(AcDbDwgFiler* pFiler)
// 同一份 SAT 文本被恢复多少次:唯一 hash 数 / 模拟命中率
SectionPerf::NoteRegionText(pszTxt, (size_t)nLen);
// 截面缓存:管道用属性 key(同 spec 数据必同截面),其余类用 SAT 文本内容 hash。
// 命中直接返回 clone,跳过"写临时文件 + acisIn + 删文件"整段。
SectionRegionCache::Channel cacheChannel = SectionRegionCache::kText;
unsigned long long nCacheKey = 0;
bool bUseAttrKey = false;
if (SectionRegionCache::Enabled())
{
bUseAttrKey = (isA() == AecDbEwPipe::desc());
cacheChannel = bUseAttrKey ? SectionRegionCache::kAttr : SectionRegionCache::kText;
nCacheKey = bUseAttrKey
? SectionRegionCache::BuildSpecAttrHash(m_mapSpecXData, isA()->name())
: SectionRegionCache::HashSatText(pszTxt, (size_t)nLen);
AcDbRegion* pHit = SectionRegionCache::Acquire(cacheChannel, nCacheKey, pszTxt, (size_t)nLen);
if (pHit != NULL)
{
delete[] pszTxt;
return pHit;
}
}
// Phase 0:属性 key 命中率(只统计,不做缓存)。
// 管道/管件没有自定义截面,同一套 spec 数据必然得到同一截面,
// 所以这里把 "类名 + 除编号外的全部 spec XData" 当作 key。
@@ -933,6 +955,10 @@ AcDbRegion* AecDbEwPipeBase::RegionAcisInDwgInFields(AcDbDwgFiler* pFiler)
acutPrintf(_T("\nAecDbEwPipeBase: 截面 acisIn 未返回 region,len=%u。"),nLen);
::DeleteFile(sFullPath);
if (SectionRegionCache::Enabled())
return SectionRegionCache::Put(cacheChannel, nCacheKey, pRegion);
return pRegion;
}
@@ -1,5 +1,6 @@
#include "stdafx.h"
#include "SectionPerfCounter.h"
#include "SectionRegionCache.h"
#include <windows.h>
#include <stdlib.h>
@@ -179,7 +180,7 @@ namespace SectionPerf
++s.nRegionTexts;
const unsigned long long h = Hash64(pszTxt, nLen);
const unsigned long long h = SectionRegionCache::HashSatText(pszTxt, nLen);
if (s.hashSeen.find(h) != s.hashSeen.end())
{
++s.nSimulatedHits;
@@ -343,6 +344,20 @@ namespace SectionPerf
}
}
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();
@@ -0,0 +1,344 @@
#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();
}
}
@@ -12,6 +12,7 @@ set(SOURCES
"AecComplexCurveDraw.cpp"
"AecCoverBooleanOper.cpp"
"SectionPerfCounter.cpp"
"SectionRegionCache.cpp"
"AecDb3dRoad.cpp"
"AecDb3dRoadAxisLine.cpp"
"AecDb3dRoadFitting.cpp"
@@ -76,6 +76,9 @@ namespace SectionPerf
// 打印 + 写文件
void Dump();
// 只追加文本到 %TEMP%ec_section_perf.txt(供其它模块复用同一份报告)
void AppendReport(const char* pszText);
// 清零
void Reset();
}
@@ -0,0 +1,65 @@
// SectionRegionCache.h —— 截面(Region)母体缓存
//
// 背景(见 pipe-section-region-performance-plan.md):
// 一次开图实测要做 5641 次截面恢复(22.9 s),而其中大量内容重复。
// 恢复一次 = 写临时 SAT 文件 + 从盘 acisIn + 删文件 + 杀软扫描,单次 2.7~5.5 ms。
//
// 三个通道,按"key 的正确性强度"分开用:
// kParam —— 纯参数函数(如 AecDbEwFitting::GetSection(dD, dt)),同参数必同几何,最强
// kAttr —— 实体属性 key(仅用于"同数据必然同截面"的类,目前只有 AecDbEwPipe)
// kText —— SAT 文本内容 hash,对所有类都成立(字节相同即几何相同)
//
// 约定:
// - 缓存持有"母体",永不外泄;Acquire/Put 都返回 clone,调用方拥有返回值。
// - 母体不入库、不参与绘制,只在缓存里待着。
// - 每次访问都 clone(一次区域深拷贝,远低于一次文件往返)。
// - 环境变量 AEC_SECTION_CACHE=0 可整体关闭(用于 A/B 对比)。
#ifndef SECTION_REGION_CACHE_H
#define SECTION_REGION_CACHE_H
#include <stddef.h>
class AcDbRegion;
class AecDbObjXDataMap;
namespace SectionRegionCache
{
enum Channel
{
kText = 0, // SAT 文本内容 hash
kAttr = 1, // 实体属性 hash(仅管道)
kParam = 2, // 纯参数 hash(管件 GetSection)
kChannelCount = 3
};
// 默认开启;AEC_SECTION_CACHE=0 关闭
bool Enabled();
// FNV-1a 64 位
unsigned long long HashBytes(const void* pData, size_t nLen);
// SAT 文本哈希:跳过 ACIS 头里的时间戳行。
// 实测同一几何存盘时间不同会让整份文本不同,直接用内容 hash 会几乎全部判成"唯一"。
unsigned long long HashSatText(const void* pData, size_t nLen);
// 属性 key:类名 + 全部 spec XData,排除编号("No."),
// 并跳过 Id / 点 / 向量 / 矩阵 / 二进制类型(与截面无关或随实体变化)。
unsigned long long BuildSpecAttrHash(const AecDbObjXDataMap& map, const TCHAR* pszClass);
// 命中返回 clone;未命中返回 NULL
// pSampleData/nSampleLen 仅在诊断(AEC_SAT_SAMPLE)时用于导出未命中的原文,可传 NULL
AcDbRegion* Acquire(Channel ch, unsigned long long nKey,
const void* pSampleData = NULL, size_t nSampleLen = 0);
// 接管 pMaster 所有权并返回它的 clone(未命中后的入库路径)
AcDbRegion* Put(Channel ch, unsigned long long nKey, AcDbRegion* pMaster);
// 打印各通道命中率与条目数
void Dump();
// 清空并释放全部母体(卸载时调用,避免 DLL 析构顺序问题)
void ClearAll();
}
#endif // SECTION_REGION_CACHE_H
+52
View File
@@ -232,6 +232,58 @@ TempFiles=6773 TempBytes=9,341,125 WriteBacks=1140
- 母体不被任何调用方 `delete`。
- miss 路径的 `DeleteFile` 逻辑保持原样(含失败分支)。
### Phase 1 实施结果(2026-10-28,big1.dwg / R230 Debug)
已实现 `CoreLibrary/SectionRegionCache.{h,cpp}`,三个通道:
| 通道 | key | 用在哪 |
| --- | --- | --- |
| `kParam` | 纯参数 hash(dD/dt/HLR 标志) | `AecDbEwFitting::GetSection` 两个重载 |
| `kAttr` | 类名 + spec XData | 仅 `AecDbEwPipe`(其余派生类实例间差异就是截面,不适用) |
| `kText` | SAT 文本内容 hash | `AecDbEwPipeBase::RegionAcisInDwgInFields` 的其它类 + `CoreTools::StringToRegion` 两个重载 |
命中返回 `clone()`,未命中走原逻辑后 `Put()` 入库;母体不外泄;`AEC_SECTION_CACHE=0` 可整体关闭做 A/B。
#### 关键发现:SAT 文本第 2 行带存盘时间戳
导出未命中的 SAT 文本后发现:同一几何存盘时间不同,整份文本就不同——
```
700 56 1 0
16 Autodesk AutoCAD 19 ASM 219.0.0.5600 NT 24 Thu Oct 22 10:19:20 2026 ← 时间戳
1 9.9999999999999995e-007 1e-010
```
采样里两份 2596 字节、内容完全一致的文本,只差 `10:19:20` 与 `10:19:16`。
这就是内容 hash 命中率只有 54 % 的真正原因,与"截面是否相同"无关。
修复:`SectionRegionCache::HashSatText()` 在计算 hash 时跳过第 2 行
(仅当该行含 "Autodesk" 时才跳过,避免误伤其它格式)。计数器改用同一口径。
#### 实测对比
| 指标 | 基线(`AEC_SECTION_CACHE=0`) | 启用缓存 + 时间戳归一化 |
| --- | ---: | ---: |
| 开图墙钟 | 108~113 s | **98 s** |
| 截面恢复总耗时 | 22.4 s | **9.87 s**(−56 %) |
| `CoreTools::StringToRegion` | 14.05 s | 7.73 s |
| `EwPipe::RegionAcisIn` | 8.38 s | 2.14 s |
| `Fitting::GetSection` | 0.13 s | 0.035 s |
| 临时文件 | 6773 次 / 9.34 MB | **2875 次 / 3.61 MB** |
| 缓存命中 | — | text 65.5 %、attr 84.1 %、param 88.8 % |
| clone 开销 | — | 命中 58 µs、未命中 73 µs(合计仅 0.37 s) |
clone 成本可以忽略,说明"每次无论命中与否都发 clone"这个保守设计没有代价。
#### 仍未解决
1. `EwPipe::RegionAcisout` 仍是 **1140 次 / 2.38 s**,开图期间把几何又序列化回 SAT,
与缓存无关,应单独立项。
2. `Fitting::GetSection` 在**开图**阶段只有 232 次调用(0.13 s),收益很小;
它的价值可能在绘制/平移时,需要另一次针对"打开后操作"的测量才能确认。
3. `StringToRegion` 仍有 1570 次未命中(唯一文本 1562),这部分是真实的不同截面,
想再提升只能靠业务层的 key,暂不做。
### Phase 2:临时目录治理(可选,止血)
Phase 1 之后残余落盘只剩"每个唯一截面一次",收益小;但如果现场仍被杀软拖慢,可做这一步:
+4 -1
View File
@@ -22,7 +22,8 @@ param(
[string]$Acad = 'C:\Program Files\Autodesk\AutoCAD 2020\acad.exe',
[string]$Profile = 'SHS-ENVI2026',
[string]$Dwg = 'E:\big1.dwg',
[int] $TimeoutSec = 900
[int] $TimeoutSec = 900,
[switch]$CacheOff
)
$ErrorActionPreference = 'Continue'
@@ -41,6 +42,8 @@ Write-Output ("report : " + $report)
$existing = @(Get-Process acad -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id)
$env:AEC_PERF_COUNTER = '1'
if ($CacheOff) { $env:AEC_SECTION_CACHE = '0' } else { $env:AEC_SECTION_CACHE = '1' }
Write-Output ("cache : " + $(if ($CacheOff) { 'OFF (A/B baseline)' } else { 'ON' }))
$acadArgs = @('/p', $Profile, '/b', $script, $Dwg)
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$p = Start-Process -FilePath $Acad -ArgumentList $acadArgs -PassThru