Skip to content

runtime/wasm: add WASI single-worker scheduler (based on #2192) - #2197

Open
cpunion wants to merge 18 commits into
xgo-dev:mainfrom
cpunion:codex/wasm-wasi-single-worker
Open

runtime/wasm: add WASI single-worker scheduler (based on #2192)#2197
cpunion wants to merge 18 commits into
xgo-dev:mainfrom
cpunion:codex/wasm-wasi-single-worker

Conversation

@cpunion

@cpunion cpunion commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Depends on #2192.

Part of #2152.

Problem

GOOS=wasip1 GOARCH=wasm llgo build can produce a module after #2192, but goroutines still select the pthread runtime. The resulting executable imports shared host memory and WASI pthread facilities, so ordinary single-worker Wasmtime execution cannot run Go scheduler behavior.

The continuation implementation also needs a clear ownership boundary. Scheduler code should not know whether a backend uses Emscripten Fiber, a raw Asyncify save area, a future resumable ABI, or standardized stack switching.

Implementation

  • Make WASI pthread scheduling an explicit LLGO_WASI_THREADS=1 compatibility mode.
  • Add a raw wasm32 execution-context backend and a one-M/one-P FIFO scheduler for default WASI Preview1 builds.
  • Run package initialization and main.main as the first schedulable task while keeping the host entry on the system stack.
  • Put backend-specific stack and Asyncify storage ownership in runtime/internal/wasmcontext.
  • Let the runtime supply root-aware allocate/free callbacks only during cold context creation and destruction. The callbacks are not retained per G; Swap, Resume, and Suspend remain direct concrete hot-path calls.
  • Keep queue ownership and scheduling policy in runtime/internal/runtime; the continuation package does not inspect G/M/P state.
  • Declare Asyncify as a typed target capability; executable linking writes a temporary core module, runs pinned Binaryen instrumentation, and atomically publishes the final module. Archives and shared libraries are unchanged.
  • Compile LLVM 19 SjLj with legacy wasm EH, apply Asyncify, then translate the final module to standardized exnref EH.
  • Keep the existing JS wasm scheduler behavior behind the same internal context boundary.
  • Preserve the historical pthread source selection and fatal behavior when LLGO_WASI_THREADS=1 is set.
  • Release partially allocated context storage plus the new G argument and runtime context when goroutine setup fails.
  • Use the static wasm defer continuation dispatch already merged through ssa/wasm: use selectors for Rund and Reth continuations #2208; without it, LLVM 19 crashes while selecting runtime.EnsureLocalInitializer after compiler/runtime: add //llgo:tls and //llgo:gls package variables #2079.

The implementation is intentionally limited to the scheduler/context foundation. Channel blocking, timers and host async integration, wasm GC integration, root/safepoint work, and optional WASI Preview2 support remain follow-up work tracked by #2152.

Validation

All local commands use GOMAXPROCS=2, GOMEMLIMIT=4GiB, and -p=1 where applicable.

  • Rebased on current runtime/wasm: add single-worker Asyncify scheduler #2192 head 6e2f57576, which includes upstream main e82e95fbe; package init functions are preserved in dependency order inside the schedulable main task.
  • macOS arm64, Go 1.26.5: J32, J64, and P1 scheduler fixtures pass, including 5,000 serial goroutine lifecycles, park deadlock, and main runtime.Goexit() lifecycle/deadlock behavior.
  • macOS: default P1 and explicit LLGO_WASI_THREADS=1 both build; default P1 validates and runs under Wasmtime 39.0.1.
  • macOS: P1 scheduler execution passes at -O0, -O3, and ThinLTO.
  • macOS: focused build/crosscompile/SSA tests and native getg/defer/locality/Goexit tests pass.
  • runtime/internal/wasmcontext and runtime/internal/runqueue each have 100% statement coverage; the changed wasm post-link and static defer dispatch paths are covered by focused tests.
  • Ubuntu 24.04 amd64 container, limited to 2 CPUs and 6 GiB RAM with swap disabled: focused wasm post-link, target selection, static defer dispatch, wasmcontext, runqueue, Emscripten fiber, and runtime package tests pass under Go 1.26.5 and LLVM 19.
  • Earlier branch validation also covers post-link validation/execution, no default pthread/shared-memory imports, the external-module _demo/c/helloc path, and the full Ubuntu/macOS CI matrix.
  • Full LTO still hits the existing LLVM 19 SelectionDAG crash in runtime.printany; the same failure reproduces on the unmodified runtime/wasm: add single-worker Asyncify scheduler #2192 base.

The lifecycle abstraction changes final scheduler modules by +2,991 bytes for J32 and +1,567 bytes for J64 against the same pre-lifecycle #2192/W tree. Binaryen reports six additional functions, two table entries, and seven indirect calls from the cold allocation callbacks. This is artifact size, not per-G memory; the existing 64 KiB stack and 64 KiB Asyncify reservations are unchanged. The measured P1 +172-byte integration delta uses an older W baseline and is not presented as a pure lifecycle comparison.

Independent diff over the current #2192 head: 33 files, +1,453/-113. The branch is linear: current #2192, then nine original #2197 commits plus one post-rebase scheduler-lifecycle integration commit. Current head: 51ad21aee. No previously runnable test is skipped or ignored.

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.57534% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/build/wasm_postlink.go 95.23% 2 Missing and 1 partial ⚠️
internal/build/build.go 85.71% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@cpunion
cpunion force-pushed the codex/wasm-wasi-single-worker branch from 24020c3 to e3e8ea8 Compare July 28, 2026 14:12
@cpunion cpunion changed the title runtime/wasm: add WASI single-worker scheduler (based on #2192) runtime/wasm: add WASI single-worker scheduler (based on #2192, #2208) Jul 29, 2026
@cpunion
cpunion force-pushed the codex/wasm-wasi-single-worker branch from 4cbebe3 to 4856fa7 Compare August 3, 2026 05:08
@cpunion
cpunion marked this pull request as ready for review August 3, 2026 05:09

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

This is a large, well-structured PR that adds single-worker WASM/WASI (Asyncify) scheduling to the LLGo runtime. The backends are cleanly separated by build tags, the intrusive run queue is allocation-free and well-tested, and wasm_postlink has strong test coverage including failure paths. No correctness-breaking bugs were found in the scheduler core, run queue, Asyncify ABI wiring, or eh.go. I did not find any blocking security issues — postLinkWasm builds argv directly (no shell) and writes atomically; the context_wasm.S offsets correctly match the Context struct layout.

Findings below are mostly minor consistency/robustness and doc-accuracy items; the most actionable is the per-goroutine memory floor. Inline comments cover the concrete diff-line findings.

Additional notes (no reliable single inline location):

  • WASI yield cost (design-inherent, informational): Unlike the js backend (direct fiber Swap), the wasip1 backend routes every switch through the RunWasmMain dispatch loop via Asyncify unwind + rewind (proc_wasip1.go, context_wasip1.go). That cost is O(active frame depth) and paid twice per switch, so Gosched on a deep stack is not cheap — runtime code should prefer park/ready handoff over spin-yielding on this backend. Largely inherent to the single-worker Asyncify design; flagging for awareness.
  • CI $module interpolation (low): .github/workflows/llgo.yml interpolates $module into an inline node -e "import Module from '$module'; ...". The value is a CI-controlled path under $RUNNER_TEMP, so it's not an injection vector today; worth a note only if these helpers are ever reused with externally-influenced paths.

Comment thread runtime/internal/wasmcontext/storage.go
Comment thread runtime/internal/runtime/os_wasm.go
Comment thread runtime/internal/runtime/proc.go Outdated
Comment thread runtime/internal/clite/c.go
Comment thread runtime/internal/runtime/proc_wasip1.go Outdated
@cpunion
cpunion force-pushed the codex/wasm-wasi-single-worker branch 3 times, most recently from 6c6211b to a64dba2 Compare August 3, 2026 22:23
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

51ad21aee9d0 | workflow run | long-term charts

Program measurements

Platform Workload File size vs main Build vs main Run vs main
Linux cprintf 18624 B +0.4% (worse) 238.387 ms -22.9% (better) 1.111 ms -18.8% (better)
Linux fmtprintf 1877736 B +0.0% (worse) 2.434 s -20.3% (better) 2.830 ms -21.1% (better)
Linux println 68288 B +0.3% (worse) 236.724 ms -22.8% (better) 1.335 ms -25.2% (better)
macOS cprintf 84672 B +0.0% 550.399 ms +78.0% (worse) 5.799 ms +122.6% (worse)
macOS fmtprintf 1888208 B +0.0% 3.038 s +34.1% (worse) 15.620 ms +47.3% (worse)
macOS println 121200 B +0.0% 512.302 ms +79.5% (worse) 7.788 ms +123.9% (worse)
Core language and compiler benchmarks
Platform Benchmark ns/op vs main
Linux BenchmarkLookupPCRandom 9.651 ns/op -21.3% (better)
Linux BenchmarkMergeCompilerFlags 111.800 ns/op -23.2% (better)
Linux BenchmarkMergeLinkerFlags 72.950 ns/op -23.9% (better)
Linux BenchmarkChannelBuffered 28.320 ns/op -22.1% (better)
Linux BenchmarkChannelHandoff 19604 ns/op -16.1% (better)
Linux BenchmarkDefer 36.830 ns/op -22.5% (better)
Linux BenchmarkDirectCall 1.363 ns/op -22.5% (better)
Linux BenchmarkGlobalRead 1.363 ns/op -22.5% (better)
Linux BenchmarkGlobalWrite 2.180 ns/op -22.4% (better)
Linux BenchmarkGoroutine 24252 ns/op -20.5% (better)
Linux BenchmarkInterfaceCall 6.822 ns/op -25.4% (better)
Linux BenchmarkRuntimeGetG 1.366 ns/op -35.4% (better)
macOS BenchmarkLookupPCRandom 13.120 ns/op +22.5% (worse)
macOS BenchmarkMergeCompilerFlags 125.200 ns/op +31.0% (worse)
macOS BenchmarkMergeLinkerFlags 87.570 ns/op +43.0% (worse)
macOS BenchmarkChannelBuffered 23.960 ns/op +17.2% (worse)
macOS BenchmarkChannelHandoff 9063 ns/op +40.4% (worse)
macOS BenchmarkDefer 33.360 ns/op +35.7% (worse)
macOS BenchmarkDirectCall 1.043 ns/op +0.4% (worse)
macOS BenchmarkGlobalRead 1.111 ns/op +9.6% (worse)
macOS BenchmarkGlobalWrite 1.027 ns/op +9.1% (worse)
macOS BenchmarkGoroutine 89147 ns/op +187.4% (worse)
macOS BenchmarkInterfaceCall 4.429 ns/op +8.3% (worse)
macOS BenchmarkRuntimeGetG 2.698 ns/op +43.3% (worse)

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

@cpunion cpunion changed the title runtime/wasm: add WASI single-worker scheduler (based on #2192, #2208) runtime/wasm: add WASI single-worker scheduler (based on #2192) Aug 4, 2026
@cpunion
cpunion force-pushed the codex/wasm-wasi-single-worker branch from a64dba2 to b4e35ee Compare August 4, 2026 01:28
@cpunion
cpunion force-pushed the codex/wasm-wasi-single-worker branch from b4e35ee to 51ad21a Compare August 9, 2026 15:11
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