feat: bound compiler environment provisioning separately from compilation - #43
Conversation
…tion Provisioning a uv-managed compiler environment (interpreter selection, downloads, installs) used to run inside the compile budget, so a cold or slow toolchain could exhaust it before compilation started. Each uv environment is now provisioned once per process, before its first compile, under a new --network-timeout (default 300 seconds). The compile budget is now --compiler-timeout (default 120 seconds, unchanged) and covers compilation in an already-provisioned environment. Both options mirror into [tool.vyupgrade]. Provisioning is best effort: a failure is reported by the compile that follows, with its usual typed failure origin.
banteg
left a comment
There was a problem hiding this comment.
Requesting changes for two provisioning-boundary issues:
- The preliminary
uv runwarms caches but cannot provision the environment later used by compilation because these invocations create fresh ephemeral environments. Environment creation and package installation can therefore still consume--compiler-timeout. - The process-global provisioning lock serializes unrelated compiler versions and makes lock wait time fall outside the waiting caller's
--network-timeout.
The rest of the patch validated cleanly: Ruff, 368 focused tests, 899 full tests, wheel smoke, and all GitHub checks passed.
| return | ||
| _PROVISIONED_ENVIRONMENTS.add(environment) | ||
| with suppress(OSError, subprocess.TimeoutExpired): | ||
| subprocess.run( |
There was a problem hiding this comment.
The no-op and subsequent adapter are separate uv run invocations, but both --isolated and --no-project --with create fresh ephemeral environments. I reproduced different sys.prefix values on identical invocations, and project mode installed all packages again. This warms uv caches but does not materialize the environment used by compilation, leaving environment creation and package linking inside compiler_timeout. Please reuse a stable prepared environment for the adapter, or revise the design and guarantees around cache warming and test the real uv environment identity.
| if not _is_uv_run_command(command): | ||
| return | ||
| environment = tuple(_compiler_runner_prefix(command)) | ||
| with _PROVISIONING_LOCK: |
There was a problem hiding this comment.
The process-global lock is held for the entire provisioning subprocess, so unrelated compiler environments cannot provision concurrently. This regresses the thread-pooled corpus runner and lets a caller wait behind another environment longer than its own network_timeout; a deterministic 0.4.1/0.4.3 repro confirmed the serialization. Please coordinate by environment key, using a per-key lock or event so only duplicate provisioning waits.
|
Addressed both review items:
Failed provisioning no longer falls through to a doomed compile; it reports |
banteg
left a comment
There was a problem hiding this comment.
Both prior provisioning findings are addressed at 7f08fe3: the adapter reuses a stable provisioned environment, and per-key locking preserves concurrency while bounding duplicate wait time. Timeout parsing now also rejects non-finite and platform-unrepresentable values. Local validation passed Ruff, 909 tests, and wheel smoke; all current PR checks are green.
co-authored by claude opus 5
Problem
Every compiler subprocess runs under a fixed 120 second budget. That budget also
pays for provisioning the uv environment the compiler runs in: interpreter
selection and download, dependency resolution, and installing
vyper==<version>.On a cold CI runner or a constrained host, provisioning alone can consume the
budget, and the run fails with a
timeoutfailure origin even though nocompilation ever happened.
The two costs have different natures. Provisioning is network- and disk-bound,
happens once per compiler version, and is highly variable. Compilation is
CPU-bound, happens once per file, and is fairly predictable. One budget for both
cannot be tuned sensibly.
Change
Provisioning is now a distinct step. Before the first compile in a given
uv-managed environment,
vyupgraderuns that exact environment once as a no-op(
uv run <same options> python -c ""), which downloads the interpreter ifneeded and materializes the compiler environment. The compile that follows
starts warm.
Two options bound the two phases:
--network-timeout(new, default 300) bounds provisioning: interpreterselection, downloads, and installs. 300 was chosen to cover a cold interpreter
download plus a compiler install on a slow runner while still failing in
bounded time; it is not a per-file cost, so a generous value is cheap.
--compiler-timeout(new, default 120 — unchanged behavior) boundscompilation in an already-provisioned environment, and keeps the existing
30 second adapter grace on top.
Both mirror into
[tool.vyupgrade]asnetwork-timeoutandcompiler-timeout,like every other option. Values must be positive numbers; anything else is a
usage error (exit 4). Defaults are unchanged, so existing runs behave the same
apart from provisioning no longer competing with the compile budget.
Provisioning is memoized per environment, so a run over many files provisions
each compiler version once rather than once per file. Explicit compiler
executables (
--source-vyper/--target-vyper) are not uv-managed and arenever provisioned. Provisioning is best effort: if it fails or times out, the
compile still runs and reports the real error with its usual typed failure
origin, so no new failure mode is introduced.
Interpreter selection for a declared project environment is also provisioning,
so it moves from the compile budget to
--network-timeout.Measurements
Compiling one contract against a cold uv cache and a cold interpreter directory
(
UV_CACHE_DIRandUV_PYTHON_INSTALL_DIRpointed at empty directories),timing each subprocess:
The 2.05s difference in the compile subprocess is toolchain provisioning that
used to be charged to the compile budget. On a slow runner that difference is
what exhausts it.
Provisioning once per version, not once per file — three contracts,
--check:Tests
compile that follows uses the compile budget plus its grace.
[tool.vyupgrade]parsing for both options, command line precedence,unchanged defaults, and usage errors for non-positive or non-numeric values.
compiler_timeouton theconfig instead of patching a module constant.
ruff check, the fullpytestsuite, andscripts/smoke-wheel.shall pass.