docs: 记录 Phase 0 实测结果、AutoCAD 自动化实测环境与属性 key 的取舍 #98

Merged
gjm merged 2 commits from company-desktop/company-desktop-computer/fix-section-perf-ndebug AGit into main 2026-10-28 19:54:15 +08:00
5 changed files with 166 additions and 3 deletions

No files matched your search

+24
View File
@@ -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` 传中文),
@@ -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);
@@ -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
+52
View File
@@ -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 <dump.scr> 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
+87
View File
@@ -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
}