Repository navigation
fix(browser): run interactive sides in nested Workers - #99
Conversation
|
Two correctness issues to address before merging:
When rebasing onto #95, rebuild the generated runtime and identity pins from the combined Rust sources. |
Browser Engine.interact failed on every dialogue (wasm-oj#98). The runner Worker omitted startupEntropyBytes, and with it added the single-threaded web runtime-core panicked in Condvar::wait as soon as either side read an empty pipe. Each side now runs in its own nested Worker as a standalone metered run. stdin and stdout are SharedArrayBuffer ring buffers whose reads and writes block with Atomics.wait, so WASIX never sees Pending. A side's pipe ends close when its program exits, giving the peer EOF or EPIPE. The result shape, transcripts and terminations match the server's interact. Fixes wasm-oj#98 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With runtime-bundle interactors accepted (wasm-oj#93), run a CPython guessing interactor against a compiled C contestant through the nested-Worker interactive path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
73e16da to
bd3e37b
Compare
|
Rebased onto main after #93 and #95: rebuilt the runtime with |
…eractive sides The pipe reader could report EOF with bytes still buffered: it saw an empty buffer, the writer then published its last bytes and closed, and the reader saw the close. The reader now checks the buffer again after it sees the close. Stdin readiness blocked in Atomics.wait, so a poll_oneoff on stdin and a clock never returned at its deadline. When the deterministic poll probes readiness for a clock subscription, stdin now checks the pipe without blocking. If nothing is readable, the virtual clock advances to the deadline, as on the server. Polls without a clock still block. The runtime and identity pins are rebuilt from the combined sources. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@JacobLinCool Both are fixed in d901464.
I rebuilt the runtime and identity pins from the combined sources with |
|
The |
|
@JacobLinCool Both issues are addressed in d901464. Ready for another review. |
Browser and server verdicts differed for the same program pair in four places: - A write the reader closed partway returned the partial count, so the guest retried the tail and the transcript and output budget counted it twice. It now fails with EPIPE, as the server's pipe does. - Closing fd 1 or fd 0 reached the peer only after the side exited. StreamOutput and StreamInput now close their pipe end when WASIX drops the last descriptor that refers to them, which is when the server drops its PipeTx or PipeRx. - The fixed 64 KiB ring blocked writers that the server's unbounded pipe never blocks, deadlocking batch protocols. Each ring now holds its writer's whole output budget. Only budgeted stdout bytes enter it, so a write never waits. - A poll_oneoff without a clock blocked on empty stdin even when another subscription was ready. Its first stdin readiness check per read subscription now returns Pending, so WASIX reports the ready subscription. Stdin blocks only when WASIX polls again because nothing else was ready.
Both side Workers are now created inside the try that terminates them, so a failure starting the interactor no longer leaves the contestant Worker running, and a side whose start message cannot be posted is terminated at once. A side's error response now says which side failed, as the exception path already did.
Rebuild the web runtime-core with wasm-bindgen 0.2.127 and refresh the runtime identity pins. The refreshed identity changes cost profiles.
Add strict-CSP interactive fixtures for a contestant that closes stdout before it reads the verdict, a batch protocol with 240 KB unread in each direction, and a clockless poll on stdin and stdout before the first query. All three ended in wall-time-limit on the previous runtime.
# Conflicts: # CHANGELOG.md
Fixes #98.
Why
Browser
Engine.interactfails on every real dialogue. The runner Worker omittedstartupEntropyBytes, so runtime-core rejected the request. With the field added, the single-threaded web runtime-core panics inCondvar::waitas soon as either side reads an empty pipe. Repro and root cause are in #98.Change
Each side of a browser interactive session now runs in its own nested Worker as a standalone metered run. Metering, the logical clock, the filesystem quota and the output budget are the same as
run.run/web.rs:runnow delegates toexecute, which takes a stdin file and an optional stdout peer and also returns the raw exit code. The standalone path behaves as before.run/web_interactive.rsruns one side with streaming stdio:poll_readandpoll_read_readycall blocking JS callbacks, so WASIX never seesPending.InteractiveOutput. A closed peer givesBrokenPipe.interact.run_interactive_side.interact_wasm_ojis removed from the web build. Nativeinteractandinteractive.rsare untouched.src/runtime/interactive-pipe.ts: a single-producer, single-consumerSharedArrayBufferring. It tracks EOF and the reader closing, and uses a sequence word so no wake-up is lost. Blocking usesAtomics.wait, which only runs inside the side Workers.src/runtime/interactive-side.worker.ts: initializes runtime-core from the runner's compiledWebAssembly.Module, runs one side, and closes both of its pipe ends as soon as the program exits. The peer then sees EOF, orEPIPEon write.src/runtime/runner.worker.ts:runningonly once both sides execute, so the wall timer starts at the same point as forrun.InteractiveRunResult.startupEntropyByteswith each program.BrowserRunnerterminates the runner Worker, which ends its side Workers. In Chromium, CDP shows both side Workers gone after cancel.runtime-core_bg.wasm(pnpm run runtime:build, wasm-bindgen 0.2.127) and refreshed the three identity pins.docs/architecture.mdnote and a CHANGELOG entry.The public API and result shape do not change. No
unsafe-evalis needed, and Workers stay same-origin.Verification
node scripts/verify-browser-csp.mjs interactiveandWASM_OJ_BROWSER=chromium|firefox|webkit. All use C++ interactors.interactive-ac-cppinteractive-ac-cinteractive-wa-cinteractive-python-contestantinteractive-instruction-limitinstruction-limit; interactor sees EOFinteractive-contestant-exitsinteractive-interactor-exitswritefails withEPIPEinteractive-output-floodoutputLimitBytes(64 KiB):output-limit, transcript exactly 64 KiBinteractive-wall-timeinteractive-cancelengine.cancel()mid-dialogue rejects within ~1 s, then the nextinteractsucceedsinteract.mainshows the same stall: the first compile hung in 1 of 3 runs of these fixtures, so it is not caused by this PR. It looks like the C++ compile stall that fix(runtime): stop waiting for stream EOF when exporting runtime files #96 leaves open.interactonly, warm. Medians of 5 runs after one cold run:iostream+std::endlCold CPython includes the page's one-time runtime-file export. Every CPython session also pays CPython start-up, which is about 2.4e9 metered instructions.
cargo test: 86 passed.cargo clippy --all-targets -- -D warnings(native) andcargo clippy --target wasm32-unknown-unknown --no-default-features --features web -- -D warnings: clean.pnpm run runtime:check-webandcargo fmt --check: clean.pnpm run ci:verify: passes; vitest 961 passed, including the new ring-buffer tests.Risks
SharedArrayBuffer. The runner already refuses to start without cross-origin isolation, so hosts are not affected.poll_oneoffon stdin with a timeout now waits for data instead of timing out. The native path's 1-ns readiness probe depends on timing anyway.runcosts.interactstill uses the host meter, so the same program can cost differently on the two hosts.runtime-core.jsandruntime-core_bg.wasmand changes the pins insrc/core/runtime-identity.ts. Whichever lands second must runpnpm run runtime:buildand refresh the pins; I can rebase this PR after fix(runtime): meter interactive programs in-module like standalone runs #95.run/web.rsmerges cleanly with fix(runtime): meter interactive programs in-module like standalone runs #95.BrowserRunnerstill has norunTrusted/interactTrusted, soengine.judge()cannot run interactive cases or Wasm checkers in the browser. Directinteractandrunwork.main: in WebKit, an emptyfor(;;);loop is not stopped by the instruction meter and runs until the wall limit, in plainrunas well. A loop with a side effect stops atinstruction-limitin about 0.7 s.main: an occasional Firefox compile stall; see Verification.🤖 Generated with Claude Code