perf: 增加截面 Region 缓存,开图恢复耗时降 56% #100
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
|
||||
@@ -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 之后残余落盘只剩"每个唯一截面一次",收益小;但如果现场仍被杀软拖慢,可做这一步:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user