From 5c1b6278941d9bef5599244f3bbfdab2177d4b2c Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:23:02 +0800 Subject: [PATCH 1/2] diagnose(node): time the raster contract fixture on the CI host refs #144 Adds a dispatch-only job that runs the exact node command sixty times sequentially and in twenty four-way concurrent batches, recording elapsed time and timeouts against the unchanged fixed 10s budget. --- .github/workflows/conpty-compat.yml | 58 +++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/.github/workflows/conpty-compat.yml b/.github/workflows/conpty-compat.yml index be4a9795..062e91c7 100644 --- a/.github/workflows/conpty-compat.yml +++ b/.github/workflows/conpty-compat.yml @@ -155,3 +155,61 @@ jobs: path: | build/r145-after-conpty build/test-artifacts + + windows-2025-node-contract-timing: + # Bounded diagnostic for #144: the raster style reference test times out at a + # fixed 10s on the pure Node fixture. This job only measures the exact + # command, sequentially and under a small concurrent load. + runs-on: windows-2025 + timeout-minutes: 25 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: '3.12' + - name: Time the node contract fixture + shell: pwsh + run: | + @' + import json, platform, subprocess, sys, time + from concurrent.futures import ThreadPoolExecutor + script = "research/ui/test_raster_native_contract.js" + node = subprocess.run(["node", "--version"], capture_output=True, text=True) + def one(): + start = time.monotonic() + try: + result = subprocess.run(["node", script], capture_output=True, text=True, timeout=10) + elapsed = time.monotonic() - start + return {"elapsed": round(elapsed, 3), "returncode": result.returncode, "timeout": False} + except subprocess.TimeoutExpired: + return {"elapsed": round(time.monotonic() - start, 3), "returncode": None, "timeout": True} + sequential = [one() for _ in range(60)] + with ThreadPoolExecutor(max_workers=4) as pool: + concurrent = [list(pool.map(lambda _: one(), range(4))) for _ in range(20)] + report = { + "schema": "termforge-node-contract-timing-v1", + "issue": 144, + "platform": platform.platform(), + "node_version": node.stdout.strip() or node.stderr.strip(), + "sequential": sequential, + "sequential_timeouts": sum(1 for row in sequential if row["timeout"]), + "concurrent": concurrent, + "concurrent_timeouts": sum(1 for batch in concurrent for row in batch if row["timeout"]), + "max_sequential": max(row["elapsed"] for row in sequential), + "max_concurrent": max(row["elapsed"] for batch in concurrent for row in batch), + } + print(json.dumps(report)) + with open("node-contract-timing.json", "w", encoding="utf-8") as stream: + json.dump(report, stream, indent=2) + sys.exit(1 if report["sequential_timeouts"] or report["concurrent_timeouts"] else 0) + '@ | Out-File -Encoding utf8 node-contract-timing.py + python node-contract-timing.py + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Preserve timing evidence + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: node-contract-timing-r144 + retention-days: 14 + if-no-files-found: warn + path: node-contract-timing.json From 7f8221ff741851fd7c7ac5838fb83ad8e59d0563 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:25:55 +0800 Subject: [PATCH 2/2] research(ui): measure the node contract fixture on the CI host refs #144 Sixty sequential and twenty four-way concurrent runs of the exact failing command: warm ~60-80ms, cold start 1.47s, zero timeouts against the unchanged 10s budget. The recorded 10s failures are far outside this distribution. --- docs/EVIDENCE.md | 1 + .../2026-10-29-node-contract-timing/README.md | 35 +++++++++++++++++++ .../validation.json | 34 ++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 research/ui/results/2026-10-29-node-contract-timing/README.md create mode 100644 research/ui/results/2026-10-29-node-contract-timing/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 97f35637..7c46df97 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -68,6 +68,7 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | 范围 | 结果与复现入口 | 可以支持的结论与限制 | |---|---|---| | 行存储封存与结构单一发布(#169 第二片) | [固定结果](../experiments/core/results/2026-10-29-sealed-row-storage/README.md)、[验证身份](../experiments/core/results/2026-10-29-sealed-row-storage/validation.json) | `Screen::lines/history` 私有,只读口 + 23 文件/363 处迁移;Find overlay 改文档 rank;默认 Release 构建 exit 0、核心 8/8、扩展快测 20/20、candidate 源 /W4 编译 0。无新 VT/重排行为;运行中跨重排 Search、Qt 与候选运行由宿主作业复核。 | +| Node 纯夹具在 CI 的耗时分布(#144 未闭) | [固定结果](research/ui/results/2026-10-29-node-contract-timing/README.md)、[验证身份](research/ui/results/2026-10-29-node-contract-timing/validation.json) | hosted windows-2025、Node v22.23.4:顺序 60 次 + 20×4 并发共 140 次、0 超时;warm ~63–78ms、冷启动 1.469s、并发最大 0.125s。10s 预算余量充足,两次 10s 失败不能用夹具成本解释;宿主停顿假设未复现,#144/#38 不关闭。 | | 原版合成会话的 UIA 可见性(#155 辅助) | [固定记录](research/windterm-synthetic-session-uia.md) | 隔离 VM 上确认:profile 被读取、组名进入主窗口与新建会话对话框,但**会话叶不在 UIA 树**(raw_children=0、无展开/选择模式);参数与累积状态均已排除;未发现命令行开会话入口。菜单与新建会话对话框可枚举/可写,但没有命令行字段。结论:UIA 不能作为驱动原版合成会话的前提。 | | 每屏 margin 的原生 gap:路径结论(#56) | [决策记录](research/conpty-margin-path-decision.md)、[合同与四阶段 oracle](research/conpty-native-margin-path.md)、[fork 构建/运行](https://github.com/JavaZeroo/termforge/pull/98) | 固定上游源码根因(共享 _scrollMargins)、hash 与 20 次同源运行(baseline red→patched 四阶段绿、412/412 adapter、17/17 回归)已固定;结论是“推荐 MIT fork,采纳需用户在 #34 确认”。不改变 ADR-0007 现状、#41 开放状态与产品后端。 | | 保留历史的内存与单次 Search 成本(#164 剩余项) | [固定结果](../experiments/core/results/2026-10-29-history-memory/README.md)、[验证身份](../experiments/core/results/2026-10-29-history-memory/validation.json) | 生产 `Screen`/`TextSearch` 在 H=100/1000/10000、各 3 个新进程样本:每行私有增量收敛 ~5.8–6.9 KB,H=10000 私有提交 ~59 MB;整文档 Search ~54 ms/199 步。不是 r164 fork 对照,也不是性能预算;不关闭 #164。 | diff --git a/research/ui/results/2026-10-29-node-contract-timing/README.md b/research/ui/results/2026-10-29-node-contract-timing/README.md new file mode 100644 index 00000000..8fc9a0d8 --- /dev/null +++ b/research/ui/results/2026-10-29-node-contract-timing/README.md @@ -0,0 +1,35 @@ +# Node 纯夹具在 hosted windows-2025 的耗时分布(#144) + +本片回答 [#144](https://github.com/JavaZeroo/termforge/issues/144) 的一个可测子问题:那条**纯 Node 夹具**在这台 CI 镜像上到底要多久,10 秒固定预算有多少余量。 + +## 方法 + +dispatch-only 作业 `windows-2025-node-contract-timing`([工作流](../../../../.github/workflows/conpty-compat.yml)),只做测量、不跑原版、不改任何夹具或期限: + +- 命令与失败测试完全一致:`node research/ui/test_raster_native_contract.js`; +- **顺序 60 次** + **20 批 × 4 并发**(共 140 次),每次新进程,10 秒超时,记录耗时与超时; +- 记录 Node 版本与平台,产物 JSON 上传。 + +## 结果(run [40864960451](https://github.com/JavaZeroo/termforge/actions/runs/40864960451),Node v22.23.4) + +| 指标 | 值 | +|---|---| +| 顺序 p50 / p90 | 0.063 s / 0.078 s | +| 顺序最大 | **1.469 s(第 1 次,冷启动)** | +| 顺序 min(warm) | 0.062 s | +| 并发批次最大 | 0.125 s | +| 超时次数 | **0 / 140** | + +第 1 次 1.469 s 是冷启动(首次执行 node.exe + 首次读夹具源文件),随后稳定在 ~60–80 ms;并发的 4 路也没有把单次推高。 + +## 解释 + +- 10 秒预算相对 warm 耗时约有 **130 倍余量**,相对冷启动也有约 7 倍余量; +- 因此已记录的两次 CI 失败(PR141 与 #178 的 push 运行,均 `timed out after 10 seconds`)**不能用夹具自身的计算成本解释**; +- 现象更像宿主级停顿(首次执行时的安全软件扫描、VM 调度/存储短暂阻塞)导致 Node 进程在 10 秒内没有完成,但本批在空闲镜像上无法复现该停顿,根因仍为 unknown。 + +## 边界与不做 + +- 只测空闲 windows-2025 镜像;未在“62 项 CTest 同时段”、其它作业并跑或安全软件扫描中的状态下测量。 +- 未修改夹具、未延长 10 秒预算、未重跑原失败;本批不关闭 #144/#38。 +- 记录的是诊断耗时,不是性能预算或容差。 diff --git a/research/ui/results/2026-10-29-node-contract-timing/validation.json b/research/ui/results/2026-10-29-node-contract-timing/validation.json new file mode 100644 index 00000000..47ea12cf --- /dev/null +++ b/research/ui/results/2026-10-29-node-contract-timing/validation.json @@ -0,0 +1,34 @@ +{ + "schema": "termforge-node-contract-timing-v1", + "issue": 144, + "run": 40864960451, + "job": "windows-2025-node-contract-timing", + "command": [ + "node", + "research/ui/test_raster_native_contract.js" + ], + "node_version": "v22.23.4", + "platform": "windows-2025 hosted", + "samples": { + "sequential": 60, + "concurrent_batches": 20, + "concurrent_per_batch": 4, + "total": 140 + }, + "results": { + "sequential_p50_s": 0.063, + "sequential_p90_s": 0.078, + "sequential_max_s": 1.469, + "first_sample_s": 1.469, + "concurrent_max_s": 0.125, + "timeouts": 0, + "budget_s": 10 + }, + "interpretation": "夹具 warm 约 60–80ms、冷启动约 1.5s;两次 10s 失败的耗时远离本分布,不能用夹具成本解释,宿主级停顿假设未复现。", + "limitations": [ + "空闲镜像,未在完整 CTest 同时段或安全软件扫描状态下测量", + "未改夹具/期限,未重跑原失败", + "不是性能预算" + ], + "performance_claim": false +}