Pre-bake cmakeBaseEnv and buildenv arch detection into discovery JSON

Author: mattgodbolt-moltyCreated May 2, 2026Updated Aug 20, 2026
Labelsenhancement

Background

loadPrediscovered() claims to be a fast-path: feed in a JSON, instantiate compilers without re-probing. In practice every compiler still runs through BaseCompiler.initialise(isPrediscovered=true), which keeps doing two things that hit the network/NFS:

  1. getCmakeBaseEnv() (base-compiler.ts:365) — calls utils.fileExists() for ld, ar, and as under toolchainPath. For a compiler with toolchainPath set this is 3 NFS round-trips per compiler. With ~6,000 prediscovered compilers and many having toolchain paths, that's a lot of stat traffic during boot — exactly the workload NFS is slowest at.
  2. buildenvsetup.initialise() (buildenvsetup/base.ts:68) — runs hasSupportForArch() which calls execCompilerCached(...) with --target-help / --help. That's an S3 GET against compiler-info-cache per compiler with buildenvsetup, plus a real exec on cache miss. compilerSupportsX86/compilerSupportsAMD64/compilerArch are the only side effects.

Neither of these depends on anything we can't compute during --discoveryonly. They're being recomputed every boot purely because the results aren't being persisted.

Proposal

Capture both during discovery and restore them in loadPrediscovered, gated on the field being present (so older discovery JSON keeps working).

Step-by-step plan

  1. Profile boot first. Instrument BaseCompiler.initialise() to log time spent in getCmakeBaseEnv() and buildenvsetup.initialise() across all compilers. Run on a real prod-shaped node with prediscovered JSON. Confirm this is actually a meaningful chunk of boot time before optimising it. (If it's not, close this and move on.)

  2. Extend CompilerInfo in types/compiler.interfaces.ts with two optional fields:

    • cachedCmakeBaseEnv?: Record<string, string>
    • cachedBuildEnvSetup?: { compilerSupportsX86: boolean; compilerSupportsAMD64: boolean; compilerArch: string | false; defaultLibCxx: string }
  3. Populate during discovery. In BaseCompiler.initialise() non-prediscovered path:

    • After computing this.cmakeBaseEnv = await this.getCmakeBaseEnv(), store it on this.compiler.cachedCmakeBaseEnv.
    • After buildenvsetup.initialise(), snapshot the resulting compilerSupportsX86/AMD64/compilerArch/defaultLibCxx onto this.compiler.cachedBuildEnvSetup.
  4. Persist via handleDiscoveryOnlyMode (compiler-discovery.ts:113) — these fields are on the compiler object so they'll serialise automatically. Verify with a manual run.

  5. Restore in prediscovered path.

    • In BaseCompiler.initialise(isPrediscovered=true): if cachedCmakeBaseEnv exists, set this.cmakeBaseEnv directly and skip getCmakeBaseEnv().
    • In buildenvsetup.initialise() (or wrap the call site): if cachedBuildEnvSetup exists, populate the four fields directly and skip hasSupportForArch(). Cleanest place is probably a new branch in BaseCompiler.initialise() that takes the cached values and seeds this.buildenvsetup before calling its initialise(), with BuildEnvSetupBase.initialise() made a no-op when arch is already known (it already early-returns when this.compilerArch is set).
    • Strip both fields from the live CompilerInfo after restoration to match current behaviour (similar to how cachedPossibleArguments is deleted).
  6. Backwards compatibility. Discovery JSON without the new fields must still boot — code branches on field presence. No format version needed.

  7. Tests.

    • Unit: loadPrediscovered with cachedCmakeBaseEnv skips fileExists; with cachedBuildEnvSetup skips hasSupportForArch.
    • Unit: discovery output now includes both fields for relevant compilers.
    • Integration sanity: discovery → prediscovered roundtrip yields equivalent compiler state.
  8. Measure. Re-run the profiling from step 1; confirm the saved time matches expectations.

Out of scope

  • Possible later wins (sidecar-per-sqfs, format dedupe, eliminating the runner entirely) are tracked separately.
  • This is purely "stop redoing work we already did during discovery."

Files likely to touch

  • types/compiler.interfaces.ts
  • lib/base-compiler.ts (initialise, getCmakeBaseEnv callers)
  • lib/buildenvsetup/base.ts (early-out path)
  • lib/app/compiler-discovery.ts (no changes if step 3 puts fields on the right object)
  • test/base-compiler-tests.ts and friends

Open questions

  • Are there other quiet probes inside initialise() worth caching at the same time? populatePossibleOverrides() and populatePossibleRuntimeTools() are already skipped for prediscovered, and argParser.parse() likewise — so the audit is mostly about whether buildenvsetup.initialise() has subclasses (ceconan, etc.) that also do per-boot work worth caching.

Source: compiler-explorer/compiler-explorer