Skip to content

wasm: support cross-worker syscall/js values and callbacks - #2738

Merged
cpunion merged 12 commits into
xgo-dev:mainfrom
cpunion:codex/wasm-js-cross-worker-20261006
Oct 14, 2026
Merged

cpunion merged 12 commits into
xgo-dev:mainfrom
cpunion:codex/wasm-js-cross-worker-20261006

Conversation

@cpunion

@cpunion cpunion commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Allow goroutines on different Emscripten workers to share public syscall/js.Value and js.Func. Host operations and callbacks execute in Go main's worker-zero JavaScript realm; remote callers park for synchronous results or exceptions and reuse an executor that retires with the caller. Worker-zero calls and reserved primitives avoid forwarding allocations; raw C/Emscripten handles remain local to their worker.

Support nested/yielding callbacks, Goexit defers and GC-safe executor cleanup. Contended runqueue/allocator paths acknowledge GC in place without replaying foreign frames. A bounded host handoff prevents a continuous collector from starving allocators. Object/metadata scans batch bounded, aligned zero/free/tail spans while retaining individual handling for mixed states and partial boundaries. Reverse object-head lookup also reads bounded, aligned metadata words; this addresses the worker full-package CI timeout without changing its 60-second limit. Filesystem proxy allocations remain rooted until their JS callers finish; numeric JS addresses alone cannot keep Go allocations alive.

Validation: Node/Chrome Memory32/64 worker acceptance, shared values, nested callbacks, private roots, exceptions, finalizer contention, executor retirement and filesystem GC stress. Host tests exercise runqueue transitions and GC metadata boundaries. The dedicated workers CI suite retains every acceptance case and its existing deadlines; its workflow contract accounts for the additional matrix entry.

Rebased on main 1bbfc1f75. Preserve upstream's single-arena lookup fast path and remove the redundant older optimization. Builds on the worker scheduler; #2743 adds cross-worker reflection entries and full test/go qualification.

Fixes #2737.

@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: WASM bounded-worker syscall/js cross-worker sharing

This is a well-structured change. The forwarding/executor pattern is sound: the single-channel-per-G design serializes caller and executor (no intra-call concurrency), nested calls on worker 0 correctly bypass forwarding (isRemoteJSWorker() is false there, avoiding deadlock and recursive round-trips), the spawnWorker save/restore around the synchronous go fn() is correct, and the channel is lazily created per caller-G and closed in finishWasmG. Cross-worker handoff of the call/done struct is correctly synchronized by the unbuffered channel. The Goexit and panic paths are handled, and test/wasm/js-workers/main.go exercises shared values, nested callbacks, panics, Goexit, and executor-leak detection.

Findings below are mostly minor/latent plus one build-tag correctness issue. No high-severity memory-safety or security issues were found.

Additional notes not placed inline:

  • Stale "realm affinity" comment (runtime/internal/runtime/proc_wasm_workers.go:46-48, outside this diff's hunks): the comment "Override both inherited realm affinity and callback-event pinning" is stale — this PR removed the jsRealm affinity model, so only callback-event pinning is overridden now.
  • Per-op round-trip amplification: each remote syscall/js call is a full synchronous round-trip to worker 0 (channel send → cross-worker switch → <-done). Method chains off worker 0 (e.g. iterating a JS array via Index/Length) become O(N) cross-worker handoffs where the single-worker build had O(N) direct FFI calls. There is no batching path. Worth documenting as a constraint for JS-heavy loops on non-zero workers.
  • Executor-G lifetime: one executor G is created per calling G and lives (holding a native + Asyncify stack on worker 0) until the caller G exits; jsCalls is never closed early. Many short-lived caller Gs each touching JS once will accumulate executor Gs/stacks concentrated on worker 0. Consider documenting this, or a shared/bounded executor pool.

Comment thread runtime/internal/lib/syscall/js/worker_call_single.go Outdated
Comment thread runtime/internal/runtime/js_worker_call_wasm_workers.go
Comment thread runtime/internal/lib/syscall/js/js.go Outdated

@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 (addendum): build-tag correctness

One inline finding from the main review was dropped on submission due to a title-length limit; posting it here.

Comment thread runtime/internal/lib/syscall/js/worker_call_single.go Outdated
@codecov

codecov Bot commented Oct 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

LLGo WebAssembly build benchmarks

d2febcf6556b | workflow run | long-term charts

WebAssembly output sizes
Example, profile and compiler Wasm module vs base Generated JS glue vs base
cprintf/j32-emscripten/LLGo 146607 B +1087 B / +0.7% (worse) 88541 B +384 B / +0.4% (worse)
cprintf/j32-goos-js/LLGo 145217 B +469 B / +0.3% (worse) 72985 B 0 B / +0.0%
cprintf/j64-emscripten-memory64/LLGo 134168 B +948 B / +0.7% (worse) 91712 B +384 B / +0.4% (worse)
cprintf/w32-goos-wasip1/LLGo 155969 B +725 B / +0.5% (worse) 0 B 0 B / 0.0%
cprintf/w32-wasi/LLGo 156667 B +745 B / +0.5% (worse) 0 B 0 B / 0.0%
fmtprintf/j32-emscripten/LLGo 3002209 B +5499 B / +0.2% (worse) 131047 B +386 B / +0.3% (worse)
fmtprintf/j32-goos-js/Go 2526852 B 0 B / +0.0% 0 B 0 B / 0.0%
fmtprintf/j32-goos-js/LLGo 3092617 B +1130 B / +0.03655% (worse) 102110 B 0 B / +0.0%
fmtprintf/j64-emscripten-memory64/LLGo 2764697 B +5416 B / +0.2% (worse) 136527 B +386 B / +0.3% (worse)
fmtprintf/w32-goos-wasip1/Go 2500019 B 0 B / +0.0% 0 B 0 B / 0.0%
fmtprintf/w32-goos-wasip1/LLGo 2342178 B +1012 B / +0.04323% (worse) 0 B 0 B / 0.0%
fmtprintf/w32-wasi/LLGo 2338874 B +1032 B / +0.04414% (worse) 0 B 0 B / 0.0%
j32-emscripten/LLGo 146124 B +1091 B / +0.8% (worse) 88541 B +384 B / +0.4% (worse)
j32-goos-js/Go 1895533 B 0 B / +0.0% 0 B 0 B / 0.0%
j32-goos-js/LLGo 144965 B +469 B / +0.3% (worse) 72985 B 0 B / +0.0%
j64-emscripten-memory64/LLGo 133725 B +952 B / +0.7% (worse) 91712 B +384 B / +0.4% (worse)
reflectcall/j32-emscripten/LLGo 1445108 B +1580 B / +0.1% (worse) 105303 B +386 B / +0.4% (worse)
reflectcall/j32-goos-js/Go 2191221 B 0 B / +0.0% 0 B 0 B / 0.0%
reflectcall/j32-goos-js/LLGo 1493967 B +702 B / +0.04701% (worse) 89743 B 0 B / +0.0%
reflectcall/j64-emscripten-memory64/LLGo 1337539 B +1455 B / +0.1% (worse) 109882 B +390 B / +0.4% (worse)
reflectcall/w32-goos-wasip1/Go 2205707 B 0 B / +0.0% 0 B 0 B / 0.0%
reflectcall/w32-goos-wasip1/LLGo 1272365 B +821 B / +0.1% (worse) 0 B 0 B / 0.0%
reflectcall/w32-wasi/LLGo 1270006 B +841 B / +0.1% (worse) 0 B 0 B / 0.0%
w32-goos-wasip1/Go 1909947 B 0 B / +0.0% 0 B 0 B / 0.0%
w32-goos-wasip1/LLGo 155643 B +725 B / +0.5% (worse) 0 B 0 B / 0.0%
w32-wasi/LLGo 156341 B +745 B / +0.5% (worse) 0 B 0 B / 0.0%
LLGo WebAssembly build measurements
Example and profile Build vs base
j32-emscripten 8.128 s -503 ms / -5.8% (better)
j32-goos-js 8.006 s -144 ms / -1.8% (better)
j64-emscripten-memory64 7.195 s -216.5 ms / -2.9% (better)
reflectcall/w32-wasi 26.197 s -298.6 ms / -1.1% (better)
w32-goos-wasip1 4.981 s -164.9 ms / -3.2% (better)
w32-wasi 4.723 s -523 ms / -10.0% (better)

Compared with 1bbfc1f75db4 measured in the same runner job.

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

LLGo baseline benchmarks

d2febcf6556b | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Text size vs base Build vs base Run vs base
Linux cprintf 8568 B 0 B / +0.0% 387 B 0 B / +0.0% 764.195 ms -45.74 ms / -5.6% (better) 1.248 ms -28.05 us / -2.2% (better)
Linux cprintf-lto 8408 B 0 B / +0.0% 368 B 0 B / +0.0% 800.763 ms -25.79 ms / -3.1% (better) 1.276 ms +54.19 us / +4.4% (worse)
Linux fmtprintf 4899440 B 0 B / +0.0% 506206 B 0 B / +0.0% 4.901 s -126.1 ms / -2.5% (better) 3.896 ms -212.8 us / -5.2% (better)
Linux fmtprintf-lto 3620608 B 0 B / +0.0% 444290 B 0 B / +0.0% 13.989 s -612.5 ms / -4.2% (better) 3.581 ms -387.7 us / -9.8% (better)
Linux println 689472 B 0 B / +0.0% 17095 B 0 B / +0.0% 783.149 ms -45.99 ms / -5.5% (better) 1.591 ms -163.1 us / -9.3% (better)
Linux println-lto 192696 B 0 B / +0.0% 14484 B 0 B / +0.0% 1.086 s -34.86 ms / -3.1% (better) 1.587 ms -25.5 us / -1.6% (better)
macOS cprintf 50736 B 0 B / +0.0% 4409 B 0 B / +0.0% 760.717 ms -138.5 ms / -15.4% (better) 3.105 ms -16.38 us / -0.5% (better)
macOS cprintf-lto 50496 B 0 B / +0.0% 161 B 0 B / +0.0% 731.633 ms -90.64 ms / -11.0% (better) 2.218 ms -251 us / -10.2% (better)
macOS fmtprintf 1793088 B 0 B / +0.0% 882736 B 0 B / +0.0% 3.023 s -154.2 ms / -4.9% (better) 30.353 ms +3.984 ms / +15.1% (worse)
macOS fmtprintf-lto 1362176 B 0 B / +0.0% 766864 B 0 B / +0.0% 7.229 s +1.362 s / +23.2% (worse) 37.693 ms +5.322 ms / +16.4% (worse)
macOS println 100080 B 0 B / +0.0% 24519 B 0 B / +0.0% 714.788 ms -131.3 ms / -15.5% (better) 4.005 ms +327.5 us / +8.9% (worse)
macOS println-lto 84016 B 0 B / +0.0% 21760 B 0 B / +0.0% 922.811 ms -92.84 ms / -9.1% (better) 3.174 ms -345.7 us / -9.8% (better)
Windows MinGW cprintf 660992 B 0 B / +0.0% 4550 B 0 B / +0.0% 1.652 s -54.96 ms / -3.2% (better) 3.410 ms -163.9 us / -4.6% (better)
Windows MinGW cprintf-lto 44544 B 0 B / +0.0% 4502 B 0 B / +0.0% 1.778 s +94.75 ms / +5.6% (worse) 4.024 ms +605 us / +17.7% (worse)
Windows MinGW fmtprintf 5457408 B 0 B / +0.0% 606134 B 0 B / +0.0% 5.540 s -77.49 ms / -1.4% (better) 10.962 ms -47.8 us / -0.4% (better)
Windows MinGW fmtprintf-lto 4160000 B 0 B / +0.0% 553254 B 0 B / +0.0% 13.354 s -466.3 ms / -3.4% (better) 10.892 ms +194.8 us / +1.8% (worse)
Windows MinGW println 716800 B 0 B / +0.0% 25446 B 0 B / +0.0% 1.680 s -15.75 ms / -0.9% (better) 6.541 ms -208 us / -3.1% (better)
Windows MinGW println-lto 210944 B 0 B / +0.0% 22326 B 0 B / +0.0% 1.928 s -6.711 ms / -0.3% (better) 6.290 ms -1.247 ms / -16.5% (better)
Windows MinGW 386 cprintf 609792 B 0 B / +0.0% 5352 B 0 B / +0.0% 1.619 s +2.16 ms / +0.1% (worse) 5.895 ms -10.5 us / -0.2% (better)
Windows MinGW 386 cprintf-lto 104448 B 0 B / +0.0% 5112 B 0 B / +0.0% 1.675 s +24.7 ms / +1.5% (worse) 5.826 ms -336.8 us / -5.5% (better)
Windows MinGW 386 fmtprintf 4758528 B 0 B / +0.0% 473902 B 0 B / +0.0% 5.547 s -3.329 ms / -0.1% (better) 14.030 ms -437.1 us / -3.0% (better)
Windows MinGW 386 fmtprintf-lto 4162048 B 0 B / +0.0% 452294 B 0 B / +0.0% 13.257 s +101.9 ms / +0.8% (worse) 23.338 ms +532.7 us / +2.3% (worse)
Windows MinGW 386 println 663040 B 0 B / +0.0% 21694 B 0 B / +0.0% 1.629 s +23.13 ms / +1.4% (worse) 10.197 ms +404.8 us / +4.1% (worse)
Windows MinGW 386 println-lto 260608 B 0 B / +0.0% 19514 B 0 B / +0.0% 1.943 s -1.295 ms / -0.1% (better) 10.203 ms +338.7 us / +3.4% (worse)
Windows MinGW ARM64 cprintf 671744 B 0 B / +0.0% 4436 B 0 B / +0.0% 1.909 s +32.15 ms / +1.7% (worse) 6.529 ms +48.6 us / +0.7% (worse)
Windows MinGW ARM64 cprintf-lto 44032 B 0 B / +0.0% 4388 B 0 B / +0.0% 1.887 s +16.16 ms / +0.9% (worse) 6.585 ms +95.7 us / +1.5% (worse)
Windows MinGW ARM64 fmtprintf 5367296 B 0 B / +0.0% 514292 B 0 B / +0.0% 5.325 s -85.21 ms / -1.6% (better) 14.391 ms -926.9 us / -6.1% (better)
Windows MinGW ARM64 fmtprintf-lto 4326400 B 0 B / +0.0% 480372 B 0 B / +0.0% 11.937 s -146.6 ms / -1.2% (better) 14.749 ms -312.1 us / -2.1% (better)
Windows MinGW ARM64 println 724480 B 0 B / +0.0% 24160 B 0 B / +0.0% 1.879 s -27.49 ms / -1.4% (better) 11.332 ms -42.6 us / -0.4% (better)
Windows MinGW ARM64 println-lto 218624 B 0 B / +0.0% 21516 B 0 B / +0.0% 2.122 s -55.93 ms / -2.6% (better) 10.580 ms -495.9 us / -4.5% (better)
Windows MSVC cprintf 905216 B 0 B / +0.0% 66003 B 0 B / +0.0% 1.687 s -122.7 ms / -6.8% (better) 3.692 ms +322.6 us / +9.6% (worse)
Windows MSVC cprintf-lto 292352 B 0 B / +0.0% 65971 B 0 B / +0.0% 1.704 s +126.3 ms / +8.0% (worse) 4.542 ms +1.191 ms / +35.5% (worse)
Windows MSVC fmtprintf 5757440 B 0 B / +0.0% 701894 B 0 B / +0.0% 5.563 s -71.26 ms / -1.3% (better) 14.612 ms +577.5 us / +4.1% (worse)
Windows MSVC fmtprintf-lto 4483072 B 0 B / +0.0% 652806 B 0 B / +0.0% 13.235 s -112.6 ms / -0.8% (better) 15.075 ms +430.1 us / +2.9% (worse)
Windows MSVC println 1029120 B 0 B / +0.0% 121334 B 0 B / +0.0% 1.668 s +115.1 ms / +7.4% (worse) 9.599 ms +1.768 ms / +22.6% (worse)
Windows MSVC println-lto 531456 B 0 B / +0.0% 118870 B 0 B / +0.0% 1.959 s +55.47 ms / +2.9% (worse) 9.399 ms +2.017 ms / +27.3% (worse)
Windows MSVC 386 cprintf 520704 B 0 B / +0.0% 3947 B 0 B / +0.0% 1.397 s -6.69 ms / -0.5% (better) 5.358 ms +50.3 us / +0.9% (worse)
Windows MSVC 386 cprintf-lto 44544 B 0 B / +0.0% 3901 B 0 B / +0.0% 1.442 s +24.86 ms / +1.8% (worse) 5.348 ms -211.4 us / -3.8% (better)
Windows MSVC 386 fmtprintf 4491776 B 0 B / +0.0% 457314 B 0 B / +0.0% 5.126 s +16.29 ms / +0.3% (worse) 13.211 ms +583.3 us / +4.6% (worse)
Windows MSVC 386 fmtprintf-lto 3909632 B 0 B / +0.0% 427681 B 0 B / +0.0% 11.391 s +25.2 ms / +0.2% (worse) 13.660 ms -614.7 us / -4.3% (better)
Windows MSVC 386 println 576000 B 0 B / +0.0% 20566 B 0 B / +0.0% 1.411 s +7.149 ms / +0.5% (worse) 9.370 ms +206.5 us / +2.3% (worse)
Windows MSVC 386 println-lto 200192 B 0 B / +0.0% 18711 B 0 B / +0.0% 1.638 s -216.7 us / -0.01323% (better) 9.584 ms +217.5 us / +2.3% (worse)
Windows MSVC ARM64 cprintf 672768 B 0 B / +0.0% 4240 B 0 B / +0.0% 1.579 s -17.52 ms / -1.1% (better) 6.113 ms -820.9 us / -11.8% (better)
Windows MSVC ARM64 cprintf-lto 49152 B 0 B / +0.0% 4140 B 0 B / +0.0% 1.557 s -26.5 ms / -1.7% (better) 6.405 ms -445.2 us / -6.5% (better)
Windows MSVC ARM64 fmtprintf 5363712 B 0 B / +0.0% 514164 B 0 B / +0.0% 5.011 s -18.44 ms / -0.4% (better) 15.928 ms -209.9 us / -1.3% (better)
Windows MSVC ARM64 fmtprintf-lto 4333056 B 0 B / +0.0% 480976 B 0 B / +0.0% 11.290 s +82.89 ms / +0.7% (worse) 15.723 ms -1.135 ms / -6.7% (better)
Windows MSVC ARM64 println 727040 B 0 B / +0.0% 24200 B 0 B / +0.0% 1.594 s -5.155 ms / -0.3% (better) 11.731 ms -202.9 us / -1.7% (better)
Windows MSVC ARM64 println-lto 223232 B 0 B / +0.0% 21688 B 0 B / +0.0% 1.811 s -24.14 ms / -1.3% (better) 11.495 ms -431.2 us / -3.6% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 14.420 ns/op -0.94 ns/op / -6.1% (better)
Linux BenchmarkMergeCompilerFlags 191.200 ns/op +0.2 ns/op / +0.1% (worse)
Linux BenchmarkMergeLinkerFlags 128.300 ns/op -3.5 ns/op / -2.7% (better)
Linux BenchmarkChannelBuffered 55.270 ns/op +0.18 ns/op / +0.3% (worse)
Linux BenchmarkChannelHandoff 12889 ns/op -6936 ns/op / -35.0% (better)
Linux BenchmarkDefer 47.500 ns/op -0.79 ns/op / -1.6% (better)
Linux BenchmarkDirectCall 1.173 ns/op +0.007 ns/op / +0.6% (worse)
Linux BenchmarkGlobalRead 1.554 ns/op +0.368 ns/op / +31.0% (worse)
Linux BenchmarkGlobalWrite 7.764 ns/op -0.019 ns/op / -0.2% (better)
Linux BenchmarkGoroutine 24633 ns/op -234 ns/op / -0.9% (better)
Linux BenchmarkInterfaceCall 6.607 ns/op +0.709 ns/op / +12.0% (worse)
Linux BenchmarkRuntimeGetG 2.952 ns/op +0.6 ns/op / +25.5% (worse)
macOS BenchmarkLookupPCRandom 11.730 ns/op +0.87 ns/op / +8.0% (worse)
macOS BenchmarkMergeCompilerFlags 101.900 ns/op +13.29 ns/op / +15.0% (worse)
macOS BenchmarkMergeLinkerFlags 64.730 ns/op +6.39 ns/op / +11.0% (worse)
macOS BenchmarkChannelBuffered 22.340 ns/op -0.1 ns/op / -0.4% (better)
macOS BenchmarkChannelHandoff 8288 ns/op -122 ns/op / -1.5% (better)
macOS BenchmarkDefer 31.980 ns/op -1.81 ns/op / -5.4% (better)
macOS BenchmarkDirectCall 0.948 ns/op -0.0168 ns/op / -1.7% (better)
macOS BenchmarkGlobalRead 0.949 ns/op +0.0853 ns/op / +9.9% (worse)
macOS BenchmarkGlobalWrite 0.940 ns/op +0.0685 ns/op / +7.9% (worse)
macOS BenchmarkGoroutine 35393 ns/op -17302 ns/op / -32.8% (better)
macOS BenchmarkInterfaceCall 3.761 ns/op -0.096 ns/op / -2.5% (better)
macOS BenchmarkRuntimeGetG 1.874 ns/op -0.02 ns/op / -1.1% (better)
Windows MinGW BenchmarkLookupPCRandom 13.250 ns/op +0.39 ns/op / +3.0% (worse)
Windows MinGW BenchmarkMergeCompilerFlags 638.100 ns/op -35.3 ns/op / -5.2% (better)
Windows MinGW BenchmarkMergeLinkerFlags 552.400 ns/op -39.1 ns/op / -6.6% (better)
Windows MinGW BenchmarkChannelBuffered 31.100 ns/op +1 ns/op / +3.3% (worse)
Windows MinGW BenchmarkChannelHandoff 1060 ns/op +2 ns/op / +0.2% (worse)
Windows MinGW BenchmarkDefer 54.260 ns/op -0.61 ns/op / -1.1% (better)
Windows MinGW BenchmarkDirectCall 1.547 ns/op -0.003 ns/op / -0.2% (better)
Windows MinGW BenchmarkGlobalRead 1.549 ns/op +0.001 ns/op / +0.1% (worse)
Windows MinGW BenchmarkGlobalWrite 2.469 ns/op -0.003 ns/op / -0.1% (better)
Windows MinGW BenchmarkGoroutine 89797 ns/op -829 ns/op / -0.9% (better)
Windows MinGW BenchmarkInterfaceCall 8.059 ns/op -0.33 ns/op / -3.9% (better)
Windows MinGW BenchmarkRuntimeGetG 2.486 ns/op +0.007 ns/op / +0.3% (worse)
Windows MinGW 386 BenchmarkLookupPCRandom 72.160 ns/op 0 ns/op / +0.0%
Windows MinGW 386 BenchmarkMergeCompilerFlags 805 ns/op +4.4 ns/op / +0.5% (worse)
Windows MinGW 386 BenchmarkMergeLinkerFlags 758.500 ns/op -26.3 ns/op / -3.4% (better)
Windows MinGW 386 BenchmarkChannelBuffered 54.810 ns/op -0.09 ns/op / -0.2% (better)
Windows MinGW 386 BenchmarkChannelHandoff 1866 ns/op -106 ns/op / -5.4% (better)
Windows MinGW 386 BenchmarkDefer 52.100 ns/op +2.26 ns/op / +4.5% (worse)
Windows MinGW 386 BenchmarkDirectCall 1.034 ns/op -0.012 ns/op / -1.1% (better)
Windows MinGW 386 BenchmarkGlobalRead 1.031 ns/op -0.284 ns/op / -21.6% (better)
Windows MinGW 386 BenchmarkGlobalWrite 17.770 ns/op -0.07 ns/op / -0.4% (better)
Windows MinGW 386 BenchmarkGoroutine 92589 ns/op +82 ns/op / +0.1% (worse)
Windows MinGW 386 BenchmarkInterfaceCall 6.579 ns/op +0.88 ns/op / +15.4% (worse)
Windows MinGW 386 BenchmarkRuntimeGetG 1.892 ns/op +0.385 ns/op / +25.5% (worse)
Windows MinGW ARM64 BenchmarkLookupPCRandom 12.070 ns/op -0.06 ns/op / -0.5% (better)
Windows MinGW ARM64 BenchmarkMergeCompilerFlags 572.700 ns/op +1.1 ns/op / +0.2% (worse)
Windows MinGW ARM64 BenchmarkMergeLinkerFlags 533.400 ns/op -0.2 ns/op / -0.03748% (better)
Windows MinGW ARM64 BenchmarkChannelBuffered 38.320 ns/op -0.36 ns/op / -0.9% (better)
Windows MinGW ARM64 BenchmarkChannelHandoff 5037 ns/op +721 ns/op / +16.7% (worse)
Windows MinGW ARM64 BenchmarkDefer 54.710 ns/op +1.41 ns/op / +2.6% (worse)
Windows MinGW ARM64 BenchmarkDirectCall 0.590 ns/op +0.0001 ns/op / +0.01696% (worse)
Windows MinGW ARM64 BenchmarkGlobalRead 0.593 ns/op +0.0029 ns/op / +0.5% (worse)
Windows MinGW ARM64 BenchmarkGlobalWrite 0.663 ns/op -0.0739 ns/op / -10.0% (better)
Windows MinGW ARM64 BenchmarkGoroutine 61685 ns/op +7 ns/op / +0.01135% (worse)
Windows MinGW ARM64 BenchmarkInterfaceCall 4.138 ns/op -0.011 ns/op / -0.3% (better)
Windows MinGW ARM64 BenchmarkRuntimeGetG 1.805 ns/op +0.002 ns/op / +0.1% (worse)
Windows MSVC BenchmarkLookupPCRandom 13.040 ns/op +0.12 ns/op / +0.9% (worse)
Windows MSVC BenchmarkMergeCompilerFlags 610.400 ns/op -20.5 ns/op / -3.2% (better)
Windows MSVC BenchmarkMergeLinkerFlags 544.300 ns/op -10.2 ns/op / -1.8% (better)
Windows MSVC BenchmarkChannelBuffered 28.840 ns/op -0.95 ns/op / -3.2% (better)
Windows MSVC BenchmarkChannelHandoff 1218 ns/op +46 ns/op / +3.9% (worse)
Windows MSVC BenchmarkDefer 68.060 ns/op +9.99 ns/op / +17.2% (worse)
Windows MSVC BenchmarkDirectCall 1.548 ns/op -0.001 ns/op / -0.1% (better)
Windows MSVC BenchmarkGlobalRead 1.549 ns/op -0.003 ns/op / -0.2% (better)
Windows MSVC BenchmarkGlobalWrite 2.460 ns/op -0.002 ns/op / -0.1% (better)
Windows MSVC BenchmarkGoroutine 89796 ns/op -699 ns/op / -0.8% (better)
Windows MSVC BenchmarkInterfaceCall 8.697 ns/op -0.034 ns/op / -0.4% (better)
Windows MSVC BenchmarkRuntimeGetG 2.175 ns/op +0.315 ns/op / +16.9% (worse)
Windows MSVC 386 BenchmarkLookupPCRandom 26.440 ns/op -0.11 ns/op / -0.4% (better)
Windows MSVC 386 BenchmarkMergeCompilerFlags 744.700 ns/op -25.7 ns/op / -3.3% (better)
Windows MSVC 386 BenchmarkMergeLinkerFlags 670 ns/op -27.4 ns/op / -3.9% (better)
Windows MSVC 386 BenchmarkChannelBuffered 39.020 ns/op +0.5 ns/op / +1.3% (worse)
Windows MSVC 386 BenchmarkChannelHandoff 958.400 ns/op +38.5 ns/op / +4.2% (worse)
Windows MSVC 386 BenchmarkDefer 45.590 ns/op +0.8 ns/op / +1.8% (worse)
Windows MSVC 386 BenchmarkDirectCall 1.549 ns/op +0.001 ns/op / +0.1% (worse)
Windows MSVC 386 BenchmarkGlobalRead 1.551 ns/op 0 ns/op / +0.0%
Windows MSVC 386 BenchmarkGlobalWrite 7.774 ns/op -0.009 ns/op / -0.1% (better)
Windows MSVC 386 BenchmarkGoroutine 108929 ns/op -841 ns/op / -0.8% (better)
Windows MSVC 386 BenchmarkInterfaceCall 8.109 ns/op -0.247 ns/op / -3.0% (better)
Windows MSVC 386 BenchmarkRuntimeGetG 2.479 ns/op +0.311 ns/op / +14.3% (worse)
Windows MSVC ARM64 BenchmarkLookupPCRandom 12.080 ns/op -0.09 ns/op / -0.7% (better)
Windows MSVC ARM64 BenchmarkMergeCompilerFlags 569.500 ns/op +2.2 ns/op / +0.4% (worse)
Windows MSVC ARM64 BenchmarkMergeLinkerFlags 540.400 ns/op +18.7 ns/op / +3.6% (worse)
Windows MSVC ARM64 BenchmarkChannelBuffered 36.910 ns/op -0.14 ns/op / -0.4% (better)
Windows MSVC ARM64 BenchmarkChannelHandoff 2195 ns/op +163 ns/op / +8.0% (worse)
Windows MSVC ARM64 BenchmarkDefer 61.640 ns/op +0.2 ns/op / +0.3% (worse)
Windows MSVC ARM64 BenchmarkDirectCall 0.589 ns/op -0.0003 ns/op / -0.1% (better)
Windows MSVC ARM64 BenchmarkGlobalRead 0.590 ns/op +0.0001 ns/op / +0.01696% (worse)
Windows MSVC ARM64 BenchmarkGlobalWrite 3.762 ns/op +0.017 ns/op / +0.5% (worse)
Windows MSVC ARM64 BenchmarkGoroutine 55762 ns/op +542 ns/op / +1.0% (worse)
Windows MSVC ARM64 BenchmarkInterfaceCall 4.137 ns/op -0.093 ns/op / -2.2% (better)
Windows MSVC ARM64 BenchmarkRuntimeGetG 1.769 ns/op -0.025 ns/op / -1.4% (better)
Timer runtime benchmarks
Platform Operation and runtime ns/op vs base
Linux AfterFuncZeroDelivery/Go 935.400 ns/op -3.2 ns/op / -0.3% (better)
Linux AfterFuncZeroDelivery/LLGo 46471 ns/op +8174 ns/op / +21.3% (worse)
Linux CreateStop/Go 293.900 ns/op -17.9 ns/op / -5.7% (better)
Linux CreateStop/LLGo 1735 ns/op -274 ns/op / -13.6% (better)
Linux RearmStopped/Go 116.300 ns/op -5 ns/op / -4.1% (better)
Linux RearmStopped/LLGo 1314 ns/op -96 ns/op / -6.8% (better)
Linux ResetActive/Go 68.880 ns/op -3.13 ns/op / -4.3% (better)
Linux ResetActive/LLGo 684.900 ns/op -60.6 ns/op / -8.1% (better)
Linux ResetHeap1024/Go 67.420 ns/op -3.1 ns/op / -4.4% (better)
Linux ResetHeap1024/LLGo 168.700 ns/op +2.3 ns/op / +1.4% (worse)
macOS AfterFuncZeroDelivery/Go 414.800 ns/op +38.6 ns/op / +10.3% (worse)
macOS AfterFuncZeroDelivery/LLGo 84388 ns/op +11675 ns/op / +16.1% (worse)
macOS CreateStop/Go 127.200 ns/op +10.9 ns/op / +9.4% (worse)
macOS CreateStop/LLGo 472 ns/op +72.8 ns/op / +18.2% (worse)
macOS RearmStopped/Go 59.640 ns/op +3.85 ns/op / +6.9% (worse)
macOS RearmStopped/LLGo 345.300 ns/op +24.9 ns/op / +7.8% (worse)
macOS ResetActive/Go 41.630 ns/op +3.15 ns/op / +8.2% (worse)
macOS ResetActive/LLGo 166.200 ns/op +14.6 ns/op / +9.6% (worse)
macOS ResetHeap1024/Go 42.580 ns/op +3.74 ns/op / +9.6% (worse)
macOS ResetHeap1024/LLGo 81.730 ns/op +19.81 ns/op / +32.0% (worse)
Windows MinGW AfterFuncZeroDelivery/Go 543 ns/op -27.8 ns/op / -4.9% (better)
Windows MinGW AfterFuncZeroDelivery/LLGo 185196 ns/op +2458 ns/op / +1.3% (worse)
Windows MinGW CreateStop/Go 123.600 ns/op +4.9 ns/op / +4.1% (worse)
Windows MinGW CreateStop/LLGo 503.900 ns/op +14.4 ns/op / +2.9% (worse)
Windows MinGW RearmStopped/Go 32.970 ns/op +1.62 ns/op / +5.2% (worse)
Windows MinGW RearmStopped/LLGo 249.500 ns/op +1.3 ns/op / +0.5% (worse)
Windows MinGW ResetActive/Go 20.820 ns/op +0.75 ns/op / +3.7% (worse)
Windows MinGW ResetActive/LLGo 159.300 ns/op +6 ns/op / +3.9% (worse)
Windows MinGW ResetHeap1024/Go 20.840 ns/op +0.44 ns/op / +2.2% (worse)
Windows MinGW ResetHeap1024/LLGo 119.100 ns/op -3.1 ns/op / -2.5% (better)
Windows MinGW 386 AfterFuncZeroDelivery/Go 1051 ns/op +8 ns/op / +0.8% (worse)
Windows MinGW 386 AfterFuncZeroDelivery/LLGo 193152 ns/op +3883 ns/op / +2.1% (worse)
Windows MinGW 386 CreateStop/Go 270.100 ns/op 0 ns/op / +0.0%
Windows MinGW 386 CreateStop/LLGo 1163 ns/op -80 ns/op / -6.4% (better)
Windows MinGW 386 RearmStopped/Go 96.440 ns/op -0.01 ns/op / -0.01037% (better)
Windows MinGW 386 RearmStopped/LLGo 355.500 ns/op -246.2 ns/op / -40.9% (better)
Windows MinGW 386 ResetActive/Go 46.590 ns/op +0.02 ns/op / +0.04295% (worse)
Windows MinGW 386 ResetActive/LLGo 337.600 ns/op +1.6 ns/op / +0.5% (worse)
Windows MinGW 386 ResetHeap1024/Go 47.250 ns/op +0.41 ns/op / +0.9% (worse)
Windows MinGW 386 ResetHeap1024/LLGo 163.900 ns/op +3.1 ns/op / +1.9% (worse)
Windows MinGW ARM64 AfterFuncZeroDelivery/Go 664.800 ns/op +6.4 ns/op / +1.0% (worse)
Windows MinGW ARM64 AfterFuncZeroDelivery/LLGo 149057 ns/op -193 ns/op / -0.1% (better)
Windows MinGW ARM64 CreateStop/Go 200 ns/op -0.1 ns/op / -0.04998% (better)
Windows MinGW ARM64 CreateStop/LLGo 332.500 ns/op -16.5 ns/op / -4.7% (better)
Windows MinGW ARM64 RearmStopped/Go 70.570 ns/op -0.04 ns/op / -0.1% (better)
Windows MinGW ARM64 RearmStopped/LLGo 211.600 ns/op +1.7 ns/op / +0.8% (worse)
Windows MinGW ARM64 ResetActive/Go 31.240 ns/op +0.12 ns/op / +0.4% (worse)
Windows MinGW ARM64 ResetActive/LLGo 110.100 ns/op +3.6 ns/op / +3.4% (worse)
Windows MinGW ARM64 ResetHeap1024/Go 31.160 ns/op +0.03 ns/op / +0.1% (worse)
Windows MinGW ARM64 ResetHeap1024/LLGo 111.600 ns/op -0.6 ns/op / -0.5% (better)
Windows MSVC AfterFuncZeroDelivery/Go 566 ns/op -13.9 ns/op / -2.4% (better)
Windows MSVC AfterFuncZeroDelivery/LLGo 176177 ns/op -3247 ns/op / -1.8% (better)
Windows MSVC CreateStop/Go 126.200 ns/op +4 ns/op / +3.3% (worse)
Windows MSVC CreateStop/LLGo 419.100 ns/op +24.5 ns/op / +6.2% (worse)
Windows MSVC RearmStopped/Go 31.660 ns/op +0.24 ns/op / +0.8% (worse)
Windows MSVC RearmStopped/LLGo 232.400 ns/op -2.4 ns/op / -1.0% (better)
Windows MSVC ResetActive/Go 20.230 ns/op +0.12 ns/op / +0.6% (worse)
Windows MSVC ResetActive/LLGo 149.800 ns/op -6.8 ns/op / -4.3% (better)
Windows MSVC ResetHeap1024/Go 20.510 ns/op +0.01 ns/op / +0.04878% (worse)
Windows MSVC ResetHeap1024/LLGo 118.600 ns/op -0.9 ns/op / -0.8% (better)
Windows MSVC 386 AfterFuncZeroDelivery/Go 953 ns/op +12.7 ns/op / +1.4% (worse)
Windows MSVC 386 AfterFuncZeroDelivery/LLGo 204763 ns/op -110 ns/op / -0.1% (better)
Windows MSVC 386 CreateStop/Go 190.800 ns/op -1 ns/op / -0.5% (better)
Windows MSVC 386 CreateStop/LLGo 497.100 ns/op +28.5 ns/op / +6.1% (worse)
Windows MSVC 386 RearmStopped/Go 63.400 ns/op -0.12 ns/op / -0.2% (better)
Windows MSVC 386 RearmStopped/LLGo 304.400 ns/op -3.4 ns/op / -1.1% (better)
Windows MSVC 386 ResetActive/Go 39.060 ns/op 0 ns/op / +0.0%
Windows MSVC 386 ResetActive/LLGo 911 ns/op +34.3 ns/op / +3.9% (worse)
Windows MSVC 386 ResetHeap1024/Go 39.410 ns/op +0.01 ns/op / +0.02538% (worse)
Windows MSVC 386 ResetHeap1024/LLGo 161.800 ns/op +1.8 ns/op / +1.1% (worse)
Windows MSVC ARM64 AfterFuncZeroDelivery/Go 659.600 ns/op -12 ns/op / -1.8% (better)
Windows MSVC ARM64 AfterFuncZeroDelivery/LLGo 144499 ns/op +3993 ns/op / +2.8% (worse)
Windows MSVC ARM64 CreateStop/Go 198.700 ns/op -0.6 ns/op / -0.3% (better)
Windows MSVC ARM64 CreateStop/LLGo 359.800 ns/op +8.9 ns/op / +2.5% (worse)
Windows MSVC ARM64 RearmStopped/Go 70.550 ns/op +0.23 ns/op / +0.3% (worse)
Windows MSVC ARM64 RearmStopped/LLGo 225.500 ns/op -0.4 ns/op / -0.2% (better)
Windows MSVC ARM64 ResetActive/Go 31.070 ns/op +0.25 ns/op / +0.8% (worse)
Windows MSVC ARM64 ResetActive/LLGo 117.500 ns/op +4.3 ns/op / +3.8% (worse)
Windows MSVC ARM64 ResetHeap1024/Go 31.040 ns/op +0.17 ns/op / +0.6% (worse)
Windows MSVC ARM64 ResetHeap1024/LLGo 116.800 ns/op +1.1 ns/op / +1.0% (worse)

Compared with 1bbfc1f75db4 measured in the same runner job.

@cpunion
cpunion force-pushed the codex/wasm-js-cross-worker-20261006 branch 2 times, most recently from 8cbc757 to b76a7b8 Compare October 6, 2026 00:15
@cpunion
cpunion force-pushed the codex/wasm-js-cross-worker-20261006 branch from b76a7b8 to 8f26b66 Compare October 6, 2026 01:26
@cpunion

cpunion commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

@fennoai please review the updated single commit. It adds cooperative finalizer locking, callback scheduling/registry fixes and regression coverage. Complete local worker acceptance and the Node/Chrome 2/4-worker Memory32/64 JS matrix pass; no CI jobs were retried.

@fennoai

fennoai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Re-review complete

Reviewed the updated commit (8f26b66) across code-quality, performance, security/concurrency, and documentation passes. Posted a PR review: #pullrequestreview-5422927013.

All three findings from the prior review are resolved: build-tag coverage gap, local-path closure allocation (now //go:noinline remoteX helpers), and the stale jsCalls channel after Goexit.

New mechanisms verified sound: the callback scheduling state machine (jsCallbackID/callbackResume/popWasmCallbackChild), runqueue.Remove/Front invariants + tests, the cooperative finalizer locking contract, and the emscripten_set_immediate latency change. No memory-safety, use-after-free, deadlock, or data-race defects found.

Remaining notes (all minor):

  • [P2] popWasmCallbackChild + runqueue.Remove do two O(n) scans per pop on the callback-active path → can degrade to O(n²); the scan already holds previous and could splice in place.
  • [P3] callbackResume is a per-worker flag read only against the top event — bounded away from corruption, but worth scoping per wasmJSEvent or documenting the invariant.
  • Stale "realm affinity" comment at proc_wasm_workers.go:47.
  • A possible dropped-finalizer ordering window at mfinal_wasm.go:97-114 that appears mitigated by KeepAlive — flagged for author confirmation.

View job run

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

Re-review of updated commit (8f26b66)

Reviewed the full PR diff at the new head. This revision is high quality and addresses every finding from my prior review:

  • Build-tag gap fixed — worker_call_single.go, func_mutex_other.go, and mfinal_lock_wasm_other.go now use the (!llgo || !llgo.wasm.workers) form, so the matrix is fully covered with no gap or overlap.
  • Local-path closure allocation fixed — forwarding closures are isolated in //go:noinline remoteX helpers (worker_calls.go), so SSA no longer hoists the heap alloc before the isRemoteJSWorker() check; the worker-0 fast path is allocation-free and the helpers are dead-code-eliminated in single-worker builds.
  • Stale jsCalls after Goexit fixed — CallMainJSWorker now detaches (jsCalls = nil) and closes the channel before Goexit(), and finishWasmG guards on non-nil, so there is no double-close.

I verified the new mechanisms the commit introduces:

  • Callback scheduling (popWasmWorkerRunq/popWasmCallbackChild + jsCallbackID + callbackResume): children are drained before reopening a host callback stack; id 0 is reserved as the "no callback" sentinel so non-callback children never spuriously match; runqueue.Remove preserves head/tail/size/queued invariants on every branch and is covered by the new runqueue_test.go.
  • Finalizer locking (mfinal_wasm.go + mfinal_lock_wasm_{workers,other}.go): the lazy-init-returns-locked contract is honored at all four call sites with a matching unlock on every path; the collector callback runs callWasmFinalizer outside the lock. No deadlock or missed unlock.
  • emscripten_set_immediate (workers.c): correctly removes the setTimeout(0) ~4ms nested-timeout clamp from every cross-worker handoff; available under the pthread/worker targets used here.

No memory-safety, use-after-free, deadlock, or data-race defects were found. The notes below are minor (performance + stale comments + one robustness hardening).

Notes not placed inline (outside diff hunks or whole-mechanism):

  • Stale "realm affinity" comment — runtime/internal/runtime/proc_wasm_workers.go:47: // Override both inherited realm affinity and callback-event pinning still describes the removed jsRealm model. In the new code, independentSpawn gates only the callback-event-pinning branch; worker affinity is now carried by spawnWorker (read first at line 316) and is unaffected by independentSpawn. Suggest rewording to reference only callback-event pinning (and jsCallbackID inheritance), dropping "both … realm affinity."
  • Possible dropped-finalizer window (please confirm) — mfinal_wasm.go:97-114: tinygogc.AddFinalizer registers the collector record (line 97) before the entry is published into wasmFinalizers.m[key] (line 113); the callback early-returns if m[key] != entry. This looks mitigated by KeepAlive(obj) at line 115 (the object stays reachable past the map store, so the collector cannot queue the callback in that window), but it is worth a quick confirmation that objPtr cannot be collected independently of what KeepAlive(obj) retains (e.g. interior pointers). If safe, a one-line comment noting the KeepAlive ordering guarantee would make the intent explicit.

Comment thread runtime/internal/runtime/proc_wasm_workers.go
Comment thread runtime/internal/runtime/proc_wasm_workers.go Outdated
@cpunion
cpunion force-pushed the codex/wasm-js-cross-worker-20261006 branch from 8f26b66 to 3234270 Compare October 6, 2026 02:11
@cpunion

cpunion commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

@fennoai please verify the two updated findings at 323427074: callback child removal no longer rescans the queue, and resume intent is scoped to each nested event. Full local worker acceptance and optimized O3/Os/Oz Node/Chrome checks pass. The CI timeout fix retains all 128 filesystem tasks, GC calls and the successful-exit check, with a 120-second browser budget for that stress case; no CI jobs were retried.

@fennoai

fennoai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Both findings verified fixed at 3234270

[P2] Callback child removal no longer rescans the queue — ✅ confirmed.

  • runqueue.go adds RemoveAfter(previous), which splices out previous.RunqueueNext() in O(1); Remove now delegates to it.
  • popWasmCallbackChild tracks previous through its single Front()→RunqueueNext() scan and calls RemoveAfter(previous) on a match, so the redundant second O(n) traversal is gone. The N-sibling drain is no longer O(n²). (popWasmWorkerRunq's Remove(e.gp) for resuming the callback's own G is a single-node lookup, not the sibling loop, so the quadratic path is eliminated.)
  • previous lags candidate by exactly one node and is advanced even when candidate == e.gp is skipped, so RemoveAfter(previous) removes the correct child, including when the match immediately follows e.gp.
  • New TestQueueRemoveAfterCursor covers tail-cursor (no-op), mid-cursor removal + reuse + FIFO order, and front removal via a nil cursor, with head/tail/length assertions.

[P3] Resume intent scoped to each nested event — ✅ confirmed.

  • The per-worker callbackResume bool is removed entirely (no references remain anywhere in runtime/).
  • Resume intent now lives on wasmJSEvent.resume, initialized true per event; popWasmWorkerRunq reads/writes e.resume on the top-of-stack event only. Each nested event therefore carries its own resume intent, and HandleWasmEvent restores the outer jsCallbackID on unwind. The stale-flag-across-events hazard is gone.

No regressions observed in the touched scheduling path: the jsCallbackID sentinel-0 convention, the PollWasmEvent _Gwaiting gate, and the finalizer-lock contract are all unchanged from the previously-reviewed revision. I did not re-run the acceptance/CI matrix; I'm relying on your note that local worker acceptance, the O3/Os/Oz Node/Chrome checks, and the CI-timeout stress case (128 filesystem tasks, GC calls, success-exit check, 120s browser budget) pass.

View job run

@cpunion
cpunion force-pushed the codex/wasm-js-cross-worker-20261006 branch from 3234270 to 4bd1194 Compare October 6, 2026 03:02
@cpunion

cpunion commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

@fennoai please review 4bd1194da. The implementation audit keeps the shared API and existing build-tag adapters: optimized single-worker forwarding checks disappear; an explicit constant produced an identical Wasm code section and gave no O0 size reduction.

The focused changes avoid remote requests for reserved primitive Type checks and ValueOf(Value/Func/nil/bool), isolate host-only captures in a noinline helper, and use the nonpreempting funcMutex for callback poll registration too. The remote primitive benchmark (5000 Type/ValueOf pairs) changed from a median 149 ms/about 5 MB to 0.25 ms/0 bytes; this does not describe general JS host calls. The executor regression fails against the old forwarding implementation and passes the final implementation.

Consolidated the 128-task GC stress in dedicated Node/Chrome worker acceptance, with an explicit stress argument; optimized filesystem smoke remains covered. Removed the generic browser timeout configuration and optimization timeout changes from this PR. The dedicated Memory64 browser stress has a bounded budget; other worker checks retain theirs. The filesystem success marker now appears only after successful cleanup, and an intentional panic is rejected by both Node and Chrome without that marker. No CI jobs were retried.

Complete local worker acceptance, O3/Os/Oz Node/Chrome boundaries, single-worker JS and runqueue tests pass. Final filesystem checks pass all eight host/width/worker combinations. Same-checkout-path size comparison against main: cprintf/println unchanged, fmtprintf +3955 bytes (0.13%; code +312 bytes). Full source-specific qualification: https://github.com/cpunion/llgo-compat-ci/actions/runs/37407152952 .

Two clarifications from the previous review: RemoveAfter is O(1) unlinking, but child matching still scans the queue; repeated scans behind unrelated nodes can still be quadratic, so there is no general O(1) scheduling claim. The finalizer publication order is unchanged from main: obj remains an SSA use through KeepAlive, which makes the compiler publish its interface pointer in the GC root frame across AddFinalizer and registry publication; roots remain linked through the function return. cl/gcroot.go and the root-frame implementation provide that lifetime, rather than an assumed LLVM KeepAlive intrinsic. Requests still allocate state and executors retain fibers until their callers exit; these costs are now documented.

@fennoai

fennoai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review complete — 4bd1194

Posted a PR review: #pullrequestreview-5423460886.

I verified the focused changes adversarially and found no correctness issues:

  • Value.Type() and remoteValueOf fast paths are realm-invariant (reserved emval handles 2/4/6/8 are global constants; dynamic handles start ≥10, so no collision) — correct on any worker without a remote round-trip.
  • The func.go lock/poll reorder and funcMutex switch introduce no race, no lock-ordering inversion, and cannot wrongly disable the poll.
  • New regression tests directly cover the portable-primitive, nested-callback-yield, executor-lifetime, and finalizer-contention paths.

Your two clarifications are both correct and I've updated accordingly: scheduling is not O(1) (child matching still scans — RemoveAfter only makes the unlink O(1)), and the finalizer publication order is sound via the GC-root-frame KeepAlive lifetime (cl/gcroot.go), resolving my earlier "please confirm" note.

Only one minor readability note (inline): add a one-line comment explaining why Type() checks reserved refs before the remote-dispatch branch, since it's the reverse of the other methods.

I relied on your reported acceptance/CI results (local worker acceptance, O3/Os/Oz Node/Chrome, single-worker JS + runqueue tests, 8-combination filesystem checks, and the linked compat-CI run); I don't have CI-log access and didn't re-run the matrix.

View job run

@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 of 4bd1194

Reviewed the incremental changes against the previously-reviewed head. This revision is focused and correct; I verified the new fast paths adversarially and found no correctness issues.

New local fast paths verified realm-invariant (the correctness-sensitive part):

  • Value.Type() now resolves the reserved refs (undefined/null/true/false, handles 2/4/6/8) before the remote-dispatch check. These are compile-time constants (not TLS) and match Emscripten's globally-reserved emval handles — dynamic handles start at ≥10, so there is no collision, and answering locally needs no realm state. Correct on any worker.
  • remoteValueOf handles Value/Func/nil/bool locally (returns a copy or a reserved handle) and forwards the rest via the //go:noinline forwardValueOf, keeping request allocations in the host-only helper. Each local case is realm-invariant.
  • func.go: moving emval_install_invoke/ensureCallbackPoll outside funcsMu, switching callbackPoll to the nonpreempting funcMutex, and gating stopCallbackPollLocked behind !keepWasmCallbackPoll introduce no race, no lock-ordering inversion (funcsMu → callbackPoll is the only nesting), and cannot wrongly disable the poll (const-true in the workers build).

The stale "realm affinity" comment flagged earlier is now accurate, and no MarkCurrentJSRealm/jsRealm references remain. The new regression tests (testPortablePrimitives asserting portable conversions create no executor, testCallbackYield covering nested-event resume scope, testExecutorLifetime, testFinalizerContention) directly exercise these paths.

On your two clarifications — both acknowledged and correct:

  • Scheduling is not O(1). Agreed, and my earlier framing overstated it. RemoveAfter makes the unlink O(1), but popWasmCallbackChild still linearly scans for the matching child, so repeated scans behind unrelated nodes remain potentially quadratic. There is no general O(1) scheduling claim — thanks for the correction.
  • Finalizer publication order. Understood: obj stays an SSA use through KeepAlive, so the compiler keeps its interface pointer live in the GC root frame across AddFinalizer and registry publication (via cl/gcroot.go + the root-frame implementation, not an LLVM KeepAlive intrinsic), and roots stay linked through the function return. That resolves my prior "please confirm" note; the ordering is unchanged from main and sound.

The documented costs (per-request state allocation, executors retaining fibers until their callers exit, non-O(1) child matching) are the right things to document. One minor readability note inline; otherwise no further findings.

Comment thread runtime/internal/lib/syscall/js/js.go
@cpunion
cpunion force-pushed the codex/wasm-js-cross-worker-20261006 branch from 4bd1194 to 2d5697d Compare October 6, 2026 03:28
@cpunion cpunion changed the title wasm: share syscall/js values across scheduler workers wasm: support cross-worker syscall/js values and callbacks Oct 6, 2026
@cpunion
cpunion force-pushed the codex/wasm-js-cross-worker-20261006 branch from 2d5697d to 411e3ca Compare October 6, 2026 04:20
@cpunion

cpunion commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

The CI callback timeout is fixed at 411e3caab. Under the same 0.18-CPU Linux quota, the old outer batch wait fails while callback operations still progress; reporting child JS completion, synchronous return and external event completion passes the entire batch, even though its total duration exceeds 30 seconds. All eight callbacks, 128 nested/yielding calls, GC and synchronous identity assertions remain; each wait still has its original 30-second no-progress bound and the outer process bound is unchanged.

Full local worker acceptance passes: Node/Chrome, Memory32/64, 2/4 workers, private roots, finalizers, sync.Pool and filesystem GC stress. It also exposed a nondeterministic portable-primitive assertion: process-global NumGoroutine can grow when a background G starts. The assertion now checks this caller's actual executor through a small internal testing accessor, which is unused and removed from ordinary modules. No CI jobs were retried.

@fennoai please review these focused test changes and the executor accessor.

@fennoai

fennoai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review complete — 411e3ca

Posted a PR review: #pullrequestreview-5423808252. The real diff vs the prior head is just three files (test-only + one test-only accessor), all correct — no findings.

  • js.go adds the exact Type() ordering comment I suggested last round (resolving my only open note).
  • HasMainJSWorkerExecutorForTesting() is a read-only, nil-guarded, build-tag-gated accessor referenced only by the test via //go:linkname, following the package's established *ForTesting convention — confirmed no ordinary-module caller.
  • testPortablePrimitives swaps the flaky NumGoroutine() delta for a precise per-caller executor check (and now also asserts a fresh caller starts with no executor).
  • testCallbacks is the CI-timeout fix: a progress channel drained 8*3 times bounds each child/sync-return/external-event axis independently instead of charging all concurrent work to one 30s timer; the counts verify (3 sends × 8 = 24).

Behavioral invariants from prior revisions are intact. I relied on your reported local acceptance results and don't have CI-log access.

View job run

@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 of 411e3ca

Scoped the diff to the three files that actually differ from the previously-reviewed head (4bd1194). All changes are test-only plus one test-only accessor, and all are correct. No findings.

  • js.go Type() — adds the one-line comment I suggested last round (// Reserved refs are valid in every realm; resolve them before remote dispatch.), resolving my only outstanding note. No behavior change.
  • HasMainJSWorkerExecutorForTesting() (js_worker_call_wasm_workers.go) — a read-only, nil-guarded accessor (gp != nil && gp.context.platform.jsCalls != nil) under the llgo && js && wasm && llgo.wasm.workers build tag. Referenced only by test/wasm/js-workers/main.go via //go:linkname; no ordinary-module caller. It follows the package's established *ForTesting convention (GMPForTesting, CurrentGForTesting, ParkForTesting, ReadyForTesting, SchedulerStateForTesting).
  • testPortablePrimitives — replaces the flaky runtime.NumGoroutine() before/after delta (which a background timer/finalizer G could perturb) with hasMainJSWorkerExecutor() checks that inspect this caller's executor directly. Strictly more precise and deterministic, and it also asserts a fresh caller starts without an executor.
  • testCallbacks — the CI-timeout fix: swaps the single aggregate result channel (8 waits) for a progress channel drained 8*3 times, bounding each child JS completion, synchronous return, and external event on its own axis instead of charging all concurrent GC/callback work to one 30s timer. The counts line up: each outer iteration emits 3 progress sends (nested child + post-invoke + post-external-callback) × 8 = 24 = 8*3. The original 30s no-progress bound per wait and the outer process bound are unchanged, as described.

The behavioral invariants from prior revisions (all eight callbacks, 128 nested/yielding calls, GC, synchronous identity assertions) remain in place. I relied on your reported local worker acceptance results (Node/Chrome, Memory32/64, 2/4 workers, finalizers, sync.Pool, filesystem GC stress); I don't have CI-log access and did not re-run the matrix.

@cpunion
cpunion force-pushed the codex/wasm-js-cross-worker-20261006 branch from 411e3ca to 87995c1 Compare October 6, 2026 05:05
@cpunion

cpunion commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

The nested callback GC stress is corrected at 87995c126. CI at 411e3caab reached the synchronous/external callback phase but still accumulated eight serialized collections inside each callback's result deadline. The test now waits until all eight callback children are parked, performs one collection while every callback's JS roots are live, and releases the children together. This covers all eight callback roots simultaneously; concurrent GC stress remains in the separate shared-values and finalizer tests. All 30-second waits, eight synchronous/external callbacks and 128 yielding/nested invocations remain.

Full local Node/Chrome worker acceptance passes (Memory32/64, 2/4 workers, private roots, finalizers, sync.Pool and filesystem GC). The callback/shared-value probe also passes under an emulated Linux 0.18-CPU quota. No CI jobs were retried; the code push triggered fresh CI.

Standard-library qualification additionally exposed worker-local libffi MakeFunc table entries. A separate follow-up contribution will address that compatibility failure.

@fennoai please review the callback GC barrier change.

@fennoai

fennoai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review complete — 87995c1

Posted a PR review: #pullrequestreview-5424011873. The only change vs the prior head is the callback GC barrier in testCallbacks (one file, +12/-1) — correct and well-commented, no findings.

The barrier replaces eight serialized in-callback runtime.GC() calls with a coordinated single collection: child Gs signal gcReady and park on gcRelease; the main G waits for all 8, runs one GC while every callback's JS roots are live, then close(gcRelease) to release them together. I verified no deadlock / no lost signal (gcReady buffered to exactly 8; close is the right one-shot broadcast), that the single collection covers all eight root sets, and that the progress accounting (8*3) still balances. Coverage isn't reduced — concurrent GC stress remains in the shared-values and finalizer tests.

The libffi MakeFunc follow-up is out of scope. I relied on your reported local acceptance results and have no CI-log access.

View job run

@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 of 87995c1

The only change vs the prior reviewed head (411e3ca) is the callback GC barrier in testCallbacks (test/wasm/js-workers/main.go, +12/-1). It is correct and well-commented — no findings.

The barrier works as intended:

  • Each callback's child G now signals gcReady <- 1 and parks on <-gcRelease instead of calling runtime.GC() itself.
  • The main G drains all 8 gcReady signals (so every callback child is parked with its JS roots live), runs one runtime.GC(), then close(gcRelease) to release all children together.

Correctness points I checked:

  • No deadlock / no lost signal — gcReady is buffered to exactly 8 (one per callback child), so no child blocks on the send before parking on gcRelease; the main G is free to reach the drain loop while the callback Gs park in await(result) and the children run on pool workers. close(gcRelease) is the correct one-shot broadcast release for all 8 blocked receivers.
  • Single collection covers all roots — GC now runs once while all eight callbacks' roots are simultaneously live, replacing eight serialized collections that each charged their cumulative wall time to an independent 30-second result deadline. This matches the stated CI-timeout fix.
  • progress accounting unchanged — the child still emits its progress <- 1 after the barrier, so the final 8*3 drain still balances; gcReady/gcRelease are separate channels and don't interfere. All 30-second waits, the eight synchronous/external callbacks, and the 128 yielding/nested invocations remain.

Concurrent (unsynchronized) GC stress is still exercised by the separate shared-values and finalizer tests, as you note, so moving this one test to a coordinated single collection doesn't reduce coverage.

Noted for the record: the follow-up on worker-local libffi MakeFunc table entries is out of scope here. I relied on your reported local acceptance results (Node/Chrome, Memory32/64, 2/4 workers, the 0.18-CPU-quota probe); I have no CI-log access and did not re-run the matrix.

@cpunion
cpunion force-pushed the codex/wasm-js-cross-worker-20261006 branch from 58f7baa to d2611e8 Compare October 6, 2026 07:58
@cpunion

cpunion commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

@fennoai The final scan follow-up is d2611e8: skip only aligned, bounded, completely zero 16-byte spans on Memory32; nonzero words still use the original four-byte Go/C pointer scan. Full test/go passes on latest main for Memory32/64, and the WAMR threaded-GC acceptance passes. All allocator handoff/scan changes are now rebased on main f167e3c; the experimental ticket lock, epoch-loop change and reflection cache are not included.

@fennoai

fennoai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review complete — d2611e8

Posted a PR review: #pullrequestreview-5425550618. The PR was rebased on a newer main (f167e3c), so I reviewed against the new declared base (three-dot) to exclude the upstream SIMD/CPU-feature churn. The only new logic this revision is the zero-16-byte-span scan fast path — correct, no findings.

I verified it: platform-gated to Memory32 via the gcScanZeroSpans const (dead-code-eliminated on memory64); both uint64 loads are 8-aligned (addr&7==0) and bounded within the object (end-addr >= 16); it skips only all-zero 16-byte spans so no pointer can be missed, with any nonzero sub-word falling through to the original 4-byte scan; the +16 - gcScanWordSize plus loop +4 advances exactly one 16-byte span; and since blocks are 16 bytes (bytesPerBlock = 16 on wasm32), the addr != end loop still terminates exactly. The adjacent word == 0 skip was already reviewed at 584723c.

I also confirmed the experimental ticket-lock / epoch-loop / reflection-cache you excluded are not in the diff. I relied on your reported acceptance results (no CI-log access).

View job run

@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 of d2611e8 (final scan follow-up, rebased on f167e3c)

I diffed the PR against its new declared base (f167e3c...d2611e8) so the rebase's upstream SIMD/CPU-feature churn is excluded — that machinery is part of the new base, not this PR. The PR's own change set is the familiar wasm-workers + GC content I've reviewed across prior revisions, and the only new logic this revision is the zero-16-byte-span scan fast path in tinygogc. It is correct — no findings.

Zero-span scan fast path (gc_tinygo.go startMark, with scanword_wasm32.go/scanword_default.go):

if gcScanZeroSpans && addr&7 == 0 && end-addr >= 16 &&
    *(*uint64)(addr) == 0 && *(*uint64)(addr+8) == 0 { addr += 16 - gcScanWordSize; continue }
  • Platform-gated: gcScanZeroSpans is a const — true only in scanword_wasm32.go (!memory64, gcScanWordSize = 4), false in scanword_default.go (memory64 / baremetal). So the 16-byte skip compiles in only on Memory32 and is dead-code-eliminated elsewhere, matching "on Memory32."
  • Alignment: addr&7 == 0 makes both *(*uint64) loads 8-aligned. Blocks are bytesPerBlock = wordsPerBlock(4) × 4 = 16 bytes on wasm32, so start/end are 16-aligned and addr stays 16-aligned across fast-path hops (+16) and 4-aligned on the fallthrough (+4).
  • Bounded: end-addr >= 16 guarantees both loads and the +16 advance stay within [start,end) — no read past the object and, since end is 16-aligned, the addr != end loop still terminates exactly.
  • No missed pointer: the span is skipped only when all 16 bytes are zero (= four zero 4-byte words), which hold no pointers. Any nonzero sub-word fails the condition and falls through to the original 4-byte loadGCScanWord scan, preserving Go-low-word / C-either-word discovery. The conservative scan cannot drop a live root.
  • Advance math: addr += 16 - gcScanWordSize (+12) plus the loop's +gcScanWordSize (+4) = exactly one 16-byte span, no word skipped or overshot.

The adjacent word == 0 single-word skip was already reviewed at 584723c and is unchanged.

I confirmed the experimental ticket lock / epoch-loop / reflection cache you mentioned are not present in the diff. I relied on your reported acceptance results (test/go on latest main for Memory32/64; WAMR threaded-GC acceptance); I have no CI-log access and did not re-run the matrix.

@cpunion

cpunion commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

@fennoai Please review the GC metadata follow-up in 1bf3662. The full Memory32 worker qualification hit its 60-second package deadline during repeated collections. Contiguous block lookup now uses the single arena directly. Object-end lookup, overflow rescanning and sweeping batch only complete metadata bytes; mixed/partial bytes retain individual state handling, and freed tail groups are cleared over the same heap range as before. The new wide-graph regression exercises mark-stack overflow, mixed allocation lengths, live contents and zeroed new allocations on Memory32/64. Full test/go passes locally in about 24/7 seconds under the original 60-second limit; threaded GC passes on both WAMR and pinned patched Wasmer. #2743 is rebased onto this change; current contribution CI is running.

@fennoai

fennoai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review complete — 1bf3662

Posted a PR review: #pullrequestreview-5425956521. Clean three-file delta (same base as prior head) — the GC whole-metadata-byte batching plus a wide-graph regression. All correct, no findings.

I verified each batch against the 2-bit state encoding (Free=00/Head=01/Tail=10/Mark=11, 4 blocks/byte, all-tails=0xAA):

  • segmentForBlock contiguous fast path — const-gated, first ≤ block ≤ last, identical panic path.
  • gcFindNextIn all-tails skip — forward mirror of the existing gcFindHeadIn byte-skip; an all-tails byte has no object boundary.
  • finishMark no-mark skip — the stateByte & (stateByte>>1) & 0x55 == 0 test is exactly "no Mark(11) nibble", and finishMark only acts on Mark blocks.
  • sweep all-free / all-tails batches — I confirmed they reproduce the per-block Free counting and tail-freeing exactly, including that four gcMarkFreeIn calls both clear the metadata byte and zero the same 4 × bytesPerBlock heap range the batch Memsets, with freeCurrentObject threaded correctly.

All batches fire only on complete aligned bytes with ≥4 blocks left, so partial bytes keep per-state handling and the advance is exactly one byte. The new gc_bitmap_test.go exercises all four paths (mark-stack overflow, mixed lengths, live contents, zeroed allocations). I relied on your reported acceptance results (no CI-log access).

View job run

@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 of 1bf3662 (GC metadata follow-up)

Both heads share base f167e3c, so this is a clean three-file delta: whole-metadata-byte batching in tinygogc plus a wide-graph regression. I verified each batch is behaviorally identical to the per-block path it accelerates (states: Free=00, Head=01, Tail=10, Mark=11; blocksPerStateByte = 4; blockStateByteAllTails = 0xAA). No findings.

  • segmentForBlock contiguous fast path — gated on const !segmentedHeap; checks heapSegments[0] with first <= block <= last (the inclusive upper bound matches the binary-search path's in-range boundary) and panics identically otherwise. Mirrors the segmentForAddress fast path from the prior revision.
  • gcFindNextIn all-tails skip — when byte-aligned with ≥4 blocks left and the byte is 0xAA (four tails), skip 4; otherwise fall through to the per-block blockAddr++. This is the forward mirror of the long-standing gcFindHeadIn byte-skip, and an all-tails byte provably contains no object boundary.
  • finishMark no-mark-byte skip — the test stateByte & (stateByte>>1) & (blockStateByteAllTails>>1) == 0 is exactly "no 11 (Mark) nibble": & (>>1) sets a state's low bit iff both its bits are 1 (Mark), and 0xAA>>1 = 0x55 masks the low bit of each 2-bit state. Since finishMark only acts on Mark blocks, skipping a byte with no marked head is a no-op — correct.
  • sweep all-free / all-tails batches — stateByte == 0 adds 4 × bytesPerBlock free bytes (identical to four blockStateFree cases, and leaves freeCurrentObject untouched). stateByte == 0xAA with freeCurrentObject zeroes the metadata byte and Memsets 4 × bytesPerBlock of heap, freed += 4 — I confirmed this exactly reproduces four gcMarkFreeIn calls, which each clear the nibble and zero the block's wordsPerBlock words, so the heap range cleared is the same as before. With freeCurrentObject == false the tails are live and skipped untouched, matching the per-block path.

All batches are gated on %blocksPerStateByte == 0 && remaining >= blocksPerStateByte, so partial/trailing bytes always fall through to the exact per-state handling, and block += blocksPerStateByte - 1 + the loop block++ advances exactly one byte with no overshoot. "Mixed/partial bytes retain individual handling" and "freed tails cleared over the same range" both hold.

The new gc_bitmap_test.go (wide graph → mark-stack overflow, varied object lengths, live contents, zeroed new allocations) directly exercises all four touched paths on Memory32/64.

I relied on your reported results (test/go ~24/7s under the 60s limit; threaded GC on WAMR and pinned patched Wasmer); I have no CI-log access and did not re-run the matrix.

@cpunion

cpunion commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

@fennoai Please review the two follow-ups in ab4626f (rebased on main be669ac). Browser Memory64 filesystem GC stress exposed raw malloc buffers held only by JS numbers; payload/request/response now use explicit rooted allocations and release every root in finally. The regression forces collection between allocations, reproduces zeroed payloads with the old proxy, and checks Memory32/64 data and root cleanup. Actual Chrome stress passes in both widths.

The GC scan now batches four aligned metadata bytes (16 blocks), bounded by each arena's usable blocks, with byte fallback at mixed/partial boundaries. Existing live graph/overflow tests pass in Memory32/64; host tests exercise metadata alignment, end boundaries, retained/freed tails, cleared storage and counters. WAMR and pinned Wasmer threaded GC pass. The host extraction test now includes the real batch constants and segmented-heap selector, fixing its native CI build failure. No CI retries or deadline increases.

@fennoai

fennoai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review complete — ab4626f

Posted a PR review: #pullrequestreview-5426571302. Reviewed against the new base (three-dot) to exclude the rebase's upstream churn. Both follow-ups correct — no findings.

  • Browser FS rooted-allocation fix — a real GC-safety fix: the proxy's payload/request/response buffers now use AllocRoot/FreeRoot (I confirmed tinygogc.AllocRoot links them into the scanned, uncollectable rootAllocations list), so a collection between allocations can no longer reclaim buffers held only by JS Numbers (the Memory64 bug). All three are released in finally; export names and __deps match; float64 keeps the boundary numeric in both widths. The .mjs regression models collection and asserts no leaked roots.
  • 16-block (4-byte word) metadata batching — sound 4× extension of the byte batching I verified last revision: blockStateWordAllTails = 0xAAAAAAAA, the no-mark word test & 0x55555555, and the sweep all-free/all-tails word cases are exact 16-block analogues, each gated on 4-byte-aligned metadata (metadata&3==0) and ≥16 remaining blocks, with byte-batch and per-block fallbacks for partial/mixed boundaries. gcFindNextIn's restructure preserves break-on-first-non-tail.

The host-extraction test now includes gcFindNextIn, the batch consts, and the segmentedHeap selector (fixing the native build), plus new boundary/contiguous-lookup tests. I relied on your reported acceptance results (no CI-log access).

View job run

@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 of ab4626f (two follow-ups, rebased on be669ac)

Reviewed against the new base (three-dot) so the rebase's upstream churn (setup-gdb, ssa/di.go, test/goroot/*) is excluded. Both follow-ups are correct — no findings.

1. Browser FS rooted-allocation fix — a genuine GC-safety fix.
The proxy's payload/request/response buffers were allocated with raw _malloc, whose only reference was a JS Number — invisible to the Go collector, so a collection between allocations could reclaim and zero them (the Memory64 stress failure). They now use llgo_browser_fs_malloc/_free, which route through AllocRoot/FreeRoot. I confirmed tinygogc.AllocRoot links the allocation into the rootAllocations list (scanned, uncollectable until freed — equivalent to GC_malloc_uncollectable), so the buffers survive any intervening collection, and the finally block releases all three on every exit path via FreeRoot (which unlinks and frees). The //export llgo_browser_fs_malloc/_free names match the JS _llgo_browser_fs_* calls and the updated __deps; float64 keeps the boundary numeric in both widths (a Memory64 address fits a double's 53-bit mantissa for realistic heaps). Build tag llgo && js && wasm is right. The .mjs test now zeroes non-rooted allocations on each _malloc (modeling collection) and asserts roots.size == 0, directly reproducing the old zeroed-payload bug and verifying root cleanup in both widths.

2. 16-block (4-byte word) metadata batching — sound extension of the byte batching reviewed previously.
blockStateWordAllTails = 0xAA × 0x01010101 = 0xAAAAAAAA; the finishMark no-mark test states & (states>>1) & (blockStateWordAllTails>>1 = 0x55555555) == 0 is the exact 16-block analogue of the byte test (11/Mark detection); the sweep word cases (states == 0 → 16×free; states == 0xAAAAAAAA + freeCurrentObject → zero the metadata word + Memset 16×bytesPerBlock + freed += 16) reproduce 16 per-block operations, consistent with the gcMarkFreeIn metadata-clear + block-zero semantics I verified last revision. Every word path is gated on metadata&3 == 0 (safe aligned 32-bit read) and segment.last-block >= blocksPerStateWord, falling back to the single-byte batch and then per-block for 4-byte-unaligned metadata, partial/trailing bytes, or mixed states — so "bounded by each arena's usable blocks, byte fallback at mixed/partial boundaries" holds. gcFindNextIn's loop restructure preserves break-on-first-non-tail. The advance (+= blocksPerStateWord - 1 then loop block++) is exactly 16 blocks.

The host extraction test (wasm_gc_segments_test.go) now pulls in gcFindNextIn, the blockStateByteAllTails/blocksPerStateWord consts, and a segmentedHeap selector, which is what fixes the native CI build; the new TestSweepMetadataBatches (8 offsets × keep/free) and TestContiguousHeapLookup exercise the batch boundaries, retained/freed tails, cleared storage, counters, and the contiguous lookup at partial-byte offsets.

I relied on your reported results (Chrome Memory32/64 FS stress; live-graph/overflow + host metadata tests; WAMR and pinned Wasmer threaded GC); I have no CI-log access and did not re-run the matrix.

@cpunion
cpunion force-pushed the codex/wasm-js-cross-worker-20261006 branch from ab4626f to 9064d9a Compare October 13, 2026 11:33

@visualfc visualfc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Non-blocking documentation note only. The forwarding/executor protocol looks correct; please spell out that a remote nested callback runs on the per-G executor, not the originating G.

Comment thread runtime/internal/lib/syscall/js/func.go Outdated
@cpunion
cpunion merged commit 5db83dc into xgo-dev:main Oct 14, 2026
110 checks passed
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.

wasm: browser filesystem callbacks can restore another Fiber stack under GC stress

2 participants