From f840d733ca6463179951ab60f686f9d8920d845e Mon Sep 17 00:00:00 2001 From: company-desktop-computer Date: Thu, 29 Oct 2026 23:00:03 +0800 Subject: [PATCH] =?UTF-8?q?diag:=20=E9=87=8F=20X3dPart=20=E5=87=A0?= =?UTF-8?q?=E4=BD=95=E5=AD=98=E7=9B=98=E7=9A=84=E7=9C=9F=E5=AE=9E=E4=BD=93?= =?UTF-8?q?=E7=A7=AF=E4=B8=8E=E5=BA=8F=E5=88=97=E5=8C=96=E6=88=90=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:上一轮判定"3D 缓存存盘对管道不做"时,顺带给 X3dPart(Rebuild 占开图 18~22%)估了个"体积 +10~40 MB、存盘 +1.5~4 s"——这是估算,没有实测,结论可能 把这条路误杀了。本次补上测量。 改动:AecDbX3dPart::EnsureCreated 里加一个探针(AEC_X3D_SIZEPROBE=1 才生效, Release 零开销):每个 part 重建后对它的 m_pSolid 做一次 AcDbBody::acisOut 到临时文件,记录 SAT 字节数与耗时,然后删文件;每 100 个 part 把汇总写 %TEMP%\aec_x3d_probe.txt。同时记录同一次 Rebuild 的耗时用于对比。 big1.dwg 实测(AutoCAD 2020 / R230 Debug,开图 95 s): - 有几何的 part:1200(1248 个里 48 个 Hide/空,无 solid) - SAT 总字节 3.56 MB,平均 2968 B/part,最大 36086 B - acisOut 平均 0.799 ms/part;Rebuild 平均 15.3 ms/part(本次合计 20.4 s) - 重建 / 序列化 = 19.1 倍 据此(都取保守侧): - DWG 体积增量 ≤ 3.4 MB(8.2 -> ~11.6 MB,+42%)——DWG 里存的是二进制 SAB, 通常比 SAT 文本更小; - 每次存盘多花 ≤ 0.96 s(1200 x 0.8 ms),且二进制写比转文本便宜、不落盘; - 开图预计省 15~18 s(重建 18~20 s,恢复按 acisOut 同量级估 1~2 s)。 结论修正:X3dPart 这条"几何存盘"是值得做的,之前的体积/耗时估算高估了 3~10 倍。 配套说明写进 3d-cache-dwg-verdict.md §7。 --- .../Code2026/CoreLibrary/AecDbX3dPart.cpp | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/SourceCode/Code2026/CoreLibrary/AecDbX3dPart.cpp b/SourceCode/Code2026/CoreLibrary/AecDbX3dPart.cpp index fdf7375..30ab4ce 100644 --- a/SourceCode/Code2026/CoreLibrary/AecDbX3dPart.cpp +++ b/SourceCode/Code2026/CoreLibrary/AecDbX3dPart.cpp @@ -20,6 +20,138 @@ static const Adesk::UInt16 kMagenta = 6; static const Adesk::UInt16 kWhite = 7; static const Adesk::UInt16 kColorByLayer = 256; +// =========================================================================== +// 试点探针(AEC_X3D_SIZEPROBE=1 才生效,Release 零开销) +// +// 目的:量清楚"把一个 part 的 m_pSolid 存进 DWG"到底要多大、多贵。 +// - acisOut 出来的 SAT 文本字节数:二进制 SAB 一般比它小,所以这是体积**上界**; +// - acisOut 的耗时:二进制写只要格式化更少,所以这是"序列化一次"的耗时**上界**; +// - 顺带对比同一次 Rebuild 的耗时,算出"恢复 vs 重建"的比值。 +// 汇总写 %TEMP%\aec_x3d_probe.txt(每 100 个 part 刷新一次)。 +// =========================================================================== +namespace +{ + struct X3dProbeStat + { + X3dProbeStat() : n(0), bytes(0), maxBytes(0), outUs(0), rebuildUs(0) {} + long long n; + long long bytes; + long long maxBytes; + long long outUs; + long long rebuildUs; + }; + + X3dProbeStat& ProbeStat() { static X3dProbeStat s; return s; } + + bool ProbeEnabled() + { + static int s_nOn = -1; + if (s_nOn < 0) + { + const char* psz = ::getenv("AEC_X3D_SIZEPROBE"); + s_nOn = (psz != NULL && psz[0] != '\0' && psz[0] != '0') ? 1 : 0; + } + return s_nOn == 1; + } + + long long ProbeQpc() + { + LARGE_INTEGER li; + ::QueryPerformanceCounter(&li); + return (long long)li.QuadPart; + } + + long long ProbeQpcFreq() + { + static long long s_f = 0; + if (s_f == 0) + { + LARGE_INTEGER li; + if (!::QueryPerformanceFrequency(&li) || li.QuadPart == 0) s_f = 1; + else s_f = (long long)li.QuadPart; + } + return s_f; + } + + void ProbeDump() + { + TCHAR szTmp[MAX_PATH + 1]; + ::memset(szTmp, 0, sizeof(szTmp)); + if (::GetTempPath(MAX_PATH, szTmp) == 0) + return; + TCHAR szFile[MAX_PATH + 1]; + ::_stprintf_s(szFile, MAX_PATH, _T("%saec_x3d_probe.txt"), szTmp); + + FILE* fp = NULL; + if (_tfopen_s(&fp, szFile, _T("w")) != 0 || fp == NULL) + return; + + const X3dProbeStat& s = ProbeStat(); + fprintf(fp, "---- X3dPart geometry size probe ----\n"); + fprintf(fp, "parts measured : %lld\n", s.n); + fprintf(fp, "total SAT bytes : %lld (%.1f MB)\n", s.bytes, (double)s.bytes / 1048576.0); + if (s.n > 0) + { + fprintf(fp, "avg SAT bytes/part : %lld\n", s.bytes / s.n); + fprintf(fp, "max SAT bytes/part : %lld\n", s.maxBytes); + fprintf(fp, "avg acisOut per part: %.3f ms\n", (double)s.outUs / (double)s.n / 1000.0); + fprintf(fp, "avg Rebuild per part: %.3f ms\n", (double)s.rebuildUs / (double)s.n / 1000.0); + if (s.outUs > 0) + fprintf(fp, "rebuild / acisOut : %.1f x\n", (double)s.rebuildUs / (double)s.outUs); + } + fclose(fp); + } + + // 对一个 solid 量 SAT 字节数 + acisOut 耗时 + void ProbeOneSolid(AcDb3dSolid* pSolid, long long nRebuildUs) + { + if (!ProbeEnabled()) + return; + + X3dProbeStat& s = ProbeStat(); + ++s.n; + s.rebuildUs += nRebuildUs; + + if (pSolid != NULL) + { + TCHAR szTmp[MAX_PATH + 1]; + ::memset(szTmp, 0, sizeof(szTmp)); + if (::GetTempPath(MAX_PATH, szTmp) != 0) + { + TCHAR szFile[MAX_PATH + 1]; + ::_stprintf_s(szFile, MAX_PATH, _T("%saec_x3d_probe_%lu.sat"), + szTmp, (unsigned long)::GetCurrentProcessId()); + + AcDbVoidPtrArray arSolids; + arSolids.append(pSolid); + + const long long t0 = ProbeQpc(); + Acad::ErrorStatus es = AcDbBody::acisOut(szFile, arSolids); + const long long t1 = ProbeQpc(); + s.outUs += (t1 - t0) * 1000000LL / ProbeQpcFreq(); + + if (es == Acad::eOk) + { + WIN32_FILE_ATTRIBUTE_DATA fad; + ::memset(&fad, 0, sizeof(fad)); + if (::GetFileAttributesEx(szFile, GetFileExInfoStandard, &fad)) + { + const long long nSize = + ((long long)fad.nFileSizeHigh << 32) | (long long)fad.nFileSizeLow; + s.bytes += nSize; + if (nSize > s.maxBytes) + s.maxBytes = nSize; + } + ::DeleteFile(szFile); + } + } + } + + if (s.n % 100 == 0) + ProbeDump(); + } +} + static void FreeBuffer(int n,AcDbVoidPtrArray& arEntPtrs) { AcDbEntity* pEnt = NULL; @@ -960,7 +1092,11 @@ bool AecDbX3dPart::EnsureCreated() const SectionPerf::NoteEnsure("X3dPart", this, true); //试点计时:X3dPart 重建总耗时(用来看"几何存盘"这套机制的天花板在哪) SectionPerf::Scope perfScope("X3dPart::Rebuild"); + const bool bProbe = ProbeEnabled(); + const long long nProbeT0 = bProbe ? ProbeQpc() : 0; const bool bOk = const_cast(this)->Rebuild(); + if (bProbe) + ProbeOneSolid(m_pSolid, (ProbeQpc() - nProbeT0) * 1000000LL / ProbeQpcFreq()); SectionPerf::NoteEnsure(m_pSolid != NULL ? "X3dPart.rebuild_ok" : "X3dPart.rebuild_null", this, false); if (m_pSolid == NULL) {