Skip to content

Say why a run is slow: per-stage timing, and a JIT check - #263

Merged
adulbrich merged 5 commits into
mainfrom
pipeline-timing
Aug 7, 2026
Merged

Say why a run is slow: per-stage timing, and a JIT check#263
adulbrich merged 5 commits into
mainfrom
pipeline-timing

Conversation

@adulbrich

@adulbrich adulbrich commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

The deployed site ran the reference bracket in 6m32s against 30s for the same build served locally, in the same browser on the same machine. This PR is what found the cause, plus the check that stops it happening to anyone else.

The cause

Microsoft Edge's "Enhance your security on the web" disables the JavaScript JIT for sites it does not consider familiar, and a freshly deployed URL never is. Everything then runs interpreted, WebAssembly included. localhost was unaffected because a site you open every day stays on that feature's allowlist — which is precisely what makes it read as "the deployment is slow".

Confirmed by disabling the setting: full speed returns.

Nothing was wrong with the deployment, the WebAssembly build, the worker, or #260.

Why this needed instrumentation

Four explanations were argued from wall-clock timestamps before anything was measured, and every one was wrong:

hypothesis killed by
hdrgen is slow merges this bracket at native speed (#261)
the wasm download 4.0 MB total; a 4m29s merge needs ~10 KB/s
compilation hdrgen compiles in 3 ms
DevTools attached tested; no change

The first instrumented run ended it in one reading:

Filtering images: kept 13 of 18 in 23.6s
hdrgen took 248.5s: fetch+compile 0.9s, instantiate 0.0s, stage 0.0s, run 247.5s, collect 0.0s

Fetch and compile under a second; execution the entire loss. And the filter stage — which runs no WebAssembly at all — was slow too, which pointed away from wasm entirely. A pure-JS loop then settled it: 2736 ms on the deployed tab against 247 ms locally, on a fresh tab with nothing loaded.

What this adds

Per-tool timing. Each invocation is timed in six parts (fetch/evaluate glue, fetch/compile wasm, instantiate, stage, callMain, collect) and reports itself only when its total passes one second. A healthy run stays silent, so any line at all is worth reading. This is what distinguishes a hosting problem from a build problem, and it cannot be recovered from timestamps afterwards.

The filter stage reports its own duration. It runs no WebAssembly, so whether it slows down alongside everything else separates "wasm is slow" from "everything is slow".

A JIT check at the start of each run. A ~30 ms arithmetic loop; if the engine is far below what a working JIT delivers, the run console says so and names the setting. The threshold errs towards firing: nothing in JavaScript distinguishes a disabled JIT from a slow device, and the message is true either way, so it reports what was measured and offers the usual cause rather than asserting it. 32 ms with a JIT here, 271 ms without.

DEPLOYMENT.md records the whole thing under known limits.

Verification

431 tests, tsc clean, lint clean, full 18-frame browser pipeline passing end to end in 32.6s with all of it in place. The calibration loop costs 32 ms against a run measured in tens of seconds.

🤖 Generated with Claude Code

The deployed site runs the same bracket roughly twelve times slower than the
same build served locally, in the same browser on the same machine, and no
amount of reading could settle why. Wall-clock timestamps in the run console
show that a stage was slow but not what it was doing, and the candidates are
not close relatives: fetching a module and compiling it are the host's problem,
computing is the build's. Three hypotheses have already been argued from those
timestamps and all three were wrong.

Each tool invocation is now timed in six parts -- fetch and evaluate the glue,
fetch and compile the .wasm, instantiate, stage inputs, callMain, collect
outputs -- and reports itself only when the total passes a second. A healthy
run is silent, because a whole 18-frame merge is around fifteen seconds and no
single invocation should approach that. Any line at all is a stage worth
reading.

The filter stage reports its own duration too. It runs no WebAssembly, only
createImageBitmap per frame, so whether it slows down alongside everything else
is what distinguishes a WebAssembly problem from something underneath both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lumilab Ready Ready Preview Aug 7, 2026 9:11am

@adulbrich

Copy link
Copy Markdown
Collaborator Author

This instrumentation did its job on the first run, so recording the outcome here.

The deployed-site log with it enabled:

Filtering images: kept 13 of 18 in 23.6s
hdrgen took 248.5s: fetch+compile 0.9s, instantiate 0.0s, stage 0.0s, run 247.5s, collect 0.0s
ra_xyze took 13.6s: fetch+compile 0.5s, ..., run 13.0s
evalglare took 15.1s: fetch+compile 0.5s, ..., run 14.6s

Fetch and compile cost under a second per module and zero once cached. Execution is the entire loss. That ruled out delivery, Content-Type, cache headers, the serial waterfall, buffer copying, instantiation and worker reuse in one reading, none of which could be separated from wall-clock timestamps before.

The Filtering images … in 23.6s line was the one that pointed away from WebAssembly altogether: that stage runs none, only createImageBitmap per frame, and it was slow too.

A pure-JavaScript loop with no network and no wasm then settled it:

tab ms
deployed 2712
local 247

11x, matching the pipeline's ratios. The tab was getting a fraction of the CPU; the deployment, the WebAssembly build and #260 are all exonerated.

Worth merging on its own merits: four hypotheses were argued from wall-clock timestamps before this existed and every one of them was wrong.

Edge's "Enhance your security on the web" disables the JavaScript JIT for sites
it does not consider familiar, and a freshly deployed URL never is. Everything
then runs interpreted, WebAssembly included. The reference bracket took 6m32s
instead of 30s, with no error, nothing different about the page, and the same
build served from localhost unaffected -- because a site you open every day
stays on that feature's allowlist.

That is indistinguishable from "this tool is slow", and it was read that way
for most of a day. Four explanations were argued from wall-clock timestamps
first: hdrgen being slow, the wasm download, compilation, and DevTools being
attached. All four were wrong. A twenty-millisecond arithmetic loop would have
found it immediately, so the run now starts with one.

The threshold errs towards firing. Nothing in JavaScript distinguishes a
disabled JIT from a slow device, and the message is true either way, so the
wording reports what was measured and offers the usual cause rather than
asserting it. For scale: 32 ms with a JIT here, 271 ms without.

Also records it in DEPLOYMENT.md, next to the other things about this build
that are surprising rather than wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@adulbrich adulbrich changed the title Say where a slow pipeline stage spent its time Say why a run is slow: per-stage timing, and a JIT check Aug 6, 2026
A local build regenerated src-tauri/gen/schemas/ and `git add -A` swept the
result into the timing commit. Nothing here is part of that change, and a
diff that carries unrelated generated files makes a reviewer check whether
they matter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t just Edge

The warning and the deployment note both read as an Edge quirk. It is not one.
Chromium browsers have had a per-site control since 122, Safari does it as part
of Lockdown Mode with no way to separate the two, Firefox does it through
about:config, and Tor Browser does it at "Safer". Managed machines can arrive
there without anyone choosing it, since the CIS benchmark for Edge recommends
disabling JIT at Level 2.

Edge stays the worked example because its rule is what made this so hard to
read: on Balanced it keeps optimisation for sites visited often, which exempts
localhost while catching a newly deployed URL, and that difference looks
exactly like a hosting problem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@adulbrich
adulbrich merged commit e47ff63 into main Aug 7, 2026
12 checks passed
@adulbrich
adulbrich deleted the pipeline-timing branch August 7, 2026 09:22
adulbrich added a commit that referenced this pull request Aug 7, 2026
* feat(pipeline): say where a slow stage spent its time

The deployed site runs the same bracket roughly twelve times slower than the
same build served locally, in the same browser on the same machine, and no
amount of reading could settle why. Wall-clock timestamps in the run console
show that a stage was slow but not what it was doing, and the candidates are
not close relatives: fetching a module and compiling it are the host's problem,
computing is the build's. Three hypotheses have already been argued from those
timestamps and all three were wrong.

Each tool invocation is now timed in six parts -- fetch and evaluate the glue,
fetch and compile the .wasm, instantiate, stage inputs, callMain, collect
outputs -- and reports itself only when the total passes a second. A healthy
run is silent, because a whole 18-frame merge is around fifteen seconds and no
single invocation should approach that. Any line at all is a stage worth
reading.

The filter stage reports its own duration too. It runs no WebAssembly, only
createImageBitmap per frame, so whether it slows down alongside everything else
is what distinguishes a WebAssembly problem from something underneath both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(pipeline): say so when the browser is running without a JIT

Edge's "Enhance your security on the web" disables the JavaScript JIT for sites
it does not consider familiar, and a freshly deployed URL never is. Everything
then runs interpreted, WebAssembly included. The reference bracket took 6m32s
instead of 30s, with no error, nothing different about the page, and the same
build served from localhost unaffected -- because a site you open every day
stays on that feature's allowlist.

That is indistinguishable from "this tool is slow", and it was read that way
for most of a day. Four explanations were argued from wall-clock timestamps
first: hdrgen being slow, the wasm download, compilation, and DevTools being
attached. All four were wrong. A twenty-millisecond arithmetic loop would have
found it immediately, so the run now starts with one.

The threshold errs towards firing. Nothing in JavaScript distinguishes a
disabled JIT from a slow device, and the message is true either way, so the
wording reports what was measured and offers the usual cause rather than
asserting it. For scale: 32 ms with a JIT here, 271 ms without.

Also records it in DEPLOYMENT.md, next to the other things about this build
that are surprising rather than wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: drop generated Tauri schema churn from this branch

A local build regenerated src-tauri/gen/schemas/ and `git add -A` swept the
result into the timing commit. Nothing here is part of that change, and a
diff that carries unrelated generated files makes a reviewer check whether
they matter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(pipeline): name every browser that can turn optimisation off, not just Edge

The warning and the deployment note both read as an Edge quirk. It is not one.
Chromium browsers have had a per-site control since 122, Safari does it as part
of Lockdown Mode with no way to separate the two, Firefox does it through
about:config, and Tor Browser does it at "Safer". Managed machines can arrive
there without anyone choosing it, since the CIS benchmark for Edge recommends
disabling JIT at Level 2.

Edge stays the worked example because its rule is what made this so hard to
read: on Balanced it keeps optimisation for sites visited often, which exempts
localhost while catching a newly deployed URL, and that difference looks
exactly like a hosting problem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <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.

1 participant