Skip to content

Precompile the test environment inside the serialized window - #66

Merged
davidanthoff merged 1 commit into
mainfrom
precompile-test-env
Aug 19, 2026
Merged

Precompile the test environment inside the serialized window#66
davidanthoff merged 1 commit into
mainfrom
precompile-test-env

Conversation

@davidanthoff

Copy link
Copy Markdown
Member

The controller nominates one test process to activate the environment and parks every other one in ProcessWaitingForPrecompile until it reports back. The point of that gate is to build the test environment's precompile caches exactly once, because Julia has no cache file locking before 1.10 (mkpidlock_hook shows up in loading.jl in 1.10 and is absent in 1.9) — two processes precompiling the same package race, and on Windows the loser cannot replace a .ji the winner holds open, so it dies with Cannot write cache file.

The gap

Whether that window actually builds anything turns out to depend entirely on the vendored TestEnv shim:

shim precompiles the sandbox?
julia-1.9, -1.11, -1.12, -1.13 yesPkg._auto_precompile(temp_ctx; already_instantiated=true), under the comment "Now that we have set up the sandbox environment, precompile all its packages"
julia-1.0julia-1.8 noactivate ends at Pkg.activate(outer_tmp), and Julia 1.0's Pkg.instantiate ends at Operations.build_versions

So on Julia 1.9+ the gate does what it was built to do. On Julia 1.0–1.8 the nominated process finished activation without writing a single .ji, all peers were released, and each one then evaluated using <Package> at the same time — whoever won the race decided whether the run passed.

This surfaced in julia-vscode/DebugAdapter.jl#122: that package had exactly one test item, so one process started and nothing raced. Adding two more made three start together, all loading JuliaInterpreter, and Windows + Julia 1.0.5 failed with Cannot write cache file. The stack put it in run_testitem, not in activation, which is the tell.

The fix

One version-guarded call, inside the window the gate already serializes. Pkg.API.precompile() rather than Pkg.precompile() — the latter does not exist before Julia 1.4, and before that the name resolves to Base.precompile through the implicit using Base and throws MethodError: no method matching precompile(). I hit exactly that on the first attempt.

A failure there is swallowed to @debug on purpose: whatever is wrong comes back with a much better message when a test item loads the package, and it should not turn into a failed activation.

Verification

A race is a bad thing to verify by "CI went green", so I verified the mechanism instead, with a fixture whose only test item is default_imports=false — nothing in the item ever loads the package or its dependency, so whether their cache files exist after a run says exactly one thing: whether activation built them.

On Julia 1.0, with ~/.julia/compiled/v1.0 cleared each time:

  • released TestItemControllers 1.6.0 → run passes, neither the package nor its dependency has a cache file;
  • this branch → run passes, both do.

Also on Julia 1.0: DebugAdapter's three test items now run clean from a cold cache, and TIC's own suite is 201/201 on 1.12 — including the test_julia_versions.jl items, which drive real Julia 1.0/1.1/1.5/1.9/1.10 test processes through this code path.

Supersedes the workaround I first proposed on DebugAdapter #122 (capping its CI to one worker), which treated the symptom.

🤖 Generated with Claude Code

The controller nominates one test process to activate the environment while the
others wait, and releases them when it reports back. The point is to build the
test environment's precompile caches exactly once, because Julia has no cache
file locking before 1.10: two processes precompiling the same package race, and
on Windows the loser cannot replace a `.ji` the winner holds open, so it dies
with "Cannot write cache file".

From Julia 1.9 on that works, because `TestEnv.activate` finishes with
`Pkg._auto_precompile` on the sandbox environment. The older TestEnv variants stop
at `Pkg.activate`, and `Pkg.instantiate` did not precompile before Julia 1.6
either — so on Julia 1.0 to 1.8 activation built nothing, every peer was released,
and each one then evaluated `using <Package>` at the same time. Whoever won
decided whether the run passed.

Precompiling here puts that work back inside the window the gate already
serializes. `Pkg.API.precompile()` rather than `Pkg.precompile()`: the latter
does not exist before Julia 1.4, where the name resolves to `Base.precompile`
through the implicit `using Base` and throws a MethodError.

Verified with a fixture whose only test item has `default_imports=false`, so
nothing in the item loads the package: on Julia 1.0 the package and its
dependency have no cache file after a run today, and both do with this change.
davidanthoff added a commit to julia-vscode/DebugAdapter.jl that referenced this pull request Aug 19, 2026
The Windows + Julia 1.0 failure was not this package's to fix. TestItemControllers
nominates one test process to activate the environment and holds the others back
until it reports, so that the test environment is precompiled exactly once — but
on Julia 1.0 to 1.8 activation built nothing, because those TestEnv variants stop
at `Pkg.activate` and only 1.9 and later finish with `Pkg._auto_precompile`. Every
process therefore reached its first `using` at the same time, with no cache file
locking in Base before 1.10 to protect them.

julia-testitems/TestItemControllers.jl#66 closes that, so capping the workers here
is unnecessary.
@davidanthoff
davidanthoff merged commit 4c30628 into main Aug 19, 2026
10 of 12 checks passed
@davidanthoff
davidanthoff deleted the precompile-test-env branch August 19, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant