From 3f387485fd0e8e2301354e9bd1fa71c09c6bf12d Mon Sep 17 00:00:00 2001 From: company-desktop-computer Date: Wed, 28 Oct 2026 19:52:18 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E8=AE=A1=E6=95=B0=E5=99=A8=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20NDEBUG=20=E5=88=A4=E6=96=AD=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=EF=BC=88=E6=9C=AC=E4=BB=93=E5=BA=93=E6=8A=8A?= =?UTF-8?q?=20=5FDEBUG=20=E5=89=A5=E6=8E=89=E4=BA=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:Debug 构建的 R230 里 ENVIPERFDUMP 报"未知命令",计数器也永远不生效。 根因:cmakelists.txt 第 23~24 行会把 /D_DEBUG 从 CMAKE_CXX_FLAGS_DEBUG 里正则剥掉, 所以即使 --config Debug 编译,_DEBUG 也没有定义。于是 Phase 0 里用 `#if defined(_DEBUG)` 包住的命令注册和 Enabled() 判定整块被编掉—— 已用二进制验证:修复前 arx 里查不到 ENVIPERFDUMP,修复后能查到(UTF-16 字符串)。 修复:改用 NDEBUG。cmakelists.txt 只对 Release 定义 NDEBUG (`$<$:NDEBUG>`),所以 `#ifndef NDEBUG` 正好等价于"Debug 构建"。 - CoreLibrary/SectionPerfCounter.cpp:计数器启用条件 - CoreCommand/EarthWorm.cpp:ENVIPERFDUMP / ENVIPERFRESET 的函数定义与命令注册 验证:R230 Debug 两个 target 重新编译通过,产物已部署; 脚本自动开图后 ENVIPERFDUMP 正常输出(数据已记录到计划文档 Phase 0 实测结果)。 已知风险:`#ifdef _DEBUG` 在本仓库本来就不生效,rxdebug.cpp 等历史代码同样如此, 本次只修自己新增的两处,没有动别处。 --- SourceCode/Code2026/CoreCommand/EarthWorm.cpp | 4 ++-- SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SourceCode/Code2026/CoreCommand/EarthWorm.cpp b/SourceCode/Code2026/CoreCommand/EarthWorm.cpp index 2667419..c9c14f1 100644 --- a/SourceCode/Code2026/CoreCommand/EarthWorm.cpp +++ b/SourceCode/Code2026/CoreCommand/EarthWorm.cpp @@ -544,7 +544,7 @@ 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) +#ifndef NDEBUG // Phase 0:截面恢复计数器。实现见 CoreLibrary/SectionPerfCounter.cpp void envierpf_dump() { @@ -1081,7 +1081,7 @@ void InitApplication() void pzjcs(); ADD_CMD(co_pzjcs, pzjcs); -#if defined(_DEBUG) +#ifndef NDEBUG // Phase 0:截面恢复计数器(仅 Debug,且需启动前设 AEC_PERF_COUNTER=1) ADD_CMD(ENVIPERFDUMP, envierpf_dump); ADD_CMD(ENVIPERFRESET, envierpf_reset); diff --git a/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp b/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp index d8309ea..47b6d0c 100644 --- a/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp +++ b/SourceCode/Code2026/CoreLibrary/SectionPerfCounter.cpp @@ -60,7 +60,7 @@ namespace if (!s_stats.bInited) { s_stats.bInited = true; -#if defined(_DEBUG) +#ifndef NDEBUG const char* psz = ::getenv("AEC_PERF_COUNTER"); s_stats.bEnabled = (psz != NULL && psz[0] != '\0' && psz[0] != '0'); #else -- 2.54.0 From 6559cdf6f51048d2d26f6496a62ba6d86a8df448 Mon Sep 17 00:00:00 2001 From: company-desktop-computer Date: Wed, 28 Oct 2026 19:52:18 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=E8=AE=B0=E5=BD=95=20Phase=200=20?= =?UTF-8?q?=E5=AE=9E=E6=B5=8B=E7=BB=93=E6=9E=9C=E3=80=81AutoCAD=20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8C=96=E5=AE=9E=E6=B5=8B=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E4=B8=8E=E5=B1=9E=E6=80=A7=20key=20=E7=9A=84=E5=8F=96=E8=88=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0 在本机跑通(big1.dwg / AutoCAD 2020 / R230 Debug,开图 108 s): RegionTexts=5641 UniqueHashes=2597 SimulatedHits=3044 HitRate=54.0% TempFiles=6773 TempBytes=9,341,125 WriteBacks=1140 CoreTools::StringToRegion 2559 次 14.6 s 平均 5.7 ms 最长 321 ms EwPipe::RegionAcisIn 3082 次 8.3 s 平均 2.7 ms EwPipe::RegionAcisout 1140 次 2.3 s 平均 2.0 ms 结论:一次开图做 5641 次 SAT 恢复,共 22.9 s(约占 108 s 的 21%)。 此前基于 CPU 采样的估算只有 550~1000 次,低估了 5~10 倍——说明采样只能算占比, 算不出调用次数,这类结论必须靠计数器。 按 54% 命中率折算,Phase 1 缓存净省约 12 s(约 11%),并少约 4000 次临时文件写盘。 同时记录三个待追查项:命中率为何只有 54%、StringToRegion 为何比 RegionAcisIn 贵一倍、 开图期间为何回写 1140 次 SAT。 文档里补充了"用管子属性做 key"的思路与风险:属性相同则截面相同,业务上成立, 但不能单独当 key——DWG 里的 SAT 文本可能来自旧版本/旧路径/手工编辑的异形截面, 属性漏一项就会静默复用错误几何。若要做,采用 L1 内容 hash + L2 属性 key 的两级结构, L2 命中后仍比对 textHash,不一致按 miss 处理;落地前先加一路属性 key 命中率计数比较。 另外新增 tools/run_acad_perf.ps1 并在 AGENTS.md 记录本机 CAD 实测环境: acad.exe 路径、/p SHS-ENVI2026 启动参数、测试图纸、部署目录, 以及"PowerShell 5.1 把无 BOM 的 UTF-8 当 ANSI 读"这个坑(脚本保持 ASCII)。 验证:文档与脚本;脚本语法检查通过,本次实测就是用它的等价命令跑出来的。 --- AGENTS.md | 24 +++++++ pipe-section-region-performance-plan.md | 52 +++++++++++++++ tools/run_acad_perf.ps1 | 87 +++++++++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 tools/run_acad_perf.ps1 diff --git a/AGENTS.md b/AGENTS.md index 74a27d9..e53275a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,30 @@ 里面记录了很多"已经决定不做"的理由,别重复踩。 - 构建命令与 target 对照见 `CODEBUDDY.md`,或直接用 skill `build-arx`。 +## 自动化 CAD 实测 + +可以无人值守跑一次「开图 → 执行命令 → 收结果 → 关进程」。本机环境: + +| 项 | 值 | +| --- | --- | +| AutoCAD | `c:\Program Files\Autodesk\AutoCAD 2020\acad.exe`(= R23.0,对应 preset **R230**) | +| 启动参数 | `/p SHS-ENVI2026`(试验用 profile,会自动加载本仓库的 arx) | +| 测试图纸 | `e:\桌面\big1.dwg`(约 8 MB,一次开图 ≈ 108 s) | +| 部署目录 | `c:\ShangHuaSoft\Envi3D-2026-M\R230\`(Debug 构建会自动部署到这里) | + +一键脚本:`tools/run_acad_perf.ps1` + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File tools\run_acad_perf.ps1 +``` + +它的做法:写一个只含 `ENVIPERFDUMP` 的 `.scr`,用 `/b` 让 AutoCAD 在**图纸载入完成后**执行; +报告落 `%TEMP%\aec_section_perf.txt`;收完报告按 PID 结束**本次拉起的** acad, +脚本里不写 QUIT,所以不会弹保存对话框、不会回写图纸。 + +写这类 `.ps1` 时注意:**Windows PowerShell 5.1 把无 BOM 的 UTF-8 当 ANSI 读**, +带中文会直接语法错误——要么保存为 UTF-8 with BOM,要么脚本全用 ASCII(推荐)。 + ## 需要用户决策时 不要把问题打在终端里就停住——先跑 `tools/notify_decision.ps1`(用 `-MessageFile` 传中文), diff --git a/pipe-section-region-performance-plan.md b/pipe-section-region-performance-plan.md index 7b3754c..40c129f 100644 --- a/pipe-section-region-performance-plan.md +++ b/pipe-section-region-performance-plan.md @@ -116,12 +116,64 @@ Release 里 `SectionPerf::Enabled()` 恒为 false,调用点只剩一次 bool - 是 → Phase 1 直接做 - 否(每个实体文本都不同)→ 停下重新评估(此时缓存收益接近 0) +#### Phase 0 实测结果(2026-10-28,big1.dwg / AutoCAD 2020 / R230 Debug) + +自动跑图:`tools/run_acad_perf.ps1`(等价于 `acad.exe /p SHS-ENVI2026 /b big1.dwg`), +开图到出报告 **108 s**。 + +``` +RegionTexts=5641 UniqueHashes=2597 SimulatedHits=3044 HitRate=54.0% +TempFiles=6773 TempBytes=9,341,125 WriteBacks=1140 + +---- timing (by tag) ---- + count total_ms avg_us max_us tag + 2559 14620.1 5713.2 320828 CoreTools::StringToRegion + 3082 8315.3 2698.0 17447 EwPipe::RegionAcisIn + 1140 2282.1 2001.8 35390 EwPipe::RegionAcisout +``` + +解读: + +| 结论 | 数值 | +| --- | --- | +| 一次开图做 SAT 恢复 | **5641 次**(此前基于采样的估算只有 550~1000,低估了 5~10 倍) | +| 恢复总耗时 | **22.9 s**(14.6 + 8.3),占 108 s 开图约 21 % | +| 单次成本 | 2.7~5.7 ms,实为「写临时文件 + 从盘 `acisIn` + 删」而非内存解析 | +| 极端值 | 单次最长 321 ms(`StringToRegion`) | +| 唯一截面内容 | 2597 种 → **命中率 54.0 %** | +| 临时文件 | 6773 次写入 / 8.9 MB(每次都过杀软) | +| 打开时仍在回写 | **1140 次 / 2.3 s**,开图期间不应发生的序列化 | + +**D0 结论:值得做。** 按命中率折算 3044 次命中 × 加权均值 4.07 ms ≈ 省 12.4 s, +减去 clone 成本(几十 µs 级,合计约 0.1~0.3 s)→ **净省约 12 s / 108 s ≈ 11 %**, +并少约 4000 次临时文件写盘。与基于 trace 的估算(10~14 %)吻合。 + +三个待追查项(不阻塞 Phase 1): + +1. **命中率只有 54 %**。2597 种唯一文本对 5641 次恢复,说明同 DN 的 SAT 文本并非完全一致。 + 需确认差异是"真的不同几何"(不同管径/壁厚/异形)还是"带了绝对坐标等无关内容"。 + 前者属性 key 也救不了;后者归一化后命中率有望到 80 %+。 +2. `StringToRegion` 单次成本是 `RegionAcisIn` 的 2 倍(多一步 `fixACISFormat` 改写文本)。 +3. **开图期间回写 1140 次 SAT(2.3 s)** 属于纯亏,应单独立项查清来源。 + ### Phase 1:截面内容缓存(核心,全平台) 新增一个缓存模块(建议 `CoreLibrary/SectionRegionCache.h/.cpp`,私有实现放匿名 namespace): - **key**:输入 SAT 文本字节的 64 位 hash(自实现 FNV-1a/xxHash,不引第三方),长度参与计算。 → **内容寻址,不需要任何失效逻辑**:样式/参数变了,文本变了,key 自然变。 + → 选它的核心理由是**可证明正确**:字节相同即几何相同,不存在"属性漏项导致复用错几何"的风险。 +- **关于"用管子属性做 key"**(业务上更自然的思路,实测命中率可能高于内容 hash): + 属性相同的管子截面必然相同,但**不能单独当 key**——DWG 里存的 SAT 文本可能来自旧版本、 + 旧代码路径或用户手工编辑过的异形截面,这些不是属性区分的;只要有一项影响截面的属性漏进 + key,就会静默复用错误几何。若要做,用两级: + ``` + L1 内容 hash → region (可证明正确,当前实测 54%) + L2 属性 key → textHash (加速索引;命中后仍比对 textHash,不一致按 miss 处理) + ``` + 对几 KB 文本做一次 hash 只需几微秒,这个校验几乎免费,但把"属性漏项"从静默错误 + 降级为缓存不命中。属性 key 还需随样式表(DN→尺寸)版本变化失效,内容 hash 不需要。 + 落地前应先加一路"属性 key 命中率"计数,与内容 hash 并行比较再决定是否值得引入。 - **value**:**母体** `AcDbRegion*`(heap 对象,不入库,永不外泄)。 - **API**: - `AcDbRegion* SectionRegionCache::Acquire(const char* psz, Adesk::UInt32 nLen)` —— 命中返回 `clone()`,未命中返回 NULL diff --git a/tools/run_acad_perf.ps1 b/tools/run_acad_perf.ps1 new file mode 100644 index 0000000..893c9db --- /dev/null +++ b/tools/run_acad_perf.ps1 @@ -0,0 +1,87 @@ +# run_acad_perf.ps1 - automatically open a drawing in AutoCAD, run ENVIPERFDUMP, +# collect the SectionPerf report, then close only the process we started. +# +# Usage (from repo root): +# powershell -NoProfile -ExecutionPolicy Bypass -File tools\run_acad_perf.ps1 +# powershell ... -Dwg "E:\path\to\big1.dwg" -TimeoutSec 900 +# +# Notes: +# - The dump command is executed through a /b script, which AutoCAD runs AFTER the +# drawing has loaded. The script contains only ENVIPERFDUMP (no QUIT) so that a +# "save changes?" dialog can never block it; the process is killed by PID instead, +# so the drawing is never written back. +# - AEC_PERF_COUNTER=1 is set for the child process only. +# - Only processes started by this run are terminated: an acad.exe that was already +# running before launch is left alone. +# - Must be started from a Debug build that contains the SectionPerf probes +# (see pipe-section-region-performance-plan.md, Phase 0). +# +# Keep this file ASCII-only: Windows PowerShell 5.1 reads BOM-less files as ANSI. + +param( + [string]$Acad = 'C:\Program Files\Autodesk\AutoCAD 2020\acad.exe', + [string]$Profile = 'SHS-ENVI2026', + [string]$Dwg = 'E:\big1.dwg', + [int] $TimeoutSec = 900 +) + +$ErrorActionPreference = 'Continue' + +$report = Join-Path $env:TEMP 'aec_section_perf.txt' +$script = Join-Path $env:TEMP 'aec_perf_dump.scr' + +if (Test-Path $report) { Remove-Item $report -Force } +Set-Content -Path $script -Value 'ENVIPERFDUMP' -Encoding ASCII + +Write-Output ("acad : " + $Acad) +Write-Output ("profile : " + $Profile) +Write-Output ("dwg : " + $Dwg) +Write-Output ("report : " + $report) + +$existing = @(Get-Process acad -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id) + +$env:AEC_PERF_COUNTER = '1' +$acadArgs = @('/p', $Profile, '/b', $script, $Dwg) +$sw = [System.Diagnostics.Stopwatch]::StartNew() +$p = Start-Process -FilePath $Acad -ArgumentList $acadArgs -PassThru +Write-Output ("launched pid=" + $p.Id) + +$got = $false +while ($sw.Elapsed.TotalSeconds -lt $TimeoutSec) { + Start-Sleep -Seconds 5 + if (Test-Path $report) { + $sz1 = (Get-Item $report).Length + Start-Sleep -Seconds 3 + $sz2 = (Get-Item $report).Length + if ($sz1 -gt 0 -and $sz1 -eq $sz2) { $got = $true; break } + } + $t = [int]$sw.Elapsed.TotalSeconds + if ($t % 60 -lt 5) { + $alive = [bool](Get-Process -Id $p.Id -ErrorAction SilentlyContinue) + Write-Output (" ... " + $t + "s running=" + $alive) + } +} + +Write-Output "" +if ($got) { + Write-Output ("[OK] report ready after " + [int]$sw.Elapsed.TotalSeconds + " s (approx. wall time to open)") +} else { + Write-Output ("[TIMEOUT] no report within " + $TimeoutSec + " s") + $alive = Get-Process -Id $p.Id -ErrorAction SilentlyContinue + if ($alive) { Write-Output (" still running, window title: '" + $alive.MainWindowTitle + "'") } + else { Write-Output " process exited (command missing? build without probes?)" } +} + +$after = @(Get-Process acad -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id) +foreach ($id in $after) { + if ($existing -notcontains $id) { + try { Stop-Process -Id $id -Force -ErrorAction Stop; Write-Output ("killed pid=" + $id) } + catch { Write-Output ("kill failed pid=" + $id + ": " + $_.Exception.Message) } + } +} + +if ($got) { + Write-Output "" + Write-Output "======== report ========" + Get-Content -Path $report -Encoding UTF8 +} -- 2.54.0