Skip to content

feat: bound compiler environment provisioning separately from compilation - #43

Merged
banteg merged 3 commits into
masterfrom
feat/network-timeout
Aug 23, 2026
Merged

feat: bound compiler environment provisioning separately from compilation#43
banteg merged 3 commits into
masterfrom
feat/network-timeout

Conversation

@charles-cooper

Copy link
Copy Markdown
Member

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 timeout failure origin even though no
compilation 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, vyupgrade runs that exact environment once as a no-op
(uv run <same options> python -c ""), which downloads the interpreter if
needed and materializes the compiler environment. The compile that follows
starts warm.

Two options bound the two phases:

  • --network-timeout (new, default 300) bounds provisioning: interpreter
    selection, 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) bounds
    compilation in an already-provisioned environment, and keeps the existing
    30 second adapter grace on top.

Both mirror into [tool.vyupgrade] as network-timeout and compiler-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 are
never 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_DIR and UV_PYTHON_INSTALL_DIR pointed at empty directories),
timing each subprocess:

provisioning enabled (--network-timeout 300)
  provision   2.33s  (budget 300s)
  compile     1.33s  (budget 150s)

provisioning suppressed (--network-timeout 0.01)
  provision   0.02s  (budget 0.01s)
  compile     3.38s  (budget 150s)

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:

compile: 6
provision vyper==0.4.1: 1
provision vyper==0.4.3: 1

Tests

  • Provisioning runs once per environment, uses the network budget, and the
    compile that follows uses the compile budget plus its grace.
  • A failed provisioning attempt does not block the compile.
  • Explicit (non-uv) compilers are not provisioned.
  • CLI and [tool.vyupgrade] parsing for both options, command line precedence,
    unchanged defaults, and usage errors for non-positive or non-numeric values.
  • The existing real-compiler timeout test now sets compiler_timeout on the
    config instead of patching a module constant.

ruff check, the full pytest suite, and scripts/smoke-wheel.sh all pass.

…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.
@charles-cooper
charles-cooper requested a review from banteg July 28, 2026 19:21

@banteg banteg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for two provisioning-boundary issues:

  1. The preliminary uv run warms 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.
  2. 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.

Comment thread src/vyupgrade/compiler.py Outdated
return
_PROVISIONED_ENVIRONMENTS.add(environment)
with suppress(OSError, subprocess.TimeoutExpired):
subprocess.run(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/vyupgrade/compiler.py Outdated
if not _is_uv_run_command(command):
return
environment = tuple(_compiler_runner_prefix(command))
with _PROVISIONING_LOCK:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@charles-cooper

Copy link
Copy Markdown
Member Author

Addressed both review items:

  • Provisioning now builds a stable per-process uv environment (keyed by the uv options, the project manifest contents, and the dependency context) and the compiler adapter runs that environment's interpreter directly — no ephemeral uv run resolution at compile time, so setup can never eat into the compile budget, and environments are reused across compiles.
  • Locking is per environment key: unrelated compiler versions provision concurrently, and a duplicate caller waits for the first provisioner with the wait bounded by the caller's own --network-timeout (a provisioning timeout is reported as a typed environment failure rather than exceeding the caller's budget).

Failed provisioning no longer falls through to a doomed compile; it reports failure_origin="environment" with the underlying uv error.

@charles-cooper
charles-cooper requested a review from banteg August 23, 2026 15:26

@banteg banteg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@banteg
banteg merged commit 2cc168e into master Aug 23, 2026
3 checks passed
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.

2 participants