Skip to content

runtime/wasm: enable cooperative safepoints and GC by default (based on #107 #111) - #116

Draft
cpunion wants to merge 4 commits into
codex/fork-wasm-c-d-base-20260801from
codex/fork-wasm-cooperative-safepoints-20260801
Draft

runtime/wasm: enable cooperative safepoints and GC by default (based on #107 #111)#116
cpunion wants to merge 4 commits into
codex/fork-wasm-c-d-base-20260801from
codex/fork-wasm-cooperative-safepoints-20260801

Conversation

@cpunion

@cpunion cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Depends on #107 and #111.

Part of xgo-dev#2152.

Problem

#111 preserves compiler-published roots for running and suspended goroutines, but collection still requires an internal opt-in tag. A CPU-bound goroutine can also monopolize the single wasm worker indefinitely, preventing host timers, runnable goroutines, and timer-triggered GC from running.

Implementation

  • Add an independent internal/safepointplan CFG analysis. It inserts polls at declared Go function entries and DFS cycle-closing backedges, intersecting reducible and irreducible cycles without polling every loop block.
  • Feed synthetic polls into the existing root-liveness planner. Pointer-bearing parameters and closure context are live at the entry poll, so values crossing a possible yield remain published.
  • Resolve generic SSA instances through Origin/Parent before instrumentation. Runtime packages, //go:nosplit, cgo bodies, and package-less synthetic forwarding wrappers remain outside instrumentation.
  • Add a fixed poll budget. The inlined fast path decrements one single-worker budget; every 1,024 polls the noinline slow path dispatches due host events and yields only when another goroutine is runnable.
  • Enable the existing non-moving collector, root publication, and cooperative polls by default for J32, J64, and default single-worker P1.
  • Preserve explicit LLGO_WASI_THREADS=1 behavior as the historical pthread/non-GC path. Combining it explicitly with the internal llgo_wasm_gc source-selection tag remains an error.
  • Apply existing SSA type patches to nested field access; default J32 exposed this through syscall/js.Value nested in another struct.
  • Supply wasm posix_memalign from the collector allocator because Emscripten GC selection uses -sMALLOC=none.

The planner, compiler emission, root liveness, poll budget, scheduler slow path, host event queue, and collector remain separate modules.

Runtime flow

  1. A compiled application function publishes its root frame and executes its entry poll.
  2. Loop backedges execute the same inlined budget check.
  3. Normal polls only decrement the budget and return.
  4. On budget expiry, the runtime polls host events.
  5. A due timer callback becomes runnable; when another goroutine is runnable, the current goroutine is requeued and the scheduler switches.
  6. GC runs synchronously on the single worker. Suspended goroutines retain their context-owned root chains, providing stop-the-world behavior without a multi-worker handshake.

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 ssa, internal/build, and runtime-module tests pass on macOS.
  • J32, J64, and P1 GC fixtures pass at -O0 and -O3; J64 and P1 ThinLTO pass.
  • The GC fixture covers globals, an active CPU-bound goroutine, timer-triggered collection, a channel-waiting goroutine, panic/recover root restoration, reclamation, aligned allocation, and linear-memory growth.
  • Existing scheduler, blocking, and timer fixtures pass at -O3 on J32/J64/P1.
  • Every tested P1 artifact passes wasm-tools validate --features all.
  • Explicit P1 pthread mode builds and contains no llvm_gc_root_chain, collector, or CooperativeSafepoint symbol.
  • Ubuntu 24.04/amd64 focused compiler tests and the complete runtime module pass with Go 1.26.5.
  • Fork CI passes 39 checks across Ubuntu/macOS, Go 1.24/1.26, wasm runtime execution, LTO, coverage, release artifacts, and embedded targets; draft release publication is the only expected skip.

Coverage highlights from local profiles:

  • internal/safepointplan: 100%
  • runtime/internal/pollbudget: 100%
  • configureWasmGC and hasBuildTag: 100%
  • SSA safepoint configuration methods: 100%
  • CL safepoint emission helpers: 87.5-100%
  • root preparation: 97.9%

Cost

On Apple M4 Max, the warm BenchmarkBudgetPoll range is 1.797-1.862 ns/op, median 1.828 ns/op, with 0 B/op and 0 allocs/op.

A target-level -O3 microbenchmark compares the same 20-million-iteration arithmetic loop with a normal poll at every backedge against a //go:nosplit no-poll control. Values are medians of five rounds:

Target no poll poll absolute delta
J32 / Node 3.342 ns/op 4.911 ns/op +1.568 ns/op
J64 / Node 3.863 ns/op 5.459 ns/op +1.595 ns/op
P1 / Wasmtime 3.879 ns/op 5.601 ns/op +1.722 ns/op

This intentionally minimal loop is a worst case: the relative increase is 41-47%, while the absolute backedge cost is about 1.6-1.7 ns and does not allocate.

Enabling the collector and polls by default increases the unoptimized scheduler fixture relative to the direct #107 + #111 integration base:

Target base this PR delta
J32 559,381 B 820,167 B +260,786 B
J64 611,693 B 850,175 B +238,482 B
P1 534,710 B 778,022 B +243,312 B

Non-wasm output is unaffected:

  • 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.

Review scope

Excluding the dependency merge, this PR changes 21 files with +786/-30. Four commits separate nested type-patch correctness, compiler planning/emission, runtime polling, and default build/acceptance behavior. M1/M2 continuation work is intentionally outside this PR.

@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

158188d7c45a | 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) 337.779 ms -18.1% (better) 1.064 ms -31.4% (better)
Linux fmtprintf 2219448 B +0.3% (worse) 3.422 s -11.2% (better) 1.947 ms -40.8% (better)
Linux println 71776 B -1.2% (better) 336.347 ms -15.9% (better) 1.319 ms -21.2% (better)
macOS cprintf 84672 B +0.0% 342.320 ms -17.4% (better) 2.875 ms -19.9% (better)
macOS fmtprintf 2361968 B +0.7% (worse) 3.244 s -17.7% (better) 20.923 ms -10.5% (better)
macOS println 125712 B -1.0% (better) 301.972 ms -27.2% (better) 3.315 ms -54.3% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs main
Linux BenchmarkLookupPCRandom 10.920 ns/op -19.0% (better)
Linux BenchmarkMergeCompilerFlags 166 ns/op +7.4% (worse)
Linux BenchmarkMergeLinkerFlags 106.100 ns/op +4.7% (worse)
Linux BenchmarkChannelBuffered 53.530 ns/op +54.0% (worse)
Linux BenchmarkChannelHandoff 21666 ns/op -32.4% (better)
Linux BenchmarkDefer 47.930 ns/op -14.9% (better)
Linux BenchmarkDirectCall 0.866 ns/op -44.6% (better)
Linux BenchmarkGlobalRead 1.154 ns/op -26.0% (better)
Linux BenchmarkGlobalWrite 6.054 ns/op +142.7% (worse)
Linux BenchmarkGoroutine 33390 ns/op -55.8% (better)
Linux BenchmarkInterfaceCall 5.030 ns/op -35.5% (better)
Linux BenchmarkRuntimeGetG 1.730 ns/op -67.4% (better)
macOS BenchmarkLookupPCRandom 11.430 ns/op -8.1% (better)
macOS BenchmarkMergeCompilerFlags 108.600 ns/op -12.8% (better)
macOS BenchmarkMergeLinkerFlags 65.950 ns/op -4.9% (better)
macOS BenchmarkChannelBuffered 21.870 ns/op -21.1% (better)
macOS BenchmarkChannelHandoff 7221 ns/op +3.0% (worse)
macOS BenchmarkDefer 26.530 ns/op -29.1% (better)
macOS BenchmarkDirectCall 1.023 ns/op -29.6% (better)
macOS BenchmarkGlobalRead 0.956 ns/op -21.1% (better)
macOS BenchmarkGlobalWrite 0.950 ns/op -36.1% (better)
macOS BenchmarkGoroutine 26844 ns/op -15.2% (better)
macOS BenchmarkInterfaceCall 4.433 ns/op -17.9% (better)
macOS BenchmarkRuntimeGetG 2.099 ns/op -18.5% (better)

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