Skip to content

feat(browser): download toolchains before the build boundary and expose prefetch - #91

Merged
JacobLinCool merged 2 commits into
wasm-oj:mainfrom
TakalaWang:feat/browser-toolchain-prefetch
Oct 5, 2026
Merged

JacobLinCool merged 2 commits into
wasm-oj:mainfrom
TakalaWang:feat/browser-toolchain-prefetch

Conversation

@TakalaWang

@TakalaWang TakalaWang commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Why

A first browser build downloads its toolchain inside the compiler request, so the download counts against the build boundary (CLANG_BUILD_CONTROL_TIMEOUT_MS = 60 s for C/C++). On a slow or filtered network the 27 MB Clang WebC alone can use the whole boundary. The timeout then recycles the compiler Worker, and nothing is persisted until a download completes, so the next build starts the download over and fails the same way.

We hit this in production during an exam in a computer lab (filtered network, ~50 students starting at once). Students saw Compiler request exceeded the 60000 ms browser boundary. or Failed to fetch for trivially small programs, while server judging of the same code was fine. createBrowserEngine warm-up does not help today: it only starts the Workers and fetches no toolchain bytes.

Change

1. browserToolchainAssetPaths(selection) in @wasm-oj/core (src/core/toolchains.ts). It lists the pinned assets one compile and run fetches per language:

  • C: the Clang WebC and cc1 pins.
  • C++: the same, plus the libc++ PCH manifest and the requested-optimization PCH, only with libcxxPrecompiledHeader: true. This matches findPrecompiledHeader.
  • Rust and Go: package plus manifest; Go also needs the stdlib.
  • Java: the three TeaVM assets.
  • Python: the CPython WebC.
  • JavaScript: QuickJS.
  • TypeScript: the TypeScript compiler plus QuickJS.

2. prefetchBrowserToolchain(sources, { language, target?, optimization?, libcxxPrecompiledHeader?, signal?, onProgress? }) in @wasm-oj/browser (src/runtime/toolchain-prefetch.ts).

  • It validates the profile against the explicit sources.
  • It fetches every asset through browserToolchainAssetUrl. These are the same digest-addressed URLs the compiler, runner and stage Workers request, so the bytes land in the HTTP cache, or in the toolchain cache when registerToolchainCache() controls the page.
  • It reports byte progress and honours signal.
  • It rejects on a non-OK response or a byte-length mismatch. Workers still verify digests before use.

3. BrowserCompiler.build() acquires the toolchain before posting the build request (src/runtime/compiler-client.ts):

  • The download no longer counts against the compiler request timeout. The 60/120/190 s boundaries now cover only the build itself.
  • A Worker recycled by a timeout or cancel() reuses completed downloads from the cache instead of starting over.
  • Each profile is acquired once per compiler: language + target + optimization + admitted PCH. clearToolchainCache() resets this.
  • cancel(), restart() and dispose() abort an in-flight download. A failed download rejects the build without crossing the Worker boundary, and the next build retries.
  • Progress is reported through onProgress as loading-toolchain.

Docs: docs/integration-guide.md has a "Prefetching a toolchain" section. Hosts should serve toolchain assets with a cacheable Cache-Control, since they are immutable per digest; otherwise Workers download them a second time.

Verification

  • New tests:
    • src/core/toolchains.test.ts: per-language selection, the PCH only for C++ at the requested optimization, unknown languages rejected.
    • src/runtime/toolchain-prefetch.test.ts: exact Worker URLs with ?sha256=, progress reaching the total, signal forwarded, truncated and 403 responses rejected, undeclared profiles rejected before any fetch.
    • src/runtime/client-lifecycle.test.ts: no build request and no timeout while a download stalls past the 190 s Rust boundary, then the boundary starts after it; one download per profile; a failed download fails the build without a Worker request and the next build retries; cancel() aborts an in-flight download; the PCH is requested only for C++ projects shipping the exact admitted header.
  • The existing lifecycle tests mock the prefetch module. In the fake-timer Rust timeout test, await Promise.resolve() became await vi.advanceTimersByTimeAsync(0), because the build now awaits the acquisition first.
  • pnpm run typecheck, pnpm run lint (0 warnings), pnpm test (194 files / 956 tests), pnpm run docs:verify, pnpm run library:build, node scripts/verify-library.mjs and node scripts/verify-library-nodenext.mjs all pass.

🤖 Generated with Claude Code

A first browser build downloads its toolchain inside the compiler request,
so on a slow or filtered network the whole C/C++ build boundary (60 s) can
go to the 27 MB Clang download; the timeout then recycles the Worker and the
next build starts the download over.

`browserToolchainAssetPaths` (core) lists the assets one compile and run
fetches per language, and `prefetchBrowserToolchain` (browser) downloads them
through the same digest-addressed URLs the Workers request, with progress,
abort and byte-length checks. A host can start it as soon as it knows the
language; later builds then read the bytes from the HTTP or toolchain cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BrowserCompiler.build now acquires the pinned toolchain assets for the
project's language, target, optimization and admitted libc++ PCH through
prefetchBrowserToolchain before posting the build request, so the download
no longer counts against the compiler request timeout. Completed downloads
live in the HTTP (or toolchain) cache, so a Worker recycled by a timeout or
cancel reuses them instead of starting over. Each profile is acquired once
per compiler; cancel, restart and dispose abort an in-flight download, and
progress is reported as `loading-toolchain`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TakalaWang TakalaWang changed the title feat(browser): prefetch a language's pinned toolchain assets feat(browser): download toolchains before the build boundary and expose prefetch Oct 5, 2026
@JacobLinCool
JacobLinCool merged commit 6b9875a into wasm-oj:main Oct 5, 2026
1 check passed
TakalaWang added a commit to NOJV-TW/NOJV that referenced this pull request Oct 5, 2026
wasm-oj/forge#91 shipped in @wasm-oj/browser 0.2.3: prefetchBrowserToolchain
fetches exactly the assets a build needs at the Workers' URLs, and the compiler
now downloads a build's toolchain before its build boundary. Drop NOJV's own
asset selection and download loop and keep only the per-language dedup,
retries and progress fan-out around the upstream helper (C++ asks for the
admitted libc++ PCH). The lockfile change is limited to @wasm-oj/browser,
core and contracts; toolchain packages stay on 0.2.0 and keep contracts 0.2.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TakalaWang added a commit to NOJV-TW/NOJV that referenced this pull request Oct 5, 2026
* feat(web): preload the browser compiler when the editor opens

A first in-browser Test downloaded its toolchain inside WASM-OJ's 60 s
build boundary, so on a slow exam network it failed with "Failed to fetch"
or a boundary timeout, and every retry started the download over.

- Opening the editor or switching language preloads that language's
  pinned toolchain assets at the Workers' digest-addressed URLs, with
  progress on the Test button; Test waits for the preload and reports a
  failed download (after two retries) without building or pointing to
  Submit.
- C++ sources that include <bits/stdc++.h> (and ship no copy of it) get
  WASM-OJ's admitted libc++ PCH header, cutting warm compiles by ~40%;
  other sources never get forced standard headers.
- Build-boundary timeouts, toolchain download failures and missing
  cross-origin isolation add a localized hint above the engine message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(decisions): link JDG-15's preload and PCH rules to #639

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(web): preload through WASM-OJ 0.2.3's prefetchBrowserToolchain

wasm-oj/forge#91 shipped in @wasm-oj/browser 0.2.3: prefetchBrowserToolchain
fetches exactly the assets a build needs at the Workers' URLs, and the compiler
now downloads a build's toolchain before its build boundary. Drop NOJV's own
asset selection and download loop and keep only the per-language dedup,
retries and progress fan-out around the upstream helper (C++ asks for the
admitted libc++ PCH). The lockfile change is limited to @wasm-oj/browser,
core and contracts; toolchain packages stay on 0.2.0 and keep contracts 0.2.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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