Skip to content

[browser][coreclr] Prototype dedicated worker runtime hosting - #136364

Draft
pavelsavara wants to merge 12 commits into
dotnet:mainfrom
pavelsavara:SAB_worker_ST5
Draft

pavelsavara wants to merge 12 commits into
dotnet:mainfrom
pavelsavara:SAB_worker_ST5

Conversation

@pavelsavara

@pavelsavara pavelsavara commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Summary

Prototype dedicated web-worker hosting for Browser/CoreCLR through dotnet.withWorker().create(). Managed execution runs on the worker; synchronous exports still block the UI caller to preserve the ST calling contract.

  • Add worker bootstrap, lifecycle control, startup timeouts, and worker-failure cleanup.
  • Share the worker-owned WASM memory with internal UI views, including view refresh after real allocator-driven growth.
  • Support synchronous scalar UI imports and managed exports using shared WASM argument frames and atomic completion signaling.
  • Reuse the worker's existing binder, primitive converters, native invocation, and managed exception marshaling.
  • Discover exports asynchronously, then return primitive values synchronously from supported exports. Preserve export aliases, normalized assembly caching, signed/default arguments, and microtask ordering.
  • Support nested synchronous export/import/export calls with two-sided pumping, independent call-slot/frame ownership, and per-call error payloads. Reuse free slots for sibling calls while outer calls remain active.
  • Propagate bounded error messages, release worker-owned managed error proxies, reclaim queue slots, and publish worker failure/exit state independently of queued UI error events.
  • Support scalar Task exports and native-Promise UI imports without changing synchronous signatures or their initial execution ordering. Release shared frames after initial invocation and transport eventual completion through typed messages.
  • Keep runtime-pack native memory non-shared; emit bounded non-shared Browser R2R imports and stage app-owned shared native/R2R copies before fingerprinting. Preserve WASI's existing import contract.
  • Add explicit library-test worker mode and UI bootstrap. Permit callbacks throughout runMainAndExit() and preserve stdout/stderr forwarding after the console proxy is installed.
  • Validate signaling protocol version 5 during startup, keep completion IDs within their signed 32-bit wire representation, and add 52 focused regression tests, including real Node worker-thread and harness cases.

Current prototype scope

Surface Implemented
Lifecycle runMain(), runMainAndExit(), exit(), configuration, and build information
UI imports setModuleImports() for one Int32 argument and an Int32 or native-Promise Int32 result; register each module once before managed execution
Managed exports Async getAssemblyExports() discovery followed by synchronous Int32-to-Int32 calls or Int32-to-Task calls; synchronous signatures do not gain Promise wrappers
Export-to-import calls The UI spin loop directly services synchronous import requests while waiting for the export
Nested calls The worker services nested exports while waiting for a UI import; each direction supports 16 in-flight call slots with explicit capacity errors
WASM memory Internal shared views; public heap APIs remain unsupported
Async completion Shared initial invocation preserves synchronous prefixes/throws; typed messages settle pending Promises after shared frames are released
Asset selection Explicit WasmEnableSharedMemory=true stages app-owned shared assets; ordinary apps retain non-shared pack assets
Library test harness WasmTestWithWorker=true selects worker mode; 18 new cross-realm scalar cases pass in worker and ordinary Chrome

Startup, lifecycle command parameters, registration metadata, and export discovery use messages. Synchronous interop requests, argument frames, and completion signals use shared memory. Managed execution and allocation remain worker-owned.

Draft scope and limitations

This remains an incomplete prototype, not a merge-ready feature:

  • Nested calls are bounded to 16 in-flight slots per direction. Capacity exhaustion fails explicitly, preserves outer call ownership, and permits recovery; this is not an unlimited-depth bridge.
  • Async support is limited to scalar Int32 Tasks/native Promises. Custom thenables and broader marshaling types remain unsupported. Inline-completing custom thenables also reproduced a failure in the existing non-worker ST Task holder; this prototype rejects them rather than changing ST marshaling.
  • Diagnostics, public heap APIs, and the full RuntimeAPI surface are not available on the proxy.
  • Exceptions cross as bounded message snapshots, not original cross-realm objects with full stack/identity semantics. Messages exceeding 4096 UTF-16 code units produce an explicit transport-limit error.
  • Shared asset staging is opt-in and does not mutate runtime-pack sources. Packaged-SDK build/publish and native relink mode transitions are tested; shared composite R2R and installed-workload scenarios still need end-to-end coverage. Arbitrary concurrent UI heap access is not established safe.
  • Ordinary non-worker Chrome startup now passes without COOP/COEP headers.
  • The full existing System.Runtime.InteropServices.JavaScript.Tests suite has not passed in worker mode. Only the new 18-case scalar subset in that assembly is verified; broader types, existing helper migration, object identity, callbacks and GC/lifetime stress remain.
  • Application-owned worker bootstrap, CSP worker-src, resource-loader/configuration callbacks and wider browser-engine coverage remain merge-readiness work. Fingerprinted loader startup is covered by the targeted packaged-SDK tests, not every hosting combination.
  • The public JavaScript builder API still needs API/design approval.
  • Mono, the Node.js console sample, other browser engines, and the full Wasm.Build.Tests suite were not run.

Validation

Local validation on Windows with Debug Browser/CoreCLR, through 2026-10-27. Latest runtime build, 52 Node tests, TypeScript and targeted lint passed. XHarness verified 18/18 scalar library tests in worker mode and 18/18 on the ordinary runtime, with zero skipped cases.

  • Passed, zero warnings/errors:
    .\build.cmd -bl -os browser -subset clr.runtime+clr.corelib+libs.native+host.native+libs.pretest -c Debug /p:RuntimeFlavor=CoreCLR
  • Passed, zero warnings/errors:
    .\dotnet.cmd build -bl:artifacts\worker-m6-final-shared-sample.binlog /p:TargetOS=browser /p:TargetArchitecture=wasm /p:Configuration=Debug /p:RuntimeFlavor=CoreCLR /p:WasmEnableSharedMemory=true /t:BuildSampleInTree .\src\mono\sample\wasm\browser
  • From src\native, passed all 52 tests:
    npm run test:worker
  • From src\native, passed:
    .\node_modules\.bin\tsc.cmd --noEmit -p tsconfig.json
    .\node_modules\.bin\eslint.cmd libs\Common\JavaScript\loader\worker-promises.ts libs\Common\JavaScript\loader\worker-imports.ts libs\Common\JavaScript\loader\worker-exports.ts libs\Common\JavaScript\loader\worker-runtime.ts libs\System.Runtime.InteropServices.JavaScript.Native\interop\invoke-cs.ts libs\System.Runtime.InteropServices.JavaScript.Native\interop\invoke-js.ts tests\worker-transport-worker.mjs
  • Passed git diff --check before committing.

Opt-in assets and library harness

  • Passed, full runtime/pack build, zero errors and four existing NU1902/NU1903 warnings from the packaging-test System.Security.Cryptography.Pkcs 8.0.1 dependency:
    .\build.cmd -bl -os browser -subset clr+libs+host+packs -c Debug /p:RuntimeFlavor=CoreCLR
  • Passed, 44 Webcil/parser/staging tests, zero failures/skips:
    .\dotnet.cmd build -bl:artifacts\worker-m6-parser-tests.binlog /p:TargetOS=browser /p:TargetArchitecture=wasm /p:RuntimeFlavor=CoreCLR /p:Configuration=Debug /p:InstallWorkloadForTesting=false /p:InstallChromeForTests=false /p:XUnitClassName=Wasm.Build.Tests.WebcilInWasmSizesTests /t:Test .\src\mono\wasm\Wasm.Build.Tests
  • Passed, both packaged-SDK browser theories: prebuilt native and native relink, each switching shared -> ordinary -> shared, verifying actual asset SHA-256 integrity and 70 async calls. These use the constructed SDK with TestUsingWorkloads=false, not the installed-workload path:
    $env:EMSDK_PATH='D:\.wasm-tools\emscripten\6.0.2-windows-x64'
    .\dotnet.cmd build -bl:artifacts\worker-m6-browser-tests.binlog /p:TargetOS=browser /p:TargetArchitecture=wasm /p:RuntimeFlavor=CoreCLR /p:Configuration=Debug /p:TestUsingWorkloads=false /p:InstallWorkloadForTesting=false /p:InstallChromeForTests=false /p:XUnitMethodName=Wasm.Build.Tests.BuildPublishTests.SharedMemoryIsAppOwnedAndOptIn /t:Test .\src\mono\wasm\Wasm.Build.Tests
  • Passed, XHarness library scalar subset: 18/18 with worker mode, 18/18 ordinary, zero failures/skips. Both runs wrote RESULTXML, exited 0 and ended with zero GCHandles/JSHandles. Used the matching cached Chrome/ChromeDriver pair; disabled unrelated SDK hot-reload tools to avoid conflicting assets from the HTTP test servers:
    $env:CHROME_PATH_FOR_TESTS='D:\.wasm-tools\chrome\151.0.7922.77-1654411-windows-x64\chrome-win\chrome.exe'
    $env:PATH='D:\.wasm-tools\chromedriver\151.0.7922.77-1654411-windows-x64\chromedriver_win32;'+$env:PATH
    .\dotnet.cmd build -bl:artifacts\worker-suite-chrome.binlog /nr:false /p:TargetOS=browser /p:TargetArchitecture=wasm /p:Configuration=Debug /p:RuntimeFlavor=CoreCLR /t:Test /p:Scenario=WasmTestOnChrome /p:InstallChromeForTests=false /p:EnableHotReloadInRuntimeConfigDevFile=false /p:WasmTestWithWorker=true /p:XUnitClassName=System.Runtime.InteropServices.JavaScript.Tests.WorkerInteropTest .\src\libraries\System.Runtime.InteropServices.JavaScript\tests\System.Runtime.InteropServices.JavaScript.UnitTests\System.Runtime.InteropServices.JavaScript.Tests.csproj
    .\dotnet.cmd build -bl:artifacts\worker-suite-ordinary.binlog /nr:false /p:TargetOS=browser /p:TargetArchitecture=wasm /p:Configuration=Debug /p:RuntimeFlavor=CoreCLR /t:Test /p:Scenario=WasmTestOnChrome /p:InstallChromeForTests=false /p:EnableHotReloadInRuntimeConfigDevFile=false /p:WasmTestWithWorker=false /p:XUnitClassName=System.Runtime.InteropServices.JavaScript.Tests.WorkerInteropTest .\src\libraries\System.Runtime.InteropServices.JavaScript\tests\System.Runtime.InteropServices.JavaScript.UnitTests\System.Runtime.InteropServices.JavaScript.Tests.csproj
    These are new scalar cross-realm tests in the library assembly, not a passing full-suite result.
  • Not run: full worker JS interop suite, shared composite R2R browser scenario, installed-workload mode, Node console smoke, other browser engines and Mono. Node smoke was blocked by execution approval.

Chrome / Playwright

Worker sample tests used a separately running server with COOP/COEP headers and browser caching disabled. Ordinary compatibility was also verified on a server without isolation headers.

  • Passed synchronized bidirectional WASM heap access before and after native malloc grew memory from 70,385,664 to 136,773,632 bytes. Old data remained intact, UI views refreshed, and scratch allocations were freed on success and intentional failure.
  • Passed 89 synchronous exports and 70 imports in an instrumented run. Both queues ended empty, all 16 slots in each direction were IDLE, and all preallocated frame pointers were unchanged.
  • Passed primitive return values, signed/default arguments, alias/cache identity, DOM result 42, managed exceptions, UI exceptions, recovery after errors, and ST null/undefined result behavior.
  • Passed microtask ordering: before,import,after until the synchronous export returned, then before,import,after,microtask after yielding. The spin loop did not pump arbitrary Promise continuations.
  • Passed 16 managed exception calls with tracked proxy counts unchanged before/after, confirming managed error proxy cleanup.
  • Passed repeated depth-4 native C# chains, 65 sibling exports within a single outer import, inner-error propagation and recovery, and both import/export capacity limits.
  • Passed a supported 16-call chain with 16 distinct frames per direction and intact argument values at every depth. The instrumented M4 session completed 274 exports and 276 imports with empty queues, all call slots IDLE, and unchanged frame pointers.
  • Passed root runMain -> import -> export nesting. Import-side capacity failure was caught by managed code, followed by a successful nested call; both queues ended at head/tail 21 with all slots reclaimed.
  • Passed nested ordering: before,import:3,import:2,import:1,import:0,after, then microtasks 3,2,1,0 only after yielding, matching the non-worker ST sample.
  • Passed worker JS failure propagation while the UI held a synchronous call loop: shared ERROR state surfaced the original message without depending on a queued UI error event.
  • Passed startup-timeout cleanup, stale signaling-protocol rejection, runMain keepalive, explicit shutdown, and runMainAndExit worker termination.
  • Passed the same scalar export/import, DOM, exception, microtask, and exit-code checks without withWorker(), creating no worker.
  • Passed M5 completed/deferred Task results and faults, synchronous prefix throws, null/undefined Tasks, native-Promise UI imports, nested synchronous callbacks, and 70 concurrent calls in both worker and non-worker Chrome.
  • Passed M5 a separate native worker check holding 70 imported Tasks unresolved simultaneously: all 16 import and 16 export slots were IDLE with empty queues before release; reverse-order completion produced the exact expected results.
  • Passed M5 exit with a native imported Task still pending: its UI export Promise rejected with the exit error, the worker closed, and a late UI completion was ignored without uncaught page errors.
  • Passed M6 immutable native/CoreLib R2R pack hashes across per-app mode changes, ordinary no-header startup, and matching fingerprints/integrity after app-owned shared staging.

Selected regression cases were also verified failing before their fixes, including missing heap sharing, scalar import/export support, completion/exit races, missing nested-call support, completion-before-wait behavior, signed completion-ID rollover, stale-protocol acceptance, and the M5 Task-prefix case against the published M4 export client. Nine formatting findings from an exploratory lint run on the pre-existing .mjs test runner were confirmed present in the baseline and left outside these changes.

Note

This pull request description and portions of the code were generated with GitHub Copilot.

pavelsavara and others added 4 commits September 30, 2026 17:37
Forward entry assembly names and arguments, distinguish runMain from
runMainAndExit, and wait for runtime shutdown before terminating workers.
Correlate command completions to avoid races between Main and exit.

Clean up workers after startup failures and timeouts, reject pending commands
on worker failure, and add focused lifecycle regression tests. Update the
browser sample and proxy API documentation, and move development notes out
of the repository.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 4 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@pavelsavara pavelsavara added NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) NO-REVIEW Experimental/testing PR, do NOT review it arch-wasm WebAssembly architecture area-System.Runtime.InteropServices.JavaScript os-browser Browser variant of arch-wasm labels Oct 23, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

Include the runtime-owned shared memory in the ready handshake and initialize
internal UI views without enabling public heap APIs. Reject missing or
non-shared memory during startup.

Add regression coverage for shared heap round trips, growth, invalid
handshakes, and scratch cleanup. Validate real allocator growth and
bidirectional access in the browser.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Register UI functions through metadata and route synchronous Int32 imports
through shared WASM argument frames. Reuse the worker's existing binder,
result converters, and managed exception marshaling.

Service imports while managed execution runs on the worker, reclaim ring
slots after completion, and preserve ST null/undefined result behavior.
Exercise DOM calls, JSException recovery, and queue wraparound in the sample
and add focused transport regression coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Discover exports asynchronously, then dispatch Int32 calls through shared
frames while the UI services synchronous imports. Reuse the worker's native
binder and preserve primitive returns, aliases, and microtask ordering.

Reclaim frames after failures, dispose managed error proxies, and publish
worker failure state independently of queued UI error events. Reject stale
signaling protocols and unsupported nested calls explicitly.

Add regression coverage and exercise export/import calls in the browser
sample without converting synchronous exports into Promise-returning APIs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pavelsavara and others added 3 commits October 26, 2026 10:47
Pump nested exports while a worker import is waiting. Separate dispatch
queues from active call slots so outer frames remain owned while nested
and sibling calls execute, and retain independent per-call error payloads.

Preserve ST microtask ordering, report capacity exhaustion explicitly, and
keep completion IDs within the signed 32-bit wire representation.

Add real worker-thread regression coverage, nested C# browser scenarios,
and recovery checks for both import and export capacity limits.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserve synchronous call prefixes while routing Task completion through
messages. Release shared frames after initial invocation, retain native
Task ownership, and reject pending operations on worker exit or failure.

Support Int32 Task exports and native-Promise UI imports. Reject custom
thenables explicitly rather than permitting unsafe inline completion.
Cover concurrent calls, lifecycle cleanup, and completion-ID rollover.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep runtime-pack memory non-shared and emit bounded Browser R2R memory
imports, preserving the existing WASI contract. Stage app-owned shared
native and R2R copies before fingerprinting rather than modifying the pack.

Replace the unsafe memory patcher with bounded parsing and explicit errors.
Cover source preservation, incremental memory-mode changes, asset integrity,
and packaged SDK build/publish/native-relink startup in Chrome.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add an explicit worker mode and UI bootstrap to the library test harness.
Exercise realm identity, synchronous nesting, scalar Task calls, concurrency,
and error recovery with the same assertions on the ordinary runtime.

Allow nested exports while runMainAndExit is executing managed Main, and
preserve stdout forwarding when diagnostics installs its console proxy.
Verify XHarness receives results and finishes worker tests through the UI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend synchronous and Task calls to Int32/String argument and result
combinations. Keep native marshaling and transient UTF16 allocation on the
runtime worker, using explicit allocation and release handshakes for UI copies.

Validate range and ownership, preserve native converter behavior, refresh
views after native memory growth, and acknowledge allocation failures before
reclaiming slots. Cover strings, nesting, concurrent Tasks, and cleanup in
transport tests and the worker/ordinary library subset.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-Host area-System.Runtime.InteropServices.JavaScript NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) NO-REVIEW Experimental/testing PR, do NOT review it os-browser Browser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant