Skip to content

wasm: preserve compiler roots across suspended goroutines (based on #102 #105) - #111

Draft
cpunion wants to merge 5 commits into
codex/fork-wasm-b-t-base-20260801from
codex/fork-wasm-suspended-roots-20260801
Draft

wasm: preserve compiler roots across suspended goroutines (based on #102 #105)#111
cpunion wants to merge 5 commits into
codex/fork-wasm-b-t-base-20260801from
codex/fork-wasm-suspended-roots-20260801

Conversation

@cpunion

@cpunion cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Depends on cpunion/llgo#102 and cpunion/llgo#105.

Problem

The opt-in wasm collector from #105 can scan linear-memory globals and its heap, but it cannot see a live Go pointer held only in an SSA/wasm local. The same problem applies to locals in a suspended goroutine. Without explicit publication, a collection can reclaim an object that Go code still owns.

Implementation

  • Add internal/gcrootplan, an LLVM-independent backward liveness planner over Go SSA. It identifies pointer-bearing values live across instructions whose lowering may call the runtime.
  • Lower one explicit root frame per affected function. Its runtime layout is { previous frame, map, roots[N] }, linked through llvm_gc_root_chain.
  • Publish parameters, SSA results, phi values, aggregates, interfaces, strings, slices, closures, maps, channels, arrays, and structs into stable root slots.
  • Give each wasm scheduler context one root-chain owner record. Context switches save the outgoing chain and install the incoming chain at the existing wasmcontext boundary.
  • Enumerate every registered context from the non-moving collector, including the active G and suspended Gs.
  • Save the root chain in defer state and restore it before siglongjmp, so panic/recover and Goexit cannot leave skipped function frames linked.

The package boundaries are intentional:

  • internal/gcrootplan: Go SSA data-flow analysis only.
  • ssa: target-independent root-frame LLVM IR and root layout helpers.
  • cl: classification and publication of Go values.
  • runtime/internal/gcroot: execution-owner registration, switching, and enumeration.
  • wasm scheduler files: lifecycle integration only; stack and Asyncify-buffer ownership remains in runtime/internal/wasmcontext.

All compiler and runtime behavior remains gated by the existing internal llgo_wasm_gc selection. Native, embedded, default wasm, and explicit WASI pthread builds do not enable compiler root frames.

Execution flow

  1. A function with live pointer-bearing values links one root frame at entry and publishes values into its slots.
  2. A scheduler context switch saves llvm_gc_root_chain in the outgoing owner and restores the incoming owner's chain before switching stacks.
  3. GC walks the registered owner list and then each owner's linked root frames.
  4. Normal returns unlink their frame. Panic/Goexit non-local unwinds restore the chain captured by the destination defer frame.
  5. Releasing a dead G unregisters its owner before wasmcontext.Context.Close releases the context storage.

Validation

  • J32, J64, and P1 GC fixtures pass and cover values reachable only from a suspended G plus panic/recover root-chain restoration.
  • J32/J64/P1 pass at default optimization, -O0, and -O3; J64 and P1 also pass -O3 -lto=thin.
  • P1 validates with wasm-tools and executes under Wasmtime 39; J32/J64 execute under Node 25.
  • macOS: internal/gcrootplan, full ssa, focused cl/build tests, and the complete runtime module pass.
  • Ubuntu amd64 container (2 CPUs, 6 GiB): D-focused SSA/CL/build tests and the complete runtime module pass. A separate full-SSA run reached the existing TestFromTestlibgo/mapzero while under QEMU and hit its 10-minute timeout; no D test failed.
  • Coverage: planner 97.6%, runtime root-chain manager 94.3%, and changed CL helpers 87.5%-100%.
  • Direct-base native output keeps the same 2,402,944-byte file size and identical section sizes. Cortex-M4 output is byte-identical at 1,804 bytes (text/data/bss = 140/0/10).
  • Fork CI: 39 checks pass across Ubuntu/macOS, Go 1.24/1.26, wasm runtime execution, LTO, coverage, release artifacts, and embedded targets; release publication is the only expected draft skip.

Independent diff against the combined #102 + #105 base: 37 files, +1859/-68.

Scope

This PR does not enable wasm GC by default and does not add cooperative safepoint polling. Default enablement and active-loop polling remain the next S stage after timers and this root ABI are integrated.

@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

5f96acb32daf | workflow run | long-term charts

Program measurements

Platform Workload File size vs main Build vs main Run vs main
Linux cprintf 18800 B +1.4% (worse) 388.659 ms -5.7% (better) 1.332 ms -14.2% (better)
Linux fmtprintf 2219352 B +0.3% (worse) 3.738 s -3.1% (better) 2.558 ms -22.2% (better)
Linux println 71760 B -1.2% (better) 376.342 ms -5.9% (better) 1.683 ms +0.6% (worse)
macOS cprintf 84672 B +0.0% 767.587 ms +85.3% (worse) 3.952 ms +10.2% (worse)
macOS fmtprintf 2361968 B +0.7% (worse) 3.757 s -4.6% (better) 26.295 ms +12.5% (worse)
macOS println 125712 B -1.0% (better) 538.622 ms +29.9% (worse) 5.589 ms -23.0% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs main
Linux BenchmarkLookupPCRandom 13.390 ns/op -0.7% (better)
Linux BenchmarkMergeCompilerFlags 153.600 ns/op -0.6% (better)
Linux BenchmarkMergeLinkerFlags 95.940 ns/op -5.3% (better)
Linux BenchmarkChannelBuffered 35.880 ns/op +3.2% (worse)
Linux BenchmarkChannelHandoff 26569 ns/op -17.1% (better)
Linux BenchmarkDefer 46.710 ns/op -17.0% (better)
Linux BenchmarkDirectCall 1.558 ns/op -0.3% (better)
Linux BenchmarkGlobalRead 1.558 ns/op -0.1% (better)
Linux BenchmarkGlobalWrite 2.491 ns/op -0.1% (better)
Linux BenchmarkGoroutine 34103 ns/op -54.9% (better)
Linux BenchmarkInterfaceCall 7.785 ns/op -0.1% (better)
Linux BenchmarkRuntimeGetG 2.495 ns/op -53.0% (better)
macOS BenchmarkLookupPCRandom 14.450 ns/op +16.2% (worse)
macOS BenchmarkMergeCompilerFlags 160.800 ns/op +29.2% (worse)
macOS BenchmarkMergeLinkerFlags 125 ns/op +80.2% (worse)
macOS BenchmarkChannelBuffered 44.090 ns/op +59.0% (worse)
macOS BenchmarkChannelHandoff 32342 ns/op +361.4% (worse)
macOS BenchmarkDefer 44.540 ns/op +19.0% (worse)
macOS BenchmarkDirectCall 1.641 ns/op +12.9% (worse)
macOS BenchmarkGlobalRead 1.479 ns/op +22.1% (worse)
macOS BenchmarkGlobalWrite 2.238 ns/op +50.5% (worse)
macOS BenchmarkGoroutine 37189 ns/op +17.5% (worse)
macOS BenchmarkInterfaceCall 6.265 ns/op +16.1% (worse)
macOS BenchmarkRuntimeGetG 3.881 ns/op +50.6% (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