Repository navigation
fix(runtime): meter interactive programs in-module like standalone runs - #95
Merged
JacobLinCool merged 3 commits intoOct 7, 2026
Merged
Conversation
Standalone runs inject radix's mutable-global meter, which adds the weighted cost of its own gas function (10 points) to every metered block. Interactive programs use the host-function backend, where radix leaves that cost to the host, and the host charge added nothing. The same tight loop therefore cost up to 11 times less under `interact` than under `run`, and each charge took a mutex inside a ~30 ns host call. With the default 10e9 budget, a CPU-bound contestant ran into the wall deadline instead of its instruction budget. The host meter now charges the in-module gas function cost that radix reports for the same rules, so interactive and standalone costs are equal for identical code, and it keeps its counter in atomics instead of a mutex. The browser runtime is rebuilt and the runtime identity pins refreshed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Interactive programs used radix's host-function meter, so every metered block was a host call. Even with the per-call cost fixed in the previous commit, a tight loop ran about 18 times slower under `interact` than under `run`: C++ `for(;;);` with the default 1e10 budget needed 12.4 s instead of 0.67 s, so a slow host could still hit the wall deadline first. Interactive programs now get the same `instrument_wasm` mutable-global meter as standalone runs, with the budget in the global's initializer. Each process is spawned like `spawn_exec_wasm`, plus a WASIX recycle hook: once the process has exited (return, `proc_exit`, trap or the meter's own budget trap), WASIX hands its store to the hook, which reads the gas counter captured at instance setup and sends it over a oneshot channel. WASIX reports the exit before it recycles the store, so `interact` awaits the exit status and the meter reading together. A negative counter is `instruction-limit` at the full budget, as in `run`. The host meter, its metering-module identities and `charge_gas` are removed. `MeterState` holds the wasmer `Global` on both backends and converts to the JS global only when read, so the browser runtime reads the recycled store the same way. Median of 5 runs through `Engine` with the native runner; costs are unchanged: - C++ `for(;;);`, default budget: run 0.68 s; interact 12.42 s -> 0.68 s - C++ bounded loop, cost 1.76e10: run 0.90 s; interact 5.70 s -> 0.93 s - guess-the-number, 20 rounds: 71 ms -> 64 ms (costs 88423 / 68766) The browser runtime is rebuilt and the runtime identity pins refreshed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 7, 2026
Contributor
Author
|
For context: NOJV (a downstream user) will run checkers and interactors in the browser for its Test button. It needs #93, this PR and #99 (which fixes browser |
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.
Why
Interactive programs were metered differently from standalone runs, and the difference broke time limits:
interactused a host-function meter (instrument_wasm_with_host_meter+charge_gas). It left out that per-block cost, so tight loops cost up to 11× less.run.With the default 1e10 budget, a CPU-bound contestant such as
for(;;);hit the wall deadline (20 s, costnull) instead ofinstruction-limit. Interactive judging therefore could not enforce time limits on CPU-bound contestants. Found downstream in NOJV: an infinite-looping interactive contestant was only stopped by the wall limit, while the same program underEngine.runstopped atinstruction-limitin under a second.Change
Interactive programs now use exactly the same meter as standalone runs.
interactive.rsinstrument_wasm, with the budget in the global's initializer.spawn_meteredspawns each process likespawn_exec_wasm, plus a WASIX recycle hook. Once the process has exited (return,proc_exit, trap, or the meter's own budget trap), WASIX hands its store to the hook. The hook reads the gas counter captured at instance setup and sends it over a oneshot channel.interactawaits the exit status and the meter reading together.instruction-limitat the full budget, as inrun.meter.rs: the host meter, its metering-module identities andcharge_gasare removed.MeterStateholds the wasmerGlobalon both backends and converts to the JS global only when it is read, so the browser runtime reads the recycled store the same way.src/runner/generated/runtime-core_bg.wasm(an LFS object) andruntime-core.jswith the repo build script, and refreshed the runtime identity pins (as in fix(runtime): preserve redirected stdio and consume QuickJS stdin #78). If you would rather regenerate the wasm yourself, the Rust change is self-contained.CHANGELOG.md: entry underUnreleased. This may conflict trivially with feat(runtime): allow Python interactors in interact #93, fix(server): keep a stdin error handler after cancelling a run #94 and fix(runtime): stop waiting for stream EOF when exporting runtime files #96.Interactive costs rise to the standalone values, and the refreshed runtime identity changes cost profiles.
Results
Median of 5 runs through
Enginewith the native runner:runinteractbeforeinteractafterfor(;;);, default budgetinstruction-limitVerification
interactive_contestant_is_metered_like_a_standalone_run: the same module viarunandinteractreports equal cost, and both stop withinstruction-limit. Before the fix: 4003 vs 14013.WASM_OJ_RUN_JUDGE_INTEGRATION=1), 4/4 pass:Engine.runandEngine.interact. Before the fix: 20,000,099 vs 25,000,159.instruction-limitwell before the wall deadline.cargo test, clippy (native and web),runtime:check-web,pnpm run typecheck,pnpm run lint,pnpm testandpnpm run buildall pass.Not in this PR
Found while testing; each could be its own issue:
exitedwith code 45 rather thantrap.failed to start…instead ofinstruction-limit.condvar wait not supportedat process exit.🤖 Generated with Claude Code