Repository navigation
fix(runtime): give loop functions without locals a local so WebKit optimizes them - #103
Open
TakalaWang wants to merge 2 commits into
Open
TakalaWang wants to merge 2 commits into
TakalaWang wants to merge 2 commits into
Conversation
…iling with EPIPE Closes wasm-oj#101. When one side of an interact session exited or closed its stdin, its peer's next write failed with EPIPE on both hosts. A CPython interactor that replied to a contestant that had already exited died with BrokenPipeError and exit 120, and because the stdout wrapper records into the transcript before writing to the pipe, each retried flush added the reply again (five copies). Judges that run both programs natively keep draining each program's output until it exits, so the interactor reads EOF and exits with its own verdict. InteractiveOutput (server) and StreamOutput (browser side Workers) now treat a broken pipe as a successful write: the bytes are already in the transcript and are dropped. Reads from a side that has exited still return the remaining buffered bytes and then EOF. The transcript records each byte a side writes exactly once, up to its output limit, including bytes written after the peer exited, so it depends only on what the writer wrote. Tests cover both hosts: runtime-core unit tests for writes after either side exits and EOF after buffered bytes, server integration tests with C and CPython interactors exiting 42/43 after replying to an exited contestant, and strict-CSP browser fixtures for the same cases plus the reply-before-EOF race. runtime-core_bg.wasm is rebuilt and the runtime identity pins refreshed, which changes cost profiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…timizes them In WebKit an empty C++ for(;;); took about 25 s to reach the default instruction budget (Chromium 1.3 s, Firefox 2.9 s), so it usually hit the wall limit first and was reported as a wall-time TLE. The meter is not optimized away; the trap does arrive, just late. JavaScriptCore (Safari 26, also the macOS 26.6 jsc shell) never enters optimized code inside a loop of a function that has no parameters and no locals. With --verboseOSR the baseline tier logs "Consider OSREntryPlan" about once per iteration (18 million times in 2e8 cost units) although the OMG entry code exists, so every iteration takes the tier-up slow path: about 14 times slower than BBQ alone (--useOMGJIT=false) and 100 times slower than the same loop in a function with one unused local. The forge meter makes no difference; any loop body behaves the same way. The final instrumentation pass (MeterInitializer, which already re-encodes the module to set the budget) now gives every function that contains a loop but has no parameters or locals one unused i32 local. It changes neither behaviour nor cost. WebKit now stops the empty loop at the budget in about 0.2 s; Chromium and Firefox are unchanged. A unit test covers which functions receive the local, and the strict-CSP suite gains empty-loop fixtures for run and interact that must reach instruction-limit before a 15 s wall limit. runtime-core_bg.wasm is rebuilt and the runtime identity pins refreshed, which changes cost profiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TakalaWang
force-pushed
the
fix/webkit-empty-loop
branch
from
October 9, 2026 10:49
7ed82f0 to
9b03522
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #102. Both PRs rebuild
runtime-core_bg.wasmand refresh the runtime identity pins, and those generated files cannot be merged textually, so this branch contains #102's commit. Please review only the last commit,fix(runtime): give loop functions without locals a local so WebKit optimizes them. After #102 merges, I'll rebase this branch and rebuild the runtime.Why
On 0.2.4 in WebKit, an empty C++
for(;;);runs until the wall limit (NOJV measured 17 s interactive and 51 s for a standard run) instead of stopping at the instruction budget like Chromium (about 2.4–4 s) and Firefox (about 2.7–5.2 s). A loop with any side effect stops quickly in WebKit.Root cause
The meter is not optimized away, and the trap is delivered; it just arrives about 20 times later than in Chromium. The instrumented empty loop is
loop (result i32) i64.const 11 call $gas br 0 end. At the default budget of 1e10 that is about 9.1e8 iterations:__original_main, no locals)volatileloop (one local)The difference in WebKit is not the loop body but the function: JavaScriptCore never enters optimized code inside a loop of a function that has no parameters and no locals. Any loop body behaves this way, including one with a memory store. Adding one unused local, or one parameter, fixes it. Evidence from the macOS 26.6
jscshell:--verboseOSR=trueon the no-locals loop (2e8 cost units) logsConsider OSREntryPlan for functionCodeIndex=5 loopIndex#017,991,528 times, roughly once per iteration, with an OSR-entry callee already compiled. The baseline tier's tier-up check calls into the runtime on every iteration and never enters. With one local, it is logged 7 times.--useOMGJIT=false(BBQ only) runs both loops in 170 ms; by default the no-locals loop takes 2400 ms. Failing to tier up makes it 14 times slower than never trying.Minimal standalone repro (plain Wasm and JS, no forge). Save it as
repro.jsand runjsc repro.js(macOS shipsjscat/System/Library/Frameworks/JavaScriptCore.framework/Versions/Current/Helpers/jsc), or paste it into a Safari console:spinspin_with_localjscjsc --useOMGJIT=falseThe 51 s standard run. forge's wall timer is on time in WebKit: a standard run of the empty loop with
wallTimeLimitMs: 3000stops at 3.0 s of execution (4.8 s including compilation), and an interactive session with a 3 s wall stops at 3.0 s. NOJV's sample runs pass nowallTimeLimitMs, so the 60 s default applies, and each sample takes about 25.7 s in WebKit to reach the instruction budget. Two samples come to about 51 s. NOJV should also pass an explicit wall limit for sample runs; there is nothing to fix in forge's watchdog.Change
MeterInitializer, the final instrumentation pass that already re-encodes the whole module to set the budget, now gives every defined function that contains aloopbut has no parameters and no locals one unusedi32local. It adds no instructions, so neither behaviour nor cost changes (call_per_local_costis 0, and the local is added after cost injection anyway). It applies to native and web alike, so both hosts still run the same module.runtime-core_bg.wasmis rebuilt withpnpm run runtime:buildand the identity pins are refreshed, which changes cost profiles: wasm96876c8c…, source rootda89f4ee…, identity07d2d2bf….docs/architecture.md(runtime policy) andCHANGELOG.md.Verification
functions_with_a_loop_and_no_params_or_locals_get_one_unused_local: only the no-params/no-locals function with a loop (including a loop nested in a block) gets the local; functions with a local, a parameter or no loop, and the injected gas function, are unchanged. It fails onmain.empty-loop-budget(standard run) andinteractive-empty-loop-budgetmust reachinstruction-limitbefore a 15 s wall limit.interactive-empty-loop-budgetwall-time-limitat 15.0 sinstruction-limitin 0.23 sempty-loop-budget(incl. compile)wall-time-limitat 17.9 sinstruction-limitin 1.0 sinteractive-instruction-limit(volatile loop)cargo test,cargo fmt --check, clippy native and web with-D warnings,pnpm run runtime:check-web, andpnpm run ci:verify(977 tests) all pass.python-16mb-recursion_error, which also fails onmainon this machine (WebKit 26.5 runs out of native stack first). One earlier WebKit run lost its page late in the suite to a pre-existing JavaScriptCore crash (SIGSEGVinSharedArrayBufferContents::grow←Wasm::Memory::growSharedin a Wasmer SDK Worker).maincrashes the same way in 2 of 3 full WebKit runs on this machine; details in fix(browser): reject promptly when a Worker dies without an error event #104. The rerun completed.Risks
🤖 Generated with Claude Code