perf: 增加截面恢复路径的 Phase 0 计数器(Debug 探针) #97

Merged
gjm merged 1 commits from company-desktop/company-desktop-computer/perf-section-counter AGit into main 2026-10-28 19:47:10 +08:00
7 changed files with 417 additions and 7 deletions

No files matched your search

@@ -20,6 +20,7 @@
#include "SelectBlkDlg.h"
#include "gpc.h"
#include "ImpMD5.h"
#include "SectionPerfCounter.h"
#include "breptools.h"
#include "DimScaleZoneDlg.h"
#include "simpleProperty.h"
@@ -3113,6 +3114,10 @@ namespace
Adesk::UInt64 CoreTools::RegionToString(const AcDbRegion *pReg, char *& pszTxt)
{
// Phase 0 计数:回写(Region -> SAT 文本)次数
SectionPerf::Scope perfScope("CoreTools::RegionToString");
SectionPerf::NoteWriteBack("CoreTools::RegionToString");
Adesk::UInt64 dwFileLength = 0;
pszTxt = NULL;
@@ -3518,6 +3523,9 @@ bool CoreTools::CurveToString(const AcDbCurve * pCurve, CString & sScr2)
AcDbRegion* CoreTools::StringToRegion(const CString & sRegionScrp)
{
// Phase 0 计数:截面恢复耗时/次数/文本唯一性,见 pipe-section-region-performance-plan.md
SectionPerf::Scope perfScope("CoreTools::StringToRegion");
std::string sUtf8 = CoreTools::unicode2Ansii((LPCTSTR)sRegionScrp, CP_UTF8);
std::string sNewSrc;
if (!fixACISFormat(sUtf8.c_str(), sUtf8.size(), sNewSrc))
@@ -3538,10 +3546,13 @@ AcDbRegion* CoreTools::StringToRegion(const CString & sRegionScrp)
CString sFullPath = MakeTempSatPath();
if (sFullPath.IsEmpty()) return NULL;
SectionPerf::NoteRegionText(sNewSrc.c_str(), sNewSrc.size());
if (file.Open(sFullPath, CFile::modeCreate | CFile::modeWrite))
{
file.Write(sNewSrc.c_str(), sizeof(char) * (UINT)sNewSrc.size());
file.Close();
SectionPerf::NoteTempFile(sNewSrc.size());
}
else
{
@@ -3580,6 +3591,8 @@ AcDbRegion* CoreTools::StringToRegion(const CString & sRegionScrp)
AcDbRegion *CoreTools::StringToRegion(Adesk::UInt64 nLen, char *pszReg)
{
SectionPerf::Scope perfScope("CoreTools::StringToRegion(buf)");
std::string sNewSrc;
if (!fixACISFormat(pszReg, (size_t)nLen, sNewSrc))
{
@@ -3597,11 +3610,14 @@ AcDbRegion *CoreTools::StringToRegion(Adesk::UInt64 nLen, char *pszReg)
CString sFullPath = MakeTempSatPath();
if (sFullPath.IsEmpty()) return NULL;
SectionPerf::NoteRegionText(sNewSrc.c_str(), sNewSrc.size());
CFile file;
if (file.Open(sFullPath, CFile::modeCreate | CFile::modeWrite))
{
file.Write(sNewSrc.c_str(), sizeof(char) * (UINT)sNewSrc.size());
file.Close();
SectionPerf::NoteTempFile(sNewSrc.size());
}
else
{
@@ -19,6 +19,7 @@
#include "AecDbPingFaDim.h"
#include "AecDbDimScaleZone.h"
#include "AecDbJoggedCamera.h"
#include "SectionPerfCounter.h"
#include "breptools.h"
#include "HlrCutOption.h"
@@ -543,6 +544,20 @@ KTDocReactor *g_pDocReactor(NULL);
#define ADD_CMD(cmd, func) CoreTools::AddCommand(_T("KTEARTHWORMCORE"), _T(#cmd), _T(#cmd), ACRX_CMD_TRANSPARENT | ACRX_CMD_USEPICKSET, func)
#define ADD_CMD_EX(cmd, func, flag) CoreTools::AddCommand(_T("KTEARTHWORMCORE"), _T(#cmd), _T(#cmd), flag, func)
#if defined(_DEBUG)
// Phase 0:截面恢复计数器。实现见 CoreLibrary/SectionPerfCounter.cpp
void envierpf_dump()
{
SectionPerf::Dump();
}
void envierpf_reset()
{
SectionPerf::Reset();
acutPrintf(_T("\n[SectionPerf] 已清零。"));
}
#endif
void InitApplication()
{
#ifdef ZWCAD
@@ -1066,6 +1081,12 @@ void InitApplication()
void pzjcs();
ADD_CMD(co_pzjcs, pzjcs);
#if defined(_DEBUG)
// Phase 0:截面恢复计数器(仅 Debug,且需启动前设 AEC_PERF_COUNTER=1)
ADD_CMD(ENVIPERFDUMP, envierpf_dump);
ADD_CMD(ENVIPERFRESET, envierpf_reset);
#endif
#pragma endregion
if (CKTCadUtility::SetSystemVar(_T("STATUSBARAUTOWRAP"), 0))
@@ -18,6 +18,7 @@
#include "GiDrawUtility.h"
#include "AcGiViewDraw.h"
#include "AcDbX3dSolid.h"
#include "SectionPerfCounter.h"
#ifndef EW_EARTHWORMINIT
#define EW_EARTHWORMINIT _T("EW_EARTHWORMINIT")
@@ -793,6 +794,9 @@ AcDbRegion *AecDbEwPipeBase::RegionFromCurvesDwgInFields(AcDbDwgFiler *pFiler, A
AcDbRegion* AecDbEwPipeBase::RegionAcisInDwgInFields(AcDbDwgFiler* pFiler)
{
// Phase 0 计数:截面恢复耗时/次数,见 pipe-section-region-performance-plan.md
SectionPerf::Scope perfScope("EwPipe::RegionAcisIn");
//高版本acis方式
CString sFileName;
TCHAR* pszFileName = NULL;
@@ -816,6 +820,9 @@ AcDbRegion* AecDbEwPipeBase::RegionAcisInDwgInFields(AcDbDwgFiler* pFiler)
::memset(pszTxt, 0, nLen + 1);
pFiler->readBytes(pszTxt,nLen);
// 同一份 SAT 文本被恢复多少次:唯一 hash 数 / 模拟命中率
SectionPerf::NoteRegionText(pszTxt, (size_t)nLen);
if (sFileName.CompareNoCase(_T("errorfile")) == 0)
{
//原生写法
@@ -854,6 +861,9 @@ AcDbRegion* AecDbEwPipeBase::RegionAcisInDwgInFields(AcDbDwgFiler* pFiler)
file.Close();
delete[] pszTxt;
// 读侧落盘的临时文件次数/字节(杀软扫描影响面)
SectionPerf::NoteTempFile((size_t)nLen);
AcDbVoidPtrArray arBodyPtrs;
Acad::ErrorStatus es = AcDbBody::acisIn(sFullPath,arBodyPtrs);
if (es != Acad::eOk)
@@ -1196,6 +1206,10 @@ Acad::ErrorStatus AecDbEwPipeBase::BaseCurveDwgOutFields(AcDbCurve* pBaseCurve,A
Acad::ErrorStatus AecDbEwPipeBase::RegionAcisoutDwgOutFields(AcDbDwgFiler* pFiler) const
{
// Phase 0 计数:回写(Region -> SAT 文本)的次数与耗时
SectionPerf::Scope perfScope("EwPipe::RegionAcisout");
SectionPerf::NoteWriteBack("EwPipe::RegionAcisout");
if (m_pRegion == NULL)
return Acad::eInvalidInput;
@@ -1251,6 +1265,9 @@ Acad::ErrorStatus AecDbEwPipeBase::RegionAcisoutDwgOutFields(AcDbDwgFiler* pFile
ULONGLONG dwFileLength = file.GetLength();
Adesk::UInt32 nLen = (Adesk::UInt32)dwFileLength;
// acisOut 写出来的临时文件(回写侧的落盘量)
SectionPerf::NoteTempFile((size_t)nLen);
char* pszTxt = new char[nLen + 1];
::memset(pszTxt, 0, nLen + 1);
@@ -0,0 +1,281 @@
#include "stdafx.h"
#include "SectionPerfCounter.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;
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 文本 次数
};
// 故意用函数内 static:AGENTS.md 禁止文件级 static。
// CoreLibrary 是静态库且只被 CoreCommand 链接,
// 所以这里的实例在 CoreCommand.arx 里是唯一的一份。
Stats& GetStats()
{
static Stats s_stats;
if (!s_stats.bInited)
{
s_stats.bInited = true;
#if defined(_DEBUG)
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 = Hash64(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;
}
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;
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 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;
}
}
@@ -11,6 +11,7 @@ set(SOURCES
"AECAngleWatch.cpp"
"AecComplexCurveDraw.cpp"
"AecCoverBooleanOper.cpp"
"SectionPerfCounter.cpp"
"AecDb3dRoad.cpp"
"AecDb3dRoadAxisLine.cpp"
"AecDb3dRoadFitting.cpp"
@@ -0,0 +1,61 @@
// SectionPerfCounter.h —— Phase 0 计数器:截面(Region)恢复路径的实测探针
//
// 目的(见 pipe-section-region-performance-plan.md 的 Phase 0):
// 1. 每次截面恢复到底多贵(µs),一次开图总共多少次、多少时间;
// 2. 同一份 SAT 文本被重复恢复多少次(唯一 hash 数、模拟命中率)——
// 这个数字直接决定 Phase 1 截面缓存能省多少;
// 3. 临时文件写入次数与累计字节(杀软扫描影响面);
// 4. 放回写(RegionToString / RegionAcisoutDwgOutFields)的次数。
//
// 使用方式:
// set AEC_PERF_COUNTER=1 必须在启动 CAD 前设好(进程内只读一次)
// ENVIPERFDUMP 在 CAD 命令行打印汇总,同时追加到 %TEMP%\aec_section_perf.txt
// ENVIPERFRESET 清零
//
// 只在 _DEBUG 下生效:Release 里 Enabled() 恒为 false,调用点只剩一次 bool 判断。
// 不改变任何几何逻辑与存盘格式。
#ifndef SECTION_PERF_COUNTER_H
#define SECTION_PERF_COUNTER_H
#include <stddef.h>
namespace SectionPerf
{
// 环境变量 AEC_PERF_COUNTER 是否开启,且当前是 Debug 构建
bool Enabled();
// 计时作用域:函数入口构造,离开时累加耗时与次数
class Scope
{
public:
explicit Scope(const char* pszTag);
~Scope();
private:
Scope(const Scope&);
Scope& operator=(const Scope&);
const char* m_pszTag;
long long m_nStart;
bool m_bActive;
};
// 记录一次"待恢复的 SAT 文本":内部按内容算 64 位 hash,
// 统计唯一 hash 数与被重复命中的次数(模拟命中率)
void NoteRegionText(const char* pszTxt, size_t nLen);
// 记录一次临时文件写入
void NoteTempFile(size_t nBytes);
// 记录一次回写(Region -> SAT 文本)
void NoteWriteBack(const char* pszTag);
// 打印 + 写文件
void Dump();
// 清零
void Reset();
}
#endif // SECTION_PERF_COUNTER_H
+20 -7
View File
@@ -88,16 +88,29 @@ pSolidO->booleanOper(kBoolSubtract, pSolidI) // 布尔减
### Phase 0:先测量(不改行为)
在 `RegionAcisInDwgInFields` 与 `StringToRegion` 两个入口 + 临时文件写/删处加计数,
`_DEBUG` 编译进、环境变量 `AEC_PERF_COUNTER=1` 开启、命令 `ENVIPERFDUMP` 打印汇总:
已在 `AecDbEwPipeBase::RegionAcisInDwgInFields` / `RegionAcisoutDwgOutFields` 与
`CoreTools::StringToRegion`(两个重载)/ `RegionToString` 上接入探针,
实现见 `inc/SectionPerfCounter.h` + `CoreLibrary/SectionPerfCounter.cpp`。
**用法**(必须是 Debug 构建):
```bat
set AEC_PERF_COUNTER=1 :: 必须在启动 CAD 之前设好,进程内只读一次
:: 打开图纸
ENVIPERFDUMP :: 命令行打印汇总,并追加到 %TEMP%\aec_section_perf.txt
ENVIPERFRESET :: 清零,方便多次对比
```
Release 里 `SectionPerf::Enabled()` 恒为 false,调用点只剩一次 bool 判断,无发布风险。
采集项:
| 计数项 | 用途 |
| --- | --- |
| 调用次数、单次/累计 µs(QPC 包住整段) | 单次成本(预估 3~15 ms) |
| 输入文本 hash 的**唯一值数** | 复用率 = 1 − 唯一/总数 → Phase 1 收益上限 |
| **模拟命中率**(只统计、不改行为) | 同上,更直观 |
| 临时文件写入次数 + 累计字节 | 杀软影响面 |
| `RegionToString` / `RegionAcisoutDwgOutFields` 次数 | 查清打开时为何在回写(3.07 %) |
| `RegionTexts` / `UniqueHashes` / `SimulatedHits` / `HitRate` | 复用率 = 1 − 唯一/总数 → Phase 1 收益上限 |
| 每个 tag 的 `count` / `total_ms` / `avg_us` / `max_us` | 单次成本,判断值不值得做 |
| `TempFiles` / `TempBytes` | 杀软扫描影响面 |
| `WriteBacks` | 打开图纸时为何在回写(3.07 %) |
**决策点 D0**:唯一数是否远小于总次数(例如 30 种 × 1500 次)。
- 是 → Phase 1 直接做