diff --git a/SourceCode/Code2026/CoreCommand/CoreTools.cpp b/SourceCode/Code2026/CoreCommand/CoreTools.cpp index 3fb2da2..91fb2a9 100644 --- a/SourceCode/Code2026/CoreCommand/CoreTools.cpp +++ b/SourceCode/Code2026/CoreCommand/CoreTools.cpp @@ -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 { diff --git a/SourceCode/Code2026/CoreCommand/EarthWorm.cpp b/SourceCode/Code2026/CoreCommand/EarthWorm.cpp index 7ec11ad..2667419 100644 --- a/SourceCode/Code2026/CoreCommand/EarthWorm.cpp +++ b/SourceCode/Code2026/CoreCommand/EarthWorm.cpp @@ -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)) diff --git a/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp b/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp index d0e4578..f38d72d 100644 --- a/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp +++ b/SourceCode/Code2026/CoreLibrary/AecDbEwPipeBase.cpp @@ -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); diff --git a/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp b/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp new file mode 100644 index 0000000..d8309ea --- /dev/null +++ b/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp @@ -0,0 +1,281 @@ +#include "stdafx.h" +#include "SectionPerfCounter.h" + +#include +#include +#include +#include +#include + +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 entries; // tag -> 次数/耗时 + + unsigned long long nRegionTexts; // 一共恢复过多少份 SAT 文本 + size_t nUniqueHashes; // 其中不同内容有多少 + unsigned long long nSimulatedHits; // 若做内容缓存,能命中多少次 + bool bHashTableOverflow; + + std::set 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::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; + } +} diff --git a/SourceCode/Code2026/CoreLibrary/cmakelists.txt b/SourceCode/Code2026/CoreLibrary/cmakelists.txt index 341c111..3494639 100644 --- a/SourceCode/Code2026/CoreLibrary/cmakelists.txt +++ b/SourceCode/Code2026/CoreLibrary/cmakelists.txt @@ -11,6 +11,7 @@ set(SOURCES "AECAngleWatch.cpp" "AecComplexCurveDraw.cpp" "AecCoverBooleanOper.cpp" + "SectionPerfCounter.cpp" "AecDb3dRoad.cpp" "AecDb3dRoadAxisLine.cpp" "AecDb3dRoadFitting.cpp" diff --git a/SourceCode/Code2026/inc/SectionPerfCounter.h b/SourceCode/Code2026/inc/SectionPerfCounter.h new file mode 100644 index 0000000..bc0f19f --- /dev/null +++ b/SourceCode/Code2026/inc/SectionPerfCounter.h @@ -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 + +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 diff --git a/pipe-section-region-performance-plan.md b/pipe-section-region-performance-plan.md index f318d6c..7b3754c 100644 --- a/pipe-section-region-performance-plan.md +++ b/pipe-section-region-performance-plan.md @@ -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 直接做