Skip to content

ci: investigate hosted test bottlenecks (Linux and Windows) - #4664

Closed
dgageot wants to merge 2 commits into
mainfrom
ci/profile-hosted-tests
Closed

dgageot wants to merge 2 commits into
mainfrom
ci/profile-hosted-tests

Conversation

@dgageot

@dgageot dgageot commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Purpose

Draft investigation only: measure hosted CI, including Windows, before choosing acceleration work. Do not merge the extra profiling matrix as normal CI. No application behavior or test assertions were changed.

The separate, non-gating workflow measures restored versus intentionally empty Go caches, module download, Linux's normal application build, compile-only prewarming, and two -count=1 test passes. It records runner details, timings and raw test JSON. The only normal-task change allows optional arguments to task test-ci; default behavior is unchanged.

Completed runs

Final PR head: c6d9cde2625e638c4cf5e858bfc36d5c1d36282a; actual PR merge checkout: df7ed90262141ac80f94adb6452e2d779d1e8174. The four final lanes tested the same merge checkout. The initial run used an earlier merge checkout; do not treat the two runs as identical source revisions.

Both operating systems reported 4 logical CPUs, approximately 16 GiB memory and Go 1.27.2, with no GOMAXPROCS override. Windows image: win25-vs2026 20261007.264.1. Linux restored/cold runners had different Ubuntu 24 image revisions (20261012.334.1 / 20261018.339.1), another reason not to interpret small differences causally.

Phase measurements

Seconds of wall time from the final run. Setup is the shared Go/Task/cache action; all other columns come from phases.tsv.

Runner / requested cache Setup Module download Application build Compile-only prewarm Uncached suite 1 Uncached suite 2
Linux / restored 61 7 48 145 118 98
Linux / empty 11 9 186 213 136 106
Windows / restored 87 13 — 243 290 238
Windows / empty 27 18 — 449 290 234

“Restored” describes the requested policy, not an exact hit. Logs show Linux restored an older module cache and a 2,306 MiB build cache. Windows restored an older 2,109 MiB build cache but missed its module cache entirely. Empty lanes used unique cache namespaces and had confirmed misses; no shared cache was deleted or written.

Observed setup+download+build+prewarm totals: Linux 261s restored vs 419s empty; Windows 343s restored vs 494s empty. Despite substantial restore overhead, retaining build caching was worthwhile in both samples. These are observations from different VMs, not guaranteed savings.

Compile-only prewarming is not a clean decomposition of normal go test: subsequent invocations still link test binaries, run vet, create temporary files, start processes and run test setup. Linux also builds the application first, matching existing CI. Windows suite measurements use the normal PowerShell → Task launcher. Pass 2 retains tool installations and other runner-local state, but Go result caching is disabled in both passes.

What to accelerate, in priority order

1. Whole CI: Windows compilation and cache effectiveness

Normal final CI took 9m19s for Windows, versus 4m50s Linux, 6m04s cross-compile, and 4m48s lint. Windows's Test step alone took 427s, versus 130s Linux. Windows was the gate's critical path; cross-compile finished 195s earlier in job-duration terms (start times should also be considered).

The controlled profiling lanes show 449s cold vs 243s restored for Windows compile-only, while module downloads cost only 13–18s. Reducing downloads alone is therefore not the principal opportunity. Inspect cache contents/freshness and retention before paying for larger runners or disabling caches: restoring more than 2 GiB already costs about a minute, and Windows's module cache is missing. Cross-compile's build cache also missed in the normal baseline, so it is the likely next gate bottleneck after Windows improves.

Do not claim all 427s are test functions: compilation, linking, vet and setup are included.

2. Best individual test fix: remove unintended gopls installation from TestLoadExamples

Raw output proves these tests run go install and download external dependencies while loading examples:

  • Linux install took 37.9–46.8s; Windows 45.4–48.8s in the final matrix.
  • Four/five example subtests repeatedly account for 24–51s each: coder.yaml, gopher.yaml, modernize-go-tests.yaml, toolset-working-dir.yaml, and sometimes lifecycle.yaml.
  • pkg/teamloader package time fell from 50.7–64.7s to 17.3–18.2s on Linux, and 61.4–66.1s to 15.6–16.7s on Windows on pass 2, when no installation was logged.
  • The installed version even varies between v0.21.0 and latest: concurrent examples share the resolver's command-only singleflight key.

Source chain: pkg/teamloader/teamloader_test.go:104 → real MCP/LSP constructors in registry_test.go → toolinstall.EnsureCommand → pkg/toolinstall/installer.go.

Recommended follow-up: disable auto-install only for the example-loading test (existing DOCKER_AGENT_AUTO_INSTALL=false control), or inject a deterministic resolver, retaining dedicated installer tests and explicit tool-construction coverage. These tests do not start the servers and already permit tool-construction warnings; do not broadly disable installation across all tests.

This is the clearest test-specific waste and a reproducibility problem, but do not add the parallel subtest durations or promise that the whole installation time disappears from the CI gate. Compilation and other packages overlap it. Suite pass 2 was 20–30s faster on Linux and 52–56s on Windows, but also benefited from other warm state; that difference is not a pure installation A/B.

3. Windows filesystem-heavy test: TestGitHubArchiveLimits/file_count

Normal CI measured 42.2s Windows vs 0.36s Linux in the final baseline; the previous baseline measured 42.3s Windows. Under compile-prewarmed profiling it measured 8.7–11.9s Windows vs 0.52–0.69s Linux. The initial Bash diagnostic had an additional 77.5s outlier; exclude that from the canonical estimate.

The test writes 4,096 actual files, then rejects the 4,097th archive entry and cleans up the directory (pkg/skills/github_test.go:425, github_archive.go:172). This is local disk work, not network latency.

Recommended follow-up: separate limit-boundary tests using small, immutable per-instance limits from real extraction integration coverage; preserve the production default and overflow/boundary assertions. Also A/B the test temporary directory location: current Defender exclusions cover the workspace and runner temp, but observed t.TempDir() uses the user's AppData temp. Defender is only a hypothesis; no antivirus A/B was performed. Do not remove security coverage or blindly lower the production limit.

4. Next candidates after the above

  • Windows analysis tests: TestBoundedCommandPipeLifetime 12.6–18.1s on first passes, 5.6–6.0s on second passes; TestSilentLimitedRead 15.1–16.3s and TestChannelDirection 14.1–15.8s on second passes. Investigate shared fixture/package loading rather than cosmetic micro-optimizations.
  • pkg/acp/TestSessionsOwnWorkspaceToolsets: 19.4–21.5s on the Windows second passes; inspect repeated runtime/SQLite setup.
  • TestRAGReferencesShareOneIndex: 6–9s Linux plus Windows overhead; uses a real watcher/debounce and an intentional 3s negative assertion. Prefer deterministic watcher synchronization, not arbitrarily shorter timeouts.

Instrumentation issue found and corrected

The initial Bash-launched Windows diagnostic was not equivalent to normal CI. It consistently failed home-expansion and unreadable-file tests, with intermittent SQLite/port failures; the unchanged normal workflow passed. Git Bash's injected HOME explains the home-expansion failure; the unreadable-file mechanism was not proven. Rather than weaken assertions, the second signed commit restored the normal PowerShell+Task launcher. All eight final suite passes succeeded. Keep the initial run as diagnostic evidence, not as proof of a product regression or reliable timing baseline.

Validation / scope

  • Local task build, task test, task lint passed before the first push.
  • ShellCheck, Bash syntax, actionlint, workflow invariants, reporter tests and the Task argument dry-run passed after the launcher correction.
  • Final hosted normal CI, profiling matrix and CodeQL passed.
  • Both commits are cryptographically signed and DCO-signed; branch is on upstream docker/docker-agent, not a fork.
  • Raw artifacts retained for 14 days; measurements and conclusions are preserved here. Leave this PR draft; land chosen optimizations separately with their own measurements.

Signed-off-by: David Gageot <david.gageot@docker.com>
Signed-off-by: David Gageot <david.gageot@docker.com>
@aheritier aheritier added area/ci CI/CD workflows and pipeline kind/chore Maintenance, deps, CI, tooling (maps to chore: commit prefix) labels Oct 20, 2026
@dgageot

dgageot commented Oct 20, 2026

Copy link
Copy Markdown
Member Author

Implemented the TestLoadExamples finding separately in #4669. Hosted Linux and Windows suites passed with no teamloader installation messages. The four formerly slow example subtests now take 0.13–0.20s on Linux and 0.08–0.15s on Windows. This investigation PR remains draft; its extra profiling workflow is not part of the fix.

@dgageot

dgageot commented Oct 20, 2026

Copy link
Copy Markdown
Member Author

Implemented the archive file-count fixture finding separately in #4670. It preserves the production 4,096-file limit and tests 3 accepted/4 rejected using the real extractor. Hosted Windows and Linux suites passed; TestGitHubArchiveLimits/file_count is now 0.01s on both (previous normal Windows measurements ~42s). This investigation PR remains draft.

@dgageot dgageot closed this Oct 20, 2026
@rumpl
rumpl deleted the ci/profile-hosted-tests branch October 20, 2026 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci CI/CD workflows and pipeline kind/chore Maintenance, deps, CI, tooling (maps to chore: commit prefix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants