Repository navigation
test-perf: diagnose and cut the wall clock of the local test and gate corpus, especially on Windows Git Bash #3716
Description
Summary
Running the local test suites and the pre-PR gate scripts takes long enough on a Windows Git Bash host that a single phase close-out spends most of its wall clock waiting on them, and several individual scripts exceed the Claude Code Bash tool's two-minute default timeout on their own. The repository already documents the cause in scripts/affected-tests.sh: "The full corpus is tens of minutes of wall clock on a Windows box (Git Bash pays ~140ms per process spawn, and these suites are spawn-bound), which is long enough that nobody runs it locally and regressions reach CI." This issue asks for a measured program to troubleshoot, diagnose, and optimize test and gate performance so the corpus is as fast as it can be, with Windows as the primary target.
Observed (2026-09-04, Windows 11, Git Bash, worktree on the feat/handoff-prompt-qol branch)
- Corpus size: 354
*.test.shsuites plus 42 Python test files underplugins/. scripts/check-purged-em-dashes.shexceeded the 120-second tool default on its first run (killed, exit 143) while scanning 105 files; it passed on a rerun under a 10-minute timeout.scripts/check-changed-skills.sh origin/mainexceeds 120 seconds for six touched skills and has to be backgrounded.scripts/affected-tests.sh --runon a 56-file diff takes several minutes per run; two runs were needed this session.plugins/session-flow/scripts/save_point.test.sh: 52 pytest cases take 10 to 14 seconds;plugins/session-flow/skills/retro/scripts/parse-transcript.test.sh: 38 cases take 2.6 to 5.4 seconds. Both are dominated by interpreter startup and per-case subprocess spawns, not by the assertions.scripts/run-plugin-tests.shkeeps--jobs 1as the default on Windows because a parallel run measured sublinear (the box saturates on process creation), and tool-honesty and jira conformance suites fail under run-plugin-tests --jobs 4, pass serially #3694 shows two suites fail under--jobs 4.- Side observation while diagnosing: with a merge in progress in the worktree,
affected-tests.shresolves its diff base to the fork point rather than the merge target, so files changed only onorigin/mainare reported as unmapped and the run exits 1 before executing any suite. Worth a guard or a clearer message.
Related
- Hook performance program: measure and cut every runtime-cost surface on the Windows host #3623 (hook performance program on the Windows host) and perf(hooks): Windows process-creation tax makes the hook set unusable - 1,778 timeouts / 20.6 hours blocked over 9 days #3508 (Windows process-creation tax on hooks): the same root cost, on the hook surface rather than the test surface.
- perf(host): melo-desk-001 process creation costs 1.4 to 4 s system-wide; 3.81M leaked kernel Token objects (~10 GB paged pool) since boot #3715 (host-level process creation of 1.4 to 4 seconds system-wide): measurements for this issue should be taken on a host that does not carry that condition, or at least record it, so the baseline reflects the scripts rather than the machine.
- ci: push-to-main wall time rose from 6 to 21 minutes after the six-job consolidation; parallelise test-linux on the push path #3705 (CI push-to-main wall time) and tool-honesty and jira conformance suites fail under run-plugin-tests --jobs 4, pass serially #3694 (suites that fail under
--jobs 4): the CI-side and parallelism-side halves of the same problem. - Five suites fail only on a Windows Git Bash host; give them the host-skip pattern or fix them #3683 (five suites fail only on Windows Git Bash).
Proposed work
- Instrument first. Add a timing mode to
scripts/run-plugin-tests.shandscripts/affected-tests.shthat records per-suite wall clock and a process-spawn count (aDEBUGtrap orbash -xline count is enough on Git Bash wherestraceis unavailable), and commit a ranked baseline table so every later change is measured against it. - Diagnose the top offenders by mechanism: command substitutions inside per-item loops,
grep/sed/jq/gitinvoked once per file or per line,findwalks repeated per suite,mktemp -dper case,cygpathandunameper call,npxcold starts, and one pytest interpreter start per wrapper. - Optimize by the same pattern
lib/hook-utils.shalready used (perf(hook-utils): build the telemetry envelope and read file_path with builtins #3678): builtins over spawns, one batchedgitcall over many, onejqparse reused, onefindwalk hoisted, and one pytest invocation for co-located Python suites (with-p no:cacheproviderso no.pytest_cacheis left for markdownlint to trip over). - Make parallelism safe on Windows: fix the suites in tool-honesty and jira conformance suites fail under run-plugin-tests --jobs 4, pass serially #3694 that fail under
--jobs, then measure whether--jobs 2or--jobs 4pays on a healthy host and set the default accordingly. - Guard the result: a per-suite wall-clock budget lane (warn, then fail above a documented ceiling) so a regression is caught locally, and a documented fast local loop (the smallest command that covers a typical diff).
Acceptance
- A committed baseline table of per-suite wall clock on Windows and on the Linux CI runner.
- No single gate script in
scripts/exceeds 60 seconds on a healthy Windows host. scripts/affected-tests.sh --runfor a typical 50-file diff completes in under 2 minutes on Windows.- The full
scripts/run-plugin-tests.shcorpus completes in under 10 minutes on Windows, with the number recorded. - Each optimization is behavior-preserving and shown so by the existing suites passing unchanged.
Activity
chore(topic-docs): prune the handoff-prompt-qol contract slice before…
feat(session-flow): shape-2 handoff save-points with the resume promp…
ci: shard test-linux four ways and gate the lint self-tests (#3773)
perf(guardrails): drop dirname and sed execs from the always-on path (#…
kyle-sexton commentedon Sep 6, 2026
This was generated by AI during triage.
Merged from #3717 (duplicate)
#3717 was filed the same day, by the same author, on the same Windows Git Bash host, against the same root cost: per-process spawn overhead making the local gate and test corpus slow enough that a per-edit check becomes a per-phase check. Closing it as a duplicate of this issue, which carries the acceptance criteria. Carrying over its measurements, which are the per-script numbers this issue asks for in step 1 and did not yet have:
plugins/skill-quality/scripts/check-skill.sh--require-evals visualize(one skill, markdownlint skipped)scripts/check-changelog-parity.sh--check-bump origin/main--check> 180 s;--check-preserved origin/main16 sscripts/affected-tests.sh--runafter anevals/evals.jsoneditscripts/check-purged-em-dashes.shplugins/planning/scripts/check-open-questions.sh--ledger … --brief …--ledger-only form returns instantlyConditions for those numbers: branch
feat/visualize-absorb-show-me, baseorigin/main8eae8e8, Claude Code 2.1.260, Windows 11, Git Bash, nonode_modulesuntilnpm ci.Suspected mechanisms #3717 named, for the step 2 diagnosis:
check-skill.shis 1630 lines and counts codepoints throughiconv, one spawn per file.check-changelog-parity.shis 728 lines and appears to diff every plugin's manifest and changelog against the base ref, so--check-bumpscales with the fleet rather than the diff.affected-tests.sh's basename reference rule maps anyevals.jsonedit to every suite mentioningevals.json, hence ~148 suites for one plugin's file. This is a selection defect as much as a performance one.check-open-questions.sh --briefreads the same ledger the fast path reads, so the cost is in the Brief extraction (extract_section), not the register parse.Two scope items this issue's acceptance should absorb:
scripts/exceeds 60 seconds" does not reach two of the worst offenders above, which live in plugin trees:plugins/skill-quality/scripts/check-skill.shandplugins/planning/scripts/check-open-questions.sh. Worth widening the criterion to plugin-scoped gates, or they get optimized last.--helpor the CI docs, so a contributor knows when to background a run rather than waiting on it. That complements this issue's step 5 budget lane: one is the enforcement, the other is the contributor-facing statement.Also from #3717, worth stating in the framing:
AGENTS.mdtells every contributor to validate withscripts/affected-tests.sh --run. At these timings the honest path is "defer to CI", which makes this a documented-contract problem, not only a comfort problem.Generated by Claude Code
fix(session-flow): treat a relocated handoff save-point as a WARN, no…
kyle-sexton commentedon Sep 7, 2026
Additional measurements (2026-09-07, Windows 11, Git Bash,
perf/hook-performance-program)Session: comment-cleanup pass touching 10 files (ruff.toml, plugins/code-tidying/**, one claude-ops script).
scripts/affected-tests.sh --run --explainover that diff ran 40+ minutes with no completion and was killed.--run, takes 54 seconds wall clock (real 0m54.474s,user 0m0.962s,sys 0m3.860s): almost all of it is process-spawn overhead, not CPU.plugins/claude-ops/skills/plugins/scripts/fleet-state.test.sh(2369 lines) exceeds the 600-second Bash tool maximum on its own, so no tool call can ever run it to completion here. It reached 15 PASS before the cap.plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py: a-ksubset of 206 cases takes 65 seconds.Over-selection compounds the cost
The reverse lookup matches on basename, so a change to
plugins/code-tidying/skills/dissolve-comments/reference/safety.mdselected three unrelated suites:Those tests reference the babysit-prs
safety.md, not the code-tidying one. A 10-file diff selected 23 suites. Matching on the repository-relative path, or at least on<skill>/<file>, would cut selection sharply for the common case of same-named reference files across skills.Asks, in addition to the program above
fleet-state.test.shis already past 600 seconds.kyle-sexton commentedon Sep 8, 2026
Same session, one more:
scripts/check-changelog-parity.sh --check-orderalone exceeded the 600-second Bash tool maximum on this host (2026-09-07, Windows 11, Git Bash) and was killed. A combined--checkplus--check-bump mainrun earlier in the session finished, but only when backgrounded past the 180-second mark. Both are pure read-only gates over CHANGELOG.md files; they belong in the same spawn-count audit as the test suites.feat(code-tidying): lift HARD path exclusions through three channels,…
fix(code-tidying): self-parse before triage, grant workflow scripts, …
perf(docs-hygiene): cut apply-rename suite spawns on Windows (#4171)
27 remaining items
kyle-sexton commentedon Oct 14, 2026
kyle-sexton commentedon Oct 19, 2026
work-item-lease reclaimed: lease expired (renewed_at 2026-10-14T00:48:23Z) with no activity.
kyle-sexton commentedon Oct 19, 2026
This was generated by AI during triage.
Held for a person: Parked umbrella by owner decision (2026-09-29, kept 2026-10-13). Criteria 5, #4607 and #4608 shipped; the rest (baseline tables, gate-script and corpus wall-clock targets) runs only as a bounded slice the owner files.
kyle-sexton commentedon Oct 27, 2026
Re-triage. The owner restated on 2026-10-27 when an item stops for them:
The umbrella is parked by the owner's 2026-09-29 decision (#3716 (comment)):
The park was kept on 2026-10-13 (#3716 (comment)). Criterion 5, #4607 and #4608 have shipped.
Decided: the park stands as the owner set it. The umbrella is not worked as one program and is not admitted. The park says progress comes as a bounded piece filed as its own issue. It does not need the owner to sign each one, so
needs-humanis a stop outside the three reasons and comes off. Agents file each remaining piece as its own issue under this umbrella. Criterion 1 (the Windows-host baseline table) is filed as a host-run slice, since it is a measurement on the Windows host. This replaces the r29 classification that keptneeds-human, which predates the 2026-10-27 rule.Target labels: remove
needs-human, addstatus: confirmed. The umbrella stays unadmitted.kyle-sexton commentedon Oct 29, 2026
New measurement for this issue (2026-10-29, Linux/WSL host, local run during PR #10610 work):
scripts/suite-seconds.txt)plugins/planning/surface/test_round.py+test_server.py+test_exporters.pyLocal runs take about twice the recorded CI time for the same three suites. Per the timeout rule, the fix is speeding the suites up (or moving them by
scripts/suite-seconds.txtbalance), never a longer limit. The next step is a profile oftest_server.pyandtest_exporters.pylocally, to see whether a shared fixture (for example, per-test server start-up) dominates.