Skip to content

runtime/wasm: add bounded Web worker scheduling (based on #116) - #119

Draft
cpunion wants to merge 3 commits into
codex/fork-wasm-cooperative-safepoints-20260801from
codex/fork-wasm-bounded-workers-20260801
Draft

runtime/wasm: add bounded Web worker scheduling (based on #116)#119
cpunion wants to merge 3 commits into
codex/fork-wasm-cooperative-safepoints-20260801from
codex/fork-wasm-bounded-workers-20260801

Conversation

@cpunion

@cpunion cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Depends on #116.

Implements the M1 bounded-Web-worker stage of xgo-dev#2152.

Problem

#116 gives J32/J64/P1 complete single-worker scheduling, timers, roots, and GC, but Web Go work still runs on one host thread. CPU-bound goroutines cannot overlap. Creating one pthread per goroutine would provide parallelism at unbounded thread and stack cost.

Design

  • LLGO_WASM_WORKERS=N opts into a fixed Emscripten pthread pool for 2 <= N <= 16; the default value 1 is inert.
  • Each worker permanently owns one M, one P, one locked run queue, one system context, one poll budget, and one long-lived locality context. Interleaved goroutine entries therefore share the physical worker owner without relying on LIFO return order.
  • A goroutine is assigned round-robin at creation and remains on that owner. getg and runtime_procPin stay O(1), and continuation storage remains owned by wasmcontext.Context.
  • Worker 0 owns host events and deadlines. Cross-worker timer changes wake it through an atomic sequence and Emscripten futex.
  • Channels, semaphores, notify lists, timers, and scheduler waiters use worker-safe synchronization. The Ready-before-Park path keeps the goroutine active until an early notification is resolved, preventing transient false deadlocks.
  • Emscripten supplies main; LLGo exposes __main_argc_argv and runs package initialization and main.main as the schedulable main goroutine.
  • The host shim supports Node and cross-origin-isolated browsers. Browser acceptance is served with COOP/COEP headers.

Build-only configuration and pre-JS selection live in internal/wasmworkers. The runtime's small Emscripten pthread/futex boundary lives in runtime/internal/wasmworkers. Scheduler policy stays in runtime/internal/runtime; existing single-worker files remain selected unchanged unless the internal llgo.wasm_workers tag is appended by the build.

GC boundary

M1 deliberately disables the current single-worker collector when workers are enabled. Explicitly combining llgo_wasm_gc with LLGO_WASM_WORKERS>1 is rejected. #116 remains the default for J32/J64/P1 when the worker setting is absent or 1.

M2 will add allocator synchronization, per-worker root publication, context enumeration, and the stop-the-world request/acknowledgement protocol. Those changes are not part of this PR.

Validation

All local work used GOMAXPROCS=2, GOMEMLIMIT=4GiB, and -p=1; Ubuntu containers were limited to 2 CPUs and 6 GiB.

  • Go 1.26.5 full internal/build, internal/crosscompile, and runtime-module tests pass on macOS.
  • Go 1.24.11 focused compatibility tests pass.
  • J32 and J64 Node stress pass 10/10 each.
  • J32 and J64 Chrome acceptance pass 10/10 each with COOP/COEP after the locality-lifetime update.
  • Each stress run covers two overlapping pthreads, stable G/M/P ownership, 5,000 goroutine lifecycles, 100,000 cross-worker channel handoffs, all configured workers, Mutex, WaitGroup, atomic operations, timer wakeups, and deliberately non-LIFO returns from two pointer-bearing //llgo:gls users per worker. Removing the persistent locality owner makes this regression fail with runtime: local context changed by nested entry.
  • Default single-worker J32, J64, and P1 GC fixtures pass; P1 also passes wasm-tools validate --features all.
  • Ubuntu 24.04/amd64 focused compiler tests and the complete runtime module pass.
  • Fork CI passes 39 checks across Ubuntu/macOS, Go 1.24/1.26, worker/runtime execution, LTO, coverage, release artifacts, and embedded targets; draft release publication is the only expected skip. The one transient Go 1.26 Chrome failure passed on a single-job retry; Go 1.24 and local J32/J64 Chrome 10/10 runs were already green.
  • internal/wasmworkers: 100% statement coverage.
  • configureWasmWorkers: 88.2%; configureWasmGC: 95.8%; worker main-entry generation: 100%.

Performance

Apple M4 Max, J32 -O3, medians of seven rounds:

Benchmark #116 single worker M1, 2 workers
Goroutine lifecycle 297,027 ns/op 8,521 ns/op
Channel round trip 1,130 ns/op 9,767 ns/op
One CPU job 31,087,417 ns/op 32,676,864 ns/op
Two CPU jobs, per job 36,004,666 ns/op 17,444,608 ns/op

These are stage-level measurements, not a scheduler-only comparison: M1 temporarily disables the collector pending M2. Parallel CPU work improves as intended, while cross-worker handoff pays locking and wakeup cost.

For the same benchmark input, the worker host adds 29,191 B of generated J32 JavaScript (234,535 B to 263,726 B). The wasm payload decreases by 126,209 B because the M1 stage excludes the single-worker collector; this is not claimed as a worker scheduler size improvement.

Default-path cost

Against #116 with the same inputs and flags:

  • Representative native Mach-O: 2,402,944 B on both branches, with identical section sizes.
  • Cortex-M4 empty program: byte-identical 1,804 B ELF; text/data/bss remains 140/0/10.
  • Default non-worker J32/J64/P1 scheduler artifacts change by -48/-55/-39 B. The delta comes from the equivalent single-worker source split and absolute source-path strings; no worker symbol is linked.

Review scope

Excluding #116, this PR changes 39 files with +2052/-118. Three commits separate implementation, execution/benchmark/browser acceptance, and the worker-locality lifetime regression.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

a8cd73f09cc2 | workflow run | long-term charts

Program measurements

Platform Workload File size vs main Build vs main Run vs main
Linux cprintf 18816 B +1.5% (worse) 393.593 ms -4.5% (better) 1.318 ms -15.1% (better)
Linux fmtprintf 2219448 B +0.3% (worse) 3.614 s -6.3% (better) 2.457 ms -25.3% (better)
Linux println 71776 B -1.2% (better) 385.879 ms -3.6% (better) 1.707 ms +2.0% (worse)
macOS cprintf 84672 B +0.0% 454.933 ms +9.8% (worse) 3.390 ms -5.5% (better)
macOS fmtprintf 2361968 B +0.7% (worse) 3.263 s -17.2% (better) 19.455 ms -16.8% (better)
macOS println 125712 B -1.0% (better) 440.450 ms +6.2% (worse) 5.835 ms -19.6% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs main
Linux BenchmarkLookupPCRandom 13.250 ns/op -1.7% (better)
Linux BenchmarkMergeCompilerFlags 151.300 ns/op -2.1% (better)
Linux BenchmarkMergeLinkerFlags 96.870 ns/op -4.4% (better)
Linux BenchmarkChannelBuffered 34.280 ns/op -1.4% (better)
Linux BenchmarkChannelHandoff 26648 ns/op -16.8% (better)
Linux BenchmarkDefer 45.240 ns/op -19.7% (better)
Linux BenchmarkDirectCall 1.556 ns/op -0.4% (better)
Linux BenchmarkGlobalRead 1.869 ns/op +19.8% (worse)
Linux BenchmarkGlobalWrite 2.487 ns/op -0.3% (better)
Linux BenchmarkGoroutine 33129 ns/op -56.2% (better)
Linux BenchmarkInterfaceCall 8.113 ns/op +4.1% (worse)
Linux BenchmarkRuntimeGetG 1.868 ns/op -64.8% (better)
macOS BenchmarkLookupPCRandom 12.060 ns/op -3.1% (better)
macOS BenchmarkMergeCompilerFlags 112.300 ns/op -9.8% (better)
macOS BenchmarkMergeLinkerFlags 76.030 ns/op +9.6% (worse)
macOS BenchmarkChannelBuffered 25.460 ns/op -8.2% (better)
macOS BenchmarkChannelHandoff 7960 ns/op +13.6% (worse)
macOS BenchmarkDefer 37.870 ns/op +1.2% (worse)
macOS BenchmarkDirectCall 1.180 ns/op -18.8% (better)
macOS BenchmarkGlobalRead 1.090 ns/op -10.0% (better)
macOS BenchmarkGlobalWrite 1.115 ns/op -25.0% (better)
macOS BenchmarkGoroutine 39328 ns/op +24.2% (worse)
macOS BenchmarkInterfaceCall 5.260 ns/op -2.6% (better)
macOS BenchmarkRuntimeGetG 2.686 ns/op +4.2% (worse)

Compared only with the latest matching platform in the main series.

Warning

  • Persistent publishing is unavailable because no data token is configured.

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