From b8c8a636ce82bb229a6118c35b585eb509a24d70 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 3 Aug 2026 00:26:38 +0800 Subject: [PATCH 1/3] runtime/wasm: separate worker continuation backends --- runtime/internal/runtime/proc_wasm_workers.go | 80 ++----------- .../runtime/proc_wasm_workers_asyncify.go | 113 ++++++++++++++++++ .../runtime/safepoint_wasm_workers.go | 5 +- .../runtime/scheduler_waiter_wasm_workers.go | 5 +- 4 files changed, 126 insertions(+), 77 deletions(-) create mode 100644 runtime/internal/runtime/proc_wasm_workers_asyncify.go diff --git a/runtime/internal/runtime/proc_wasm_workers.go b/runtime/internal/runtime/proc_wasm_workers.go index 5c6b826207..4b4a987086 100644 --- a/runtime/internal/runtime/proc_wasm_workers.go +++ b/runtime/internal/runtime/proc_wasm_workers.go @@ -26,15 +26,13 @@ import ( "github.com/goplus/llgo/runtime/internal/clite/sync/atomic" "github.com/goplus/llgo/runtime/internal/pollbudget" "github.com/goplus/llgo/runtime/internal/runqueue" - "github.com/goplus/llgo/runtime/internal/wasmcontext" "github.com/goplus/llgo/runtime/internal/wasmevent" "github.com/goplus/llgo/runtime/internal/wasmworkers" ) const maxWasmWorkers = 16 -type runtimeContextPlatform struct { - context wasmcontext.Context +type wasmWorkerContextState struct { gcRoot wasmGCRootContext runqNext *g runqQueued bool @@ -49,7 +47,7 @@ type wasmWorker struct { runq runqueue.Queue[*g] wake uint32 - system wasmcontext.Context + platform wasmWorkerPlatform index int safepointBudget pollbudget.Budget gc wasmWorkerGCState @@ -125,9 +123,6 @@ func initWasmScheduler(gp *g) { } } -//go:linkname wasmMainTask __llgo_wasm_main -func wasmMainTask(unsafe.Pointer) unsafe.Pointer - func RunWasmMain() { gp := getg() worker := currentWasmWorker() @@ -135,7 +130,7 @@ func RunWasmMain() { fatal("runtime: invalid WebAssembly main goroutine") return } - initWasmFiber(gp, wasmcontext.Entry(wasmMainStart), unsafe.Pointer(gp), 0) + initWasmWorkerMain(gp) initWasmWorkerSystem(worker) releaseWasmWorkerG(worker, gp) casgstatus(gp, _Grunning, _Grunnable) @@ -144,17 +139,6 @@ func RunWasmMain() { c.Exit(0) } -func wasmMainStart(arg unsafe.Pointer) { - gp := (*g)(arg) - if gp == nil || getg() != gp { - fatal("runtime: invalid WebAssembly main entry") - return - } - wasmMainTask(nil) - wasmMultiSched.mainReturned = true - finishWasmG(gp) -} - func wasmWorkerStart(arg unsafe.Pointer) unsafe.Pointer { worker := (*wasmWorker)(arg) if worker == nil || worker.index == 0 { @@ -173,13 +157,9 @@ func wasmWorkerStart(arg unsafe.Pointer) unsafe.Pointer { } func initWasmWorkerSystem(worker *wasmWorker) { - if worker.system.Ready() { - return - } - if !worker.system.InitCurrent(AllocRoot) { - panic("runtime: failed to allocate WebAssembly system context") + if initWasmWorkerBackendSystem(worker) { + initWasmWorkerGCSystem(worker) } - initWasmWorkerGCSystem(worker) } func runWasmWorker(worker *wasmWorker, stopAtMain bool) { @@ -213,10 +193,7 @@ func runWasmG(worker *wasmWorker, gp *g) { for { bindWasmWorkerG(worker, gp) setg(gp) - worker.system.Swap( - &gp.context.platform.context, - wasmGCRootPointer(&gp.context.platform.gcRoot), - ) + runWasmWorkerContext(worker, gp) setg(nil) releaseWasmWorkerG(worker, gp) if readgstatus(gp) != _Grunning { @@ -245,7 +222,7 @@ func newprocBackend(fn goroutineFunc, arg unsafe.Pointer, stackSize uintptr, cal gp := newproc1(fn, arg, callergp) worker := nextWasmWorker() gp.context.platform.owner = worker - initWasmFiber(gp, wasmcontext.Entry(wasmGStart), unsafe.Pointer(gp), stackSize) + initWasmWorkerG(gp, fn, arg, stackSize) atomic.Add(&wasmMultiSched.active, uint32(1)) enqueueWasmG(worker, gp) } @@ -255,19 +232,6 @@ func nextWasmWorker() *wasmWorker { return &wasmMultiSched.workers[int(index%uint32(wasmMultiSched.count))] } -func initWasmFiber(gp *g, entry wasmcontext.Entry, arg unsafe.Pointer, stackSize uintptr) { - platform := &gp.context.platform - if !platform.context.Init( - entry, - arg, - stackSize, - AllocRoot, - FreeRoot, - ) { - panic("runtime: failed to allocate WebAssembly goroutine stack") - } -} - func releaseWasmContext(gp *g) { if gp == nil || gp.context == nil { return @@ -277,32 +241,16 @@ func releaseWasmContext(gp *g) { if wasmGCRootEnabled { unregisterWasmGCRoot(&platform.gcRoot) } - platform.context.Close(FreeRoot) + closeWasmWorkerContext(platform) freeRuntimeContext(ctx) } -func wasmGStart(arg unsafe.Pointer) { - gp := (*g)(arg) - if gp == nil || getg() != gp { - fatal("runtime: invalid WebAssembly goroutine entry") - return - } - fn, fnarg := gp.startfn, gp.startarg - gp.startfn = nil - gp.startarg = nil - fn(fnarg) - finishWasmG(gp) -} - func finishWasmG(gp *g) { casgstatus(gp, _Grunning, _Gdead) atomic.Add(&wasmMultiSched.active, ^uint32(0)) wakeWasmEventWorker() worker := gp.context.platform.owner - gp.context.platform.context.Swap( - &worker.system, - wasmWorkerSystemRootPointer(worker), - ) + suspendWasmWorkerG(worker, gp) fatal("runtime: resumed dead WebAssembly goroutine") } @@ -311,10 +259,7 @@ func goschedBackend() { worker := currentWasmWorker() casgstatus(gp, _Grunning, _Grunnable) enqueueWasmG(worker, gp) - gp.context.platform.context.Swap( - &worker.system, - wasmWorkerSystemRootPointer(worker), - ) + suspendWasmWorkerG(worker, gp) } func gopark() { @@ -327,10 +272,7 @@ func parkWasmG(gp *g) { atomic.Add(&wasmMultiSched.active, ^uint32(0)) wakeWasmEventWorker() worker := gp.context.platform.owner - gp.context.platform.context.Swap( - &worker.system, - wasmWorkerSystemRootPointer(worker), - ) + suspendWasmWorkerG(worker, gp) } func goready(gp *g) { diff --git a/runtime/internal/runtime/proc_wasm_workers_asyncify.go b/runtime/internal/runtime/proc_wasm_workers_asyncify.go new file mode 100644 index 0000000000..2a2395350e --- /dev/null +++ b/runtime/internal/runtime/proc_wasm_workers_asyncify.go @@ -0,0 +1,113 @@ +//go:build llgo && js && wasm && llgo.wasm_workers && !llgo.wasm_resume + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import ( + "unsafe" + + "github.com/goplus/llgo/runtime/internal/wasmcontext" +) + +type runtimeContextPlatform struct { + wasmWorkerContextState + context wasmcontext.Context +} + +type wasmWorkerPlatform struct { + system wasmcontext.Context +} + +//go:linkname wasmMainTask __llgo_wasm_main +func wasmMainTask(unsafe.Pointer) unsafe.Pointer + +func initWasmWorkerMain(gp *g) { + initWasmFiber(gp, wasmcontext.Entry(wasmMainStart), unsafe.Pointer(gp), 0) +} + +func initWasmWorkerBackendSystem(worker *wasmWorker) bool { + system := &worker.platform.system + if system.Ready() { + return false + } + if !system.InitCurrent(AllocRoot) { + panic("runtime: failed to allocate WebAssembly system context") + } + return true +} + +func runWasmWorkerContext(worker *wasmWorker, gp *g) { + worker.platform.system.Swap( + &gp.context.platform.context, + wasmGCRootPointer(&gp.context.platform.gcRoot), + ) +} + +func initWasmWorkerG( + gp *g, _ goroutineFunc, _ unsafe.Pointer, stackSize uintptr, +) { + initWasmFiber(gp, wasmcontext.Entry(wasmGStart), unsafe.Pointer(gp), stackSize) +} + +func closeWasmWorkerContext(platform *runtimeContextPlatform) { + platform.context.Close(FreeRoot) +} + +func suspendWasmWorkerG(worker *wasmWorker, gp *g) { + gp.context.platform.context.Swap( + &worker.platform.system, + wasmWorkerSystemRootPointer(worker), + ) +} + +func initWasmFiber(gp *g, entry wasmcontext.Entry, arg unsafe.Pointer, stackSize uintptr) { + platform := &gp.context.platform + if !platform.context.Init( + entry, + arg, + stackSize, + AllocRoot, + FreeRoot, + ) { + panic("runtime: failed to allocate WebAssembly goroutine stack") + } +} + +func wasmMainStart(arg unsafe.Pointer) { + gp := (*g)(arg) + if gp == nil || getg() != gp { + fatal("runtime: invalid WebAssembly main entry") + return + } + wasmMainTask(nil) + wasmMultiSched.mainReturned = true + finishWasmG(gp) +} + +func wasmGStart(arg unsafe.Pointer) { + gp := (*g)(arg) + if gp == nil || getg() != gp { + fatal("runtime: invalid WebAssembly goroutine entry") + return + } + fn, fnarg := gp.startfn, gp.startarg + gp.startfn = nil + gp.startarg = nil + fn(fnarg) + finishWasmG(gp) +} diff --git a/runtime/internal/runtime/safepoint_wasm_workers.go b/runtime/internal/runtime/safepoint_wasm_workers.go index 69d2ed49ee..2a615e1b18 100644 --- a/runtime/internal/runtime/safepoint_wasm_workers.go +++ b/runtime/internal/runtime/safepoint_wasm_workers.go @@ -22,10 +22,7 @@ func cooperativeSafepointSlow() { } if wasmGCRequestPending(worker) { if gp := getg(); gp != nil { - gp.context.platform.context.Swap( - &worker.system, - wasmWorkerSystemRootPointer(worker), - ) + suspendWasmWorkerG(worker, gp) } else { wasmWorkerStopForGC(worker) } diff --git a/runtime/internal/runtime/scheduler_waiter_wasm_workers.go b/runtime/internal/runtime/scheduler_waiter_wasm_workers.go index 93f0b068b6..91191353e5 100644 --- a/runtime/internal/runtime/scheduler_waiter_wasm_workers.go +++ b/runtime/internal/runtime/scheduler_waiter_wasm_workers.go @@ -43,10 +43,7 @@ func (w *SchedulerWaiter) Park() { atomic.Add(&wasmMultiSched.active, ^uint32(0)) wakeWasmEventWorker() worker := gp.context.platform.owner - gp.context.platform.context.Swap( - &worker.system, - wasmWorkerSystemRootPointer(worker), - ) + suspendWasmWorkerG(worker, gp) atomic.Store(&w.notified, uint32(0)) } From d4c8323cc0e347d5271e8a19643d17d2331d41df Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 3 Aug 2026 04:43:44 +0800 Subject: [PATCH 2/3] runtime/wasm: compose resumable workers --- internal/wasmresume/boundary.go | 54 +++-- internal/wasmresume/boundary_test.go | 14 ++ runtime/internal/gcroot/gcroot.go | 13 +- runtime/internal/gcroot/gcroot_test.go | 17 ++ runtime/internal/runtime/proc_wasm_resume.go | 86 ++------ .../internal/runtime/proc_wasm_resume_abi.go | 83 ++++++++ runtime/internal/runtime/proc_wasm_workers.go | 16 +- .../runtime/proc_wasm_workers_resume.go | 191 ++++++++++++++++++ .../runtime/tinygogc/mutex_wasm_workers.go | 2 +- runtime/internal/runtime/wasm_gc_stw.go | 6 +- runtime/internal/wasmresume/resume.go | 27 ++- runtime/internal/wasmresume/resume_test.go | 45 +++++ runtime/internal/wasmsync/mutex.go | 14 ++ 13 files changed, 464 insertions(+), 104 deletions(-) create mode 100644 runtime/internal/runtime/proc_wasm_resume_abi.go create mode 100644 runtime/internal/runtime/proc_wasm_workers_resume.go diff --git a/internal/wasmresume/boundary.go b/internal/wasmresume/boundary.go index 63e1dfd12b..a22525aa6b 100644 --- a/internal/wasmresume/boundary.go +++ b/internal/wasmresume/boundary.go @@ -19,18 +19,32 @@ package wasmresume import "strings" const ( - runtimeResumePrefix = "github.com/goplus/llgo/runtime/internal/wasmresume." - runtimeGCRootPrefix = "github.com/goplus/llgo/runtime/internal/gcroot." - runtimeTinyGCPrefix = "github.com/goplus/llgo/runtime/internal/runtime/tinygogc." - runtimeAllocRoot = "github.com/goplus/llgo/runtime/internal/runtime.AllocRoot" - runtimeFreeRoot = "github.com/goplus/llgo/runtime/internal/runtime.FreeRoot" - runtimeRunWasmMain = "github.com/goplus/llgo/runtime/internal/runtime.RunWasmMain" - runtimeRunWasmResumeContext = "github.com/goplus/llgo/runtime/internal/runtime.runWasmResumeContext" - runtimeFrameAlloc = "__llgo_wasm_resume_alloc" - runtimeDynamicAlloc = "__llgo_wasm_resume_alloc_dynamic" - runtimeFrameFree = "__llgo_wasm_resume_free" - runtimeCompatEnter = "__llgo_wasm_resume_compat_enter" - runtimeCompatLeave = "__llgo_wasm_resume_compat_leave" + runtimeResumePrefix = "github.com/goplus/llgo/runtime/internal/wasmresume." + runtimeGCRootPrefix = "github.com/goplus/llgo/runtime/internal/gcroot." + runtimeTinyGCPrefix = "github.com/goplus/llgo/runtime/internal/runtime/tinygogc." + runtimeWasmWorkersPrefix = "github.com/goplus/llgo/runtime/internal/wasmworkers." + runtimeAllocRoot = "github.com/goplus/llgo/runtime/internal/runtime.AllocRoot" + runtimeFreeRoot = "github.com/goplus/llgo/runtime/internal/runtime.FreeRoot" + runtimeCurrentWasmWorker = "github.com/goplus/llgo/runtime/internal/runtime.currentWasmWorker" + runtimeCurrentResumeOwners = "github.com/goplus/llgo/runtime/internal/runtime.currentWasmResumeOwners" + runtimeRunWasmG = "github.com/goplus/llgo/runtime/internal/runtime.runWasmG" + runtimeRunWasmMain = "github.com/goplus/llgo/runtime/internal/runtime.RunWasmMain" + runtimeRunWasmResumeContext = "github.com/goplus/llgo/runtime/internal/runtime.runWasmResumeContext" + runtimeRunWasmWorker = "github.com/goplus/llgo/runtime/internal/runtime.runWasmWorker" + runtimeRunWasmWorkerContext = "github.com/goplus/llgo/runtime/internal/runtime.runWasmWorkerContext" + runtimeSetCurrentWasmWorker = "github.com/goplus/llgo/runtime/internal/runtime.setCurrentWasmWorker" + runtimeWasmWorkerStart = "github.com/goplus/llgo/runtime/internal/runtime.wasmWorkerStart" + runtimeWasmGCAllocatorYield = "github.com/goplus/llgo/runtime/internal/runtime.wasmGCAllocatorYield" + runtimeWasmGCWorldOwner = "github.com/goplus/llgo/runtime/internal/runtime.wasmGCWorldOwner" + runtimeWasmPublishGCRoot = "github.com/goplus/llgo/runtime/internal/runtime.publishWasmGCRoot" + runtimeWasmWorkerStopForGC = "github.com/goplus/llgo/runtime/internal/runtime.wasmWorkerStopForGC" + runtimeWasmSyncLockNoSuspend = "github.com/goplus/llgo/runtime/internal/wasmsync.(*Mutex).LockNoSuspend" + runtimeWasmSyncUnlock = "github.com/goplus/llgo/runtime/internal/wasmsync.(*Mutex).Unlock" + runtimeFrameAlloc = "__llgo_wasm_resume_alloc" + runtimeDynamicAlloc = "__llgo_wasm_resume_alloc_dynamic" + runtimeFrameFree = "__llgo_wasm_resume_free" + runtimeCompatEnter = "__llgo_wasm_resume_compat_enter" + runtimeCompatLeave = "__llgo_wasm_resume_compat_leave" ) // IsRuntimeABIImplementation reports functions which implement the resumable @@ -43,7 +57,8 @@ func IsRuntimeABIImplementation(name string) bool { // callable without allocating a resumable frame. func IsNonSuspendingBoundary(name string) bool { if strings.HasPrefix(name, runtimeGCRootPrefix) || - strings.HasPrefix(name, runtimeTinyGCPrefix) { + strings.HasPrefix(name, runtimeTinyGCPrefix) || + strings.HasPrefix(name, runtimeWasmWorkersPrefix) { return true } switch name { @@ -67,8 +82,21 @@ func IsNonSuspendingBoundary(name string) bool { return (IsRuntimeABIImplementation(name) && name != SuspendSymbol) || name == runtimeAllocRoot || name == runtimeFreeRoot || + name == runtimeCurrentWasmWorker || + name == runtimeCurrentResumeOwners || + name == runtimeRunWasmG || name == runtimeRunWasmMain || name == runtimeRunWasmResumeContext || + name == runtimeRunWasmWorker || + name == runtimeRunWasmWorkerContext || + name == runtimeSetCurrentWasmWorker || + name == runtimeWasmWorkerStart || + name == runtimeWasmGCAllocatorYield || + name == runtimeWasmGCWorldOwner || + name == runtimeWasmPublishGCRoot || + name == runtimeWasmWorkerStopForGC || + name == runtimeWasmSyncLockNoSuspend || + name == runtimeWasmSyncUnlock || name == runtimeFrameAlloc || name == runtimeDynamicAlloc || name == runtimeFrameFree || diff --git a/internal/wasmresume/boundary_test.go b/internal/wasmresume/boundary_test.go index e12a02455e..66fea4fbec 100644 --- a/internal/wasmresume/boundary_test.go +++ b/internal/wasmresume/boundary_test.go @@ -20,13 +20,26 @@ func TestRuntimeBoundaries(t *testing.T) { for _, name := range []string{ runtimeAllocRoot, runtimeFreeRoot, + runtimeCurrentWasmWorker, + runtimeCurrentResumeOwners, + runtimeRunWasmG, runtimeRunWasmMain, + runtimeRunWasmWorker, + runtimeRunWasmWorkerContext, runtimeFrameAlloc, runtimeDynamicAlloc, runtimeFrameFree, runtimeCompatEnter, runtimeCompatLeave, runtimeRunWasmResumeContext, + runtimeSetCurrentWasmWorker, + runtimeWasmWorkerStart, + runtimeWasmGCAllocatorYield, + runtimeWasmGCWorldOwner, + runtimeWasmPublishGCRoot, + runtimeWasmWorkerStopForGC, + runtimeWasmSyncLockNoSuspend, + runtimeWasmSyncUnlock, "github.com/goplus/llgo/runtime/internal/runtime.AllocU", "github.com/goplus/llgo/runtime/internal/runtime.AllocZ", "github.com/goplus/llgo/runtime/internal/runtime.GetThreadDefer", @@ -44,6 +57,7 @@ func TestRuntimeBoundaries(t *testing.T) { runtimeGCRootPrefix + "RestoreChain", runtimeTinyGCPrefix + "Alloc", runtimeTinyGCPrefix + "GC", + runtimeWasmWorkersPrefix + "Wait", } { if !IsNonSuspendingBoundary(name) { t.Fatalf("%q is not a non-suspending boundary", name) diff --git a/runtime/internal/gcroot/gcroot.go b/runtime/internal/gcroot/gcroot.go index 9946365fcc..483614a4de 100644 --- a/runtime/internal/gcroot/gcroot.go +++ b/runtime/internal/gcroot/gcroot.go @@ -35,9 +35,7 @@ type stackEntry struct { m *frameMap } -var ( - contexts *Context -) +var contexts *Context // CurrentChain returns the active execution owner's compiler root chain. func CurrentChain() unsafe.Pointer { @@ -99,6 +97,15 @@ func SwitchAtBoundary(next *Context) { } } +// ClearSuspendedChain drops the saved chain of an inactive context when its +// target-specific backend owns that context's root lifetime separately. +func ClearSuspendedChain(ctx *Context) { + if ctx == nil || uintptr(unsafe.Pointer(ctx)) == activeContext { + panic("gcroot: invalid suspended context") + } + ctx.chain = nil +} + // AdoptCurrent marks next active after a target-specific stack switch has // already restored currentRootChain. func AdoptCurrent(next *Context) { diff --git a/runtime/internal/gcroot/gcroot_test.go b/runtime/internal/gcroot/gcroot_test.go index 0805cf6659..16d9fe930c 100644 --- a/runtime/internal/gcroot/gcroot_test.go +++ b/runtime/internal/gcroot/gcroot_test.go @@ -131,6 +131,23 @@ func TestPublishAndSwitchToSystem(t *testing.T) { } } +func TestClearSuspendedChain(t *testing.T) { + resetForTest() + t.Cleanup(resetForTest) + + var active, suspended Context + RegisterActive(&active) + Register(&suspended) + chain := unsafe.Pointer(uintptr(0x55)) + suspended.chain = chain + ClearSuspendedChain(&suspended) + if suspended.chain != nil { + t.Fatalf("suspended chain = %p, want nil", suspended.chain) + } + assertPanics(t, func() { ClearSuspendedChain(nil) }) + assertPanics(t, func() { ClearSuspendedChain(&active) }) +} + func assertPanics(t *testing.T, fn func()) { t.Helper() defer func() { diff --git a/runtime/internal/runtime/proc_wasm_resume.go b/runtime/internal/runtime/proc_wasm_resume.go index cf490e2533..f3330ba2ca 100644 --- a/runtime/internal/runtime/proc_wasm_resume.go +++ b/runtime/internal/runtime/proc_wasm_resume.go @@ -1,4 +1,4 @@ -//go:build llgo && wasm && llgo.wasm_resume && (js || wasip1) && !(wasip1 && llgo.wasi_threads) +//go:build llgo && wasm && llgo.wasm_resume && (js || wasip1) && !(wasip1 && llgo.wasi_threads) && !llgo.wasm_workers /* * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. @@ -38,7 +38,7 @@ type runtimeContextPlatform struct { var wasmSched struct { m m p p - compat wasmresume.Context + resume wasmResumeOwners gcRoot wasmGCRootContext runq runqueue.Queue[*g] started bool @@ -47,13 +47,6 @@ var wasmSched struct { mainExited bool } -var ( - // The frame owner stays on runtime-owned storage while the compatibility - // owner follows nested stack-local wrappers. - wasmResumeFrameOwner *wasmresume.Context - wasmResumeCompatOwner *wasmresume.Context -) - func initRuntimeContext(ctx *runtimeContext, callergp *g, status uint32) *g { gp := initG(ctx, callergp, status) if wasmGCRootEnabled { @@ -146,7 +139,7 @@ func RunWasmMain() { if gp.isMain { releaseWasmContext(gp) stopWasmResumeHost() - wasmSched.compat.Close(FreeRoot) + wasmSched.resume.compat.Close(FreeRoot) wasmSched.running = false return } @@ -184,13 +177,14 @@ func runWasmResumeContext(gp *g) wasmresume.Action { previousRoot := platform.unwindRoot platform.unwind = unwind platform.unwindRoot = captureWasmResumeGCRoot() - previousFrameOwner := wasmResumeFrameOwner - previousCompatOwner := wasmResumeCompatOwner - wasmResumeFrameOwner = &platform.context - wasmResumeCompatOwner = &platform.context + owners := &wasmSched.resume + previousFrameOwner := owners.frameOwner + previousCompatOwner := owners.compatOwner + owners.frameOwner = &platform.context + owners.compatOwner = &platform.context if c.Sigsetjmp(unwind, 0) != 0 { - wasmResumeFrameOwner = previousFrameOwner - wasmResumeCompatOwner = previousCompatOwner + owners.frameOwner = previousFrameOwner + owners.compatOwner = previousCompatOwner if !platform.context.Unwind(unsafe.Pointer(gp.defer_)) { platform.unwind = previous platform.unwindRoot = previousRoot @@ -212,8 +206,11 @@ func runWasmResumeContext(gp *g) wasmresume.Action { } } action := platform.context.Run() - wasmResumeFrameOwner = previousFrameOwner - wasmResumeCompatOwner = previousCompatOwner + if wasmGCRootEnabled { + restoreWasmResumeGCRoot(platform.context.RootChain()) + } + owners.frameOwner = previousFrameOwner + owners.compatOwner = previousCompatOwner platform.unwind = previous platform.unwindRoot = previousRoot if wasmGCRootEnabled { @@ -222,6 +219,10 @@ func runWasmResumeContext(gp *g) wasmresume.Action { return action } +func currentWasmResumeOwners() *wasmResumeOwners { + return &wasmSched.resume +} + func releaseWasmOwnership(gp *g) { if gp != nil { gp.m = nil @@ -287,55 +288,6 @@ func goexitBackend(gp *g) { fatal("runtime: resumed dead WebAssembly goroutine") } -//go:linkname wasmResumeAlloc __llgo_wasm_resume_alloc -func wasmResumeAlloc(ctx *wasmresume.Context, size, align uintptr) unsafe.Pointer { - if ctx == wasmResumeCompatOwner && wasmResumeFrameOwner != nil { - ctx = wasmResumeFrameOwner - } - return ctx.AllocateFrame(size, align, AllocRoot) -} - -//go:linkname wasmResumeAllocDynamic __llgo_wasm_resume_alloc_dynamic -func wasmResumeAllocDynamic(ctx *wasmresume.Context, size, align uintptr) unsafe.Pointer { - if ctx == wasmResumeCompatOwner && wasmResumeFrameOwner != nil { - ctx = wasmResumeFrameOwner - } - return ctx.AllocateFrame(size, align, AllocRoot) -} - -//go:linkname wasmResumeFree __llgo_wasm_resume_free -func wasmResumeFree(ctx *wasmresume.Context, frame *wasmresume.Frame) { - if ctx == wasmResumeCompatOwner && wasmResumeFrameOwner != nil { - ctx = wasmResumeFrameOwner - } - ctx.ReleaseFrame(frame) -} - -//go:linkname wasmResumeCompatEnter __llgo_wasm_resume_compat_enter -func wasmResumeCompatEnter(ctx *wasmresume.Context) unsafe.Pointer { - if ctx == nil { - fatal("runtime: invalid WebAssembly compatibility arena entry") - return nil - } - owner := wasmResumeCompatOwner - if owner == nil { - owner = &wasmSched.compat - wasmResumeFrameOwner = owner - } - wasmResumeCompatOwner = ctx - return unsafe.Pointer(owner) -} - -//go:linkname wasmResumeCompatLeave __llgo_wasm_resume_compat_leave -func wasmResumeCompatLeave(ctx *wasmresume.Context, rawOwner unsafe.Pointer) { - owner := (*wasmresume.Context)(rawOwner) - if owner == nil || wasmResumeCompatOwner != ctx { - fatal("runtime: invalid WebAssembly compatibility arena exit") - return - } - wasmResumeCompatOwner = owner -} - // CurrentGForTesting returns an opaque handle suitable for ReadyForTesting. func CurrentGForTesting() unsafe.Pointer { return unsafe.Pointer(getg()) diff --git a/runtime/internal/runtime/proc_wasm_resume_abi.go b/runtime/internal/runtime/proc_wasm_resume_abi.go new file mode 100644 index 0000000000..4420eb04df --- /dev/null +++ b/runtime/internal/runtime/proc_wasm_resume_abi.go @@ -0,0 +1,83 @@ +//go:build llgo && wasm && llgo.wasm_resume && (js || wasip1) && !(wasip1 && llgo.wasi_threads) + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import ( + "unsafe" + + "github.com/goplus/llgo/runtime/internal/wasmresume" +) + +// wasmResumeOwners keeps compiler-created frames on runtime-owned storage +// while nested compatibility wrappers temporarily change the visible owner. +type wasmResumeOwners struct { + compat wasmresume.Context + frameOwner *wasmresume.Context + compatOwner *wasmresume.Context +} + +//go:linkname wasmResumeAlloc __llgo_wasm_resume_alloc +func wasmResumeAlloc(ctx *wasmresume.Context, size, align uintptr) unsafe.Pointer { + state := currentWasmResumeOwners() + if state != nil && ctx == state.compatOwner && state.frameOwner != nil { + ctx = state.frameOwner + } + return ctx.AllocateFrame(size, align, AllocRoot) +} + +//go:linkname wasmResumeAllocDynamic __llgo_wasm_resume_alloc_dynamic +func wasmResumeAllocDynamic(ctx *wasmresume.Context, size, align uintptr) unsafe.Pointer { + return wasmResumeAlloc(ctx, size, align) +} + +//go:linkname wasmResumeFree __llgo_wasm_resume_free +func wasmResumeFree(ctx *wasmresume.Context, frame *wasmresume.Frame) { + state := currentWasmResumeOwners() + if state != nil && ctx == state.compatOwner && state.frameOwner != nil { + ctx = state.frameOwner + } + ctx.ReleaseFrame(frame) +} + +//go:linkname wasmResumeCompatEnter __llgo_wasm_resume_compat_enter +func wasmResumeCompatEnter(ctx *wasmresume.Context) unsafe.Pointer { + state := currentWasmResumeOwners() + if ctx == nil || state == nil { + fatal("runtime: invalid WebAssembly compatibility arena entry") + return nil + } + owner := state.compatOwner + if owner == nil { + owner = &state.compat + state.frameOwner = owner + } + state.compatOwner = ctx + return unsafe.Pointer(owner) +} + +//go:linkname wasmResumeCompatLeave __llgo_wasm_resume_compat_leave +func wasmResumeCompatLeave(ctx *wasmresume.Context, rawOwner unsafe.Pointer) { + state := currentWasmResumeOwners() + owner := (*wasmresume.Context)(rawOwner) + if state == nil || owner == nil || state.compatOwner != ctx { + fatal("runtime: invalid WebAssembly compatibility arena exit") + return + } + state.compatOwner = owner +} diff --git a/runtime/internal/runtime/proc_wasm_workers.go b/runtime/internal/runtime/proc_wasm_workers.go index 4b4a987086..7c9a56d982 100644 --- a/runtime/internal/runtime/proc_wasm_workers.go +++ b/runtime/internal/runtime/proc_wasm_workers.go @@ -22,11 +22,11 @@ import ( "unsafe" c "github.com/goplus/llgo/runtime/internal/clite" - "github.com/goplus/llgo/runtime/internal/clite/pthread/sync" "github.com/goplus/llgo/runtime/internal/clite/sync/atomic" "github.com/goplus/llgo/runtime/internal/pollbudget" "github.com/goplus/llgo/runtime/internal/runqueue" "github.com/goplus/llgo/runtime/internal/wasmevent" + "github.com/goplus/llgo/runtime/internal/wasmsync" "github.com/goplus/llgo/runtime/internal/wasmworkers" ) @@ -43,7 +43,7 @@ type wasmWorker struct { m m p p - lock sync.Mutex + lock wasmsync.Mutex runq runqueue.Queue[*g] wake uint32 @@ -96,10 +96,6 @@ func initWasmScheduler(gp *g) { worker := &wasmMultiSched.workers[i] worker.index = i worker.safepointBudget = pollbudget.New(wasmSafepointQuantum) - if worker.lock.Init(nil) != 0 { - fatal("runtime: failed to initialize WebAssembly worker queue") - return - } worker.m.id = nextMid(&worker.m) worker.m.p = &worker.p worker.p.id = nextPid(&worker.p) @@ -147,9 +143,9 @@ func wasmWorkerStart(arg unsafe.Pointer) unsafe.Pointer { } // Goroutines interleave on this native worker, so their entry calls need // one long-lived locality owner instead of relying on strict nesting. + setCurrentWasmWorker(worker) var localContext LocalContext EnterLocalContext(&localContext) - setCurrentWasmWorker(worker) setg(nil) initWasmWorkerSystem(worker) runWasmWorker(worker, false) @@ -297,7 +293,7 @@ func enqueueWasmG(worker *wasmWorker, gp *g) { fatal("runtime: enqueue on nil WebAssembly worker") return } - worker.lock.Lock() + worker.lock.LockNoSuspend(nil) ok := worker.runq.Push(gp) worker.lock.Unlock() if !ok { @@ -308,14 +304,14 @@ func enqueueWasmG(worker *wasmWorker, gp *g) { } func popWasmWorkerRunq(worker *wasmWorker) *g { - worker.lock.Lock() + worker.lock.LockNoSuspend(nil) gp := worker.runq.Pop() worker.lock.Unlock() return gp } func wasmWorkerRunqLen(worker *wasmWorker) uintptr { - worker.lock.Lock() + worker.lock.LockNoSuspend(nil) size := worker.runq.Len() worker.lock.Unlock() return size diff --git a/runtime/internal/runtime/proc_wasm_workers_resume.go b/runtime/internal/runtime/proc_wasm_workers_resume.go new file mode 100644 index 0000000000..d0d3506340 --- /dev/null +++ b/runtime/internal/runtime/proc_wasm_workers_resume.go @@ -0,0 +1,191 @@ +//go:build llgo && js && wasm && llgo.wasm_workers && llgo.wasm_resume + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import ( + "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" + "github.com/goplus/llgo/runtime/internal/clite/sync/atomic" + "github.com/goplus/llgo/runtime/internal/gcroot" + "github.com/goplus/llgo/runtime/internal/wasmresume" +) + +type runtimeContextPlatform struct { + wasmWorkerContextState + context wasmresume.Context + unwind unsafe.Pointer + unwindRoot unsafe.Pointer + retired bool +} + +type wasmWorkerPlatform struct { + resume wasmResumeOwners + ready bool +} + +//go:linkname wasmMainStart C.__llgo_wasm_start.__llgo_wasm_main +func wasmMainStart(*wasmresume.Context, unsafe.Pointer) *wasmresume.Frame + +func initWasmWorkerMain(gp *g) { + context := &gp.context.platform.context + context.Start(wasmMainStart(context, nil)) +} + +func initWasmWorkerBackendSystem(worker *wasmWorker) bool { + if worker.platform.ready { + return false + } + worker.platform.ready = true + return true +} + +func runWasmWorkerContext(worker *wasmWorker, gp *g) { + action := runWasmResumeContext(worker, gp) + status := readgstatus(gp) + switch action { + case wasmresume.Return: + if status != _Grunning { + fatal("runtime: invalid completed WebAssembly goroutine") + return + } + retireWasmResumeG(gp) + case wasmresume.Suspend: + if status == _Gdead { + retireWasmResumeG(gp) + } + default: + fatal("runtime: invalid WebAssembly resume action") + } +} + +func runWasmResumeContext(worker *wasmWorker, gp *g) wasmresume.Action { + platform := &gp.context.platform + if wasmGCRootEnabled { + switchWasmGCRoot(&platform.gcRoot) + // Scheduler state is anchored by wasmMultiSched. Keep no native frame + // after leaving system code; an STW in active system code publishes its + // transient compatibility roots before acknowledging the request. + gcroot.ClearSuspendedChain(&worker.gc.systemRoot) + } + unwind := c.AllocaSigjmpBuf() + previous := platform.unwind + previousRoot := platform.unwindRoot + platform.unwind = unwind + platform.unwindRoot = captureWasmResumeGCRoot() + + owners := &worker.platform.resume + previousFrameOwner := owners.frameOwner + previousCompatOwner := owners.compatOwner + owners.frameOwner = &platform.context + owners.compatOwner = &platform.context + if c.Sigsetjmp(unwind, 0) != 0 { + owners.frameOwner = previousFrameOwner + owners.compatOwner = previousCompatOwner + if !platform.context.Unwind(unsafe.Pointer(gp.defer_)) { + platform.unwind = previous + platform.unwindRoot = previousRoot + if gp.goexit { + casgstatus(gp, _Grunning, _Gdead) + if wasmGCRootEnabled { + switchWasmGCRoot(&worker.gc.systemRoot) + } + return wasmresume.Suspend + } + Rethrow(nil) + if wasmGCRootEnabled { + switchWasmGCRoot(&worker.gc.systemRoot) + } + return wasmresume.Return + } + owners.frameOwner = &platform.context + owners.compatOwner = &platform.context + } + action := platform.context.Run() + if wasmGCRootEnabled { + restoreWasmResumeGCRoot(platform.context.RootChain()) + } + owners.frameOwner = previousFrameOwner + owners.compatOwner = previousCompatOwner + platform.unwind = previous + platform.unwindRoot = previousRoot + if wasmGCRootEnabled { + switchWasmGCRoot(&worker.gc.systemRoot) + } + return action +} + +func initWasmWorkerG( + gp *g, fn goroutineFunc, arg unsafe.Pointer, _ uintptr, +) { + platform := &gp.context.platform + gp.startfn = nil + gp.startarg = nil + platform.context.Start(fn(&platform.context, arg)) +} + +func closeWasmWorkerContext(platform *runtimeContextPlatform) { + platform.context.Close(FreeRoot) +} + +func suspendWasmWorkerG(worker *wasmWorker, gp *g) { + if worker == nil || gp == nil || gp.context.platform.owner != worker || + currentWasmWorker() != worker { + fatal("runtime: invalid resumable WebAssembly worker suspension") + return + } + if readgstatus(gp) == _Gdead { + gp.context.platform.retired = true + } + wasmresume.SuspendCurrent() +} + +func retireWasmResumeG(gp *g) { + platform := &gp.context.platform + if platform.retired { + return + } + if status := readgstatus(gp); status == _Grunning { + casgstatus(gp, _Grunning, _Gdead) + } else if status != _Gdead { + fatal("runtime: retired WebAssembly goroutine is not dead") + return + } + platform.retired = true + atomic.Add(&wasmMultiSched.active, ^uint32(0)) + if gp.isMain { + if gp.goexit { + wasmMultiSched.mainGoexit = true + } else { + wasmMultiSched.mainReturned = true + } + } + wakeWasmEventWorker() +} + +func currentWasmResumeOwners() *wasmResumeOwners { + worker := currentWasmWorker() + if worker == nil { + if wasmMultiSched.started { + return nil + } + worker = &wasmMultiSched.workers[0] + } + return &worker.platform.resume +} diff --git a/runtime/internal/runtime/tinygogc/mutex_wasm_workers.go b/runtime/internal/runtime/tinygogc/mutex_wasm_workers.go index b149e8aecb..ec27f470c4 100644 --- a/runtime/internal/runtime/tinygogc/mutex_wasm_workers.go +++ b/runtime/internal/runtime/tinygogc/mutex_wasm_workers.go @@ -11,7 +11,7 @@ import ( type mutex = wasmsync.Mutex func lock(m *mutex) { - m.Lock(gcAllocatorYield) + m.LockNoSuspend(gcAllocatorYield) } func unlock(m *mutex) { diff --git a/runtime/internal/runtime/wasm_gc_stw.go b/runtime/internal/runtime/wasm_gc_stw.go index d04302e134..ba987e1e06 100644 --- a/runtime/internal/runtime/wasm_gc_stw.go +++ b/runtime/internal/runtime/wasm_gc_stw.go @@ -106,11 +106,7 @@ func wasmGCAllocatorYield() { if worker == nil { return } - if getg() == nil { - wasmWorkerStopForGC(worker) - return - } - CooperativeSafepoint() + wasmWorkerStopForGC(worker) } func wasmGCWorldOwner(worker *wasmWorker) uint32 { diff --git a/runtime/internal/wasmresume/resume.go b/runtime/internal/wasmresume/resume.go index fe68d84a2d..551cd2b7f0 100644 --- a/runtime/internal/wasmresume/resume.go +++ b/runtime/internal/wasmresume/resume.go @@ -18,7 +18,11 @@ // WebAssembly resumable call ABI. package wasmresume -import "unsafe" +import ( + "unsafe" + + "github.com/goplus/llgo/runtime/internal/gcroot" +) // SuspendCurrent yields the active resumable frame to its scheduler. The // compiler replaces calls to SuspendCurrent with a frame-PC transition; no @@ -77,9 +81,10 @@ type Frame struct { // Context owns the active frame chain for one logical goroutine. type Context struct { - top *Frame - returned *Frame - storage frameStorage + top *Frame + returned *Frame + storage frameStorage + rootChain unsafe.Pointer } // Start installs the root frame of a new logical goroutine. @@ -88,6 +93,7 @@ func (c *Context) Start(frame *Frame) { panic("wasmresume: invalid root frame") } c.returned = nil + c.rootChain = nil c.top = frame } @@ -143,6 +149,12 @@ func (c *Context) Close(release Releaser) { c.storage.close(release) c.top = nil c.returned = nil + c.rootChain = nil +} + +// RootChain returns the compiler root chain owned by the resumable frames. +func (c *Context) RootChain() unsafe.Pointer { + return c.rootChain } // Top returns the active frame. @@ -171,9 +183,14 @@ func (c *Context) TakeReturned() *Frame { // Run resumes the active frame chain until it completes or suspends. func (c *Context) Run() Action { + // The caller's host frame is temporary. Resume entries instead link their + // persistent root records to the logical chain retained by this context. + gcroot.RestoreChain(c.rootChain) for c.top != nil { frame := c.top - switch frame.Descriptor.Resume(c, frame) { + action := frame.Descriptor.Resume(c, frame) + c.rootChain = gcroot.CurrentChain() + switch action { case Continue: case Return: c.top = frame.Parent diff --git a/runtime/internal/wasmresume/resume_test.go b/runtime/internal/wasmresume/resume_test.go index 2463429abe..f5ef2fa423 100644 --- a/runtime/internal/wasmresume/resume_test.go +++ b/runtime/internal/wasmresume/resume_test.go @@ -3,6 +3,8 @@ package wasmresume import ( "testing" "unsafe" + + "github.com/goplus/llgo/runtime/internal/gcroot" ) type testRootFrame struct { @@ -116,6 +118,49 @@ func TestContextRunEmpty(t *testing.T) { } } +func TestContextRunRetainsLogicalRootChain(t *testing.T) { + outer := unsafe.Pointer(uintptr(0x11)) + logical := unsafe.Pointer(uintptr(0x22)) + gcroot.RestoreChain(outer) + t.Cleanup(func() { gcroot.RestoreChain(nil) }) + + var calls int + descriptor := Descriptor{Resume: func(*Context, *Frame) Action { + calls++ + switch calls { + case 1: + if got := gcroot.CurrentChain(); got != nil { + t.Fatalf("initial logical chain = %p, want nil", got) + } + gcroot.RestoreChain(logical) + return Suspend + case 2: + if got := gcroot.CurrentChain(); got != logical { + t.Fatalf("resumed logical chain = %p, want %p", got, logical) + } + gcroot.RestoreChain(nil) + return Return + default: + t.Fatalf("resume call = %d, want at most 2", calls) + return Return + } + }} + var ( + ctx Context + frame Frame + ) + ctx.Push(&frame, &descriptor) + if action := ctx.Run(); action != Suspend || ctx.RootChain() != logical { + t.Fatalf("first Run = (%d, %p), want (%d, %p)", action, ctx.RootChain(), Suspend, logical) + } + + // LLGo's caller epilogue restores its host chain after Context.Run. + gcroot.RestoreChain(outer) + if action := ctx.Run(); action != Return || ctx.RootChain() != nil { + t.Fatalf("second Run = (%d, %p), want (%d, nil)", action, ctx.RootChain(), Return) + } +} + func TestSuspendCurrentRequiresCompilerLowering(t *testing.T) { defer func() { if recover() == nil { diff --git a/runtime/internal/wasmsync/mutex.go b/runtime/internal/wasmsync/mutex.go index 41e338253c..5c92b07435 100644 --- a/runtime/internal/wasmsync/mutex.go +++ b/runtime/internal/wasmsync/mutex.go @@ -30,6 +30,20 @@ func (m *Mutex) Lock(yield func()) { } } +// LockNoSuspend acquires m without allowing a logical goroutine suspension. +// The callback must only participate in host-level stop-the-world waits. +func (m *Mutex) LockNoSuspend(yield func()) { + for { + if _, ok := atomic.CompareAndExchange(&m.state, uint32(0), uint32(1)); ok { + return + } + if yield != nil { + yield() + } + wasmworkers.Wait(&m.state, 1, mutexWaitNanoseconds) + } +} + // Unlock releases m and wakes all waiters. func (m *Mutex) Unlock() { atomic.Store(&m.state, uint32(0)) From 11fed5f4ee5b520af2a6da5f2a8759a2195cfbd2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 3 Aug 2026 04:45:21 +0800 Subject: [PATCH 3/3] ci/wasm: cover resumable worker composition --- .github/workflows/llgo.yml | 35 +++++++++++++++++- internal/build/testdata/wasm-workers/main.go | 11 ++++++ internal/build/wasm_resume_test.go | 39 ++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 553339813a..998be388fd 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -557,6 +557,36 @@ jobs: LLGO_WASM_WORKERS=2 llgo build -target wasm \ -o "$RUNNER_TEMP/wasm-hardening-workers.mjs" ./internal/build/testdata/wasm-hardening run_wasm_hardening "$RUNNER_TEMP/wasm-hardening-workers.mjs" "hardening-workers-j32" + LLGO_WASM_RESUME=1 LLGO_WASM_WORKERS=2 GOOS=js GOARCH=wasm llgo build -p=1 \ + -o "$RUNNER_TEMP/wasm-resume-workers-go.mjs" ./internal/build/testdata/wasm-workers + run_wasm_workers "$RUNNER_TEMP/wasm-resume-workers-go.mjs" "wasm workers ok" + LLGO_WASM_RESUME=1 LLGO_WASM_WORKERS=2 llgo build -p=1 -target wasm \ + -o "$RUNNER_TEMP/wasm-resume-workers.mjs" ./internal/build/testdata/wasm-workers + run_wasm_workers "$RUNNER_TEMP/wasm-resume-workers.mjs" "wasm workers ok" + LLGO_WASM_RESUME=1 LLGO_WASM_WORKERS=2 GOOS=js GOARCH=wasm llgo build -p=1 \ + -o "$RUNNER_TEMP/wasm-resume-gc-workers-go.mjs" ./internal/build/testdata/wasm-gc + run_wasm_workers "$RUNNER_TEMP/wasm-resume-gc-workers-go.mjs" "wasm gc ok" + LLGO_WASM_RESUME=1 LLGO_WASM_WORKERS=2 llgo build -p=1 -target wasm \ + -o "$RUNNER_TEMP/wasm-resume-gc-workers.mjs" ./internal/build/testdata/wasm-gc + run_wasm_workers "$RUNNER_TEMP/wasm-resume-gc-workers.mjs" "wasm gc ok" + LLGO_WASM_RESUME=1 LLGO_WASM_WORKERS=2 GOOS=js GOARCH=wasm llgo build -p=1 \ + -o "$RUNNER_TEMP/wasm-resume-hardening-workers-go.mjs" ./internal/build/testdata/wasm-hardening + run_wasm_hardening "$RUNNER_TEMP/wasm-resume-hardening-workers-go.mjs" "resume-hardening-workers-j64" + LLGO_WASM_RESUME=1 LLGO_WASM_WORKERS=2 llgo build -p=1 -target wasm \ + -o "$RUNNER_TEMP/wasm-resume-hardening-workers.mjs" ./internal/build/testdata/wasm-hardening + run_wasm_hardening "$RUNNER_TEMP/wasm-resume-hardening-workers.mjs" "resume-hardening-workers-j32" + for module in \ + "$RUNNER_TEMP/wasm-resume-workers-go.wasm" \ + "$RUNNER_TEMP/wasm-resume-workers.wasm" \ + "$RUNNER_TEMP/wasm-resume-gc-workers-go.wasm" \ + "$RUNNER_TEMP/wasm-resume-gc-workers.wasm" \ + "$RUNNER_TEMP/wasm-resume-hardening-workers-go.wasm" \ + "$RUNNER_TEMP/wasm-resume-hardening-workers.wasm"; do + if strings "$module" | grep -Eiq 'asyncify|emscripten_fiber|runtime/internal/wasmcontext'; then + echo "resumable worker artifact retained Asyncify/Fiber code: $module" + exit 1 + fi + done cp ./internal/build/testdata/wasm-workers/browser.html "$RUNNER_TEMP/browser.html" node ./internal/build/testdata/wasm-workers/server.mjs "$RUNNER_TEMP" 8123 & browser_server=$! @@ -572,7 +602,10 @@ jobs: test -n "$browser" for module in wasm-timers.mjs wasm-workers.mjs wasm-workers-go.mjs \ wasm-gc-workers.mjs wasm-gc-workers-go.mjs wasm-hardening-workers.mjs \ - wasm-hardening-workers-go.mjs; do + wasm-hardening-workers-go.mjs wasm-resume-workers.mjs \ + wasm-resume-workers-go.mjs wasm-resume-gc-workers.mjs \ + wasm-resume-gc-workers-go.mjs wasm-resume-hardening-workers.mjs \ + wasm-resume-hardening-workers-go.mjs; do html=$("$browser" --headless=new --no-sandbox --disable-gpu \ --disable-dev-shm-usage --virtual-time-budget=15000 --dump-dom \ "http://127.0.0.1:8123/browser.html?module=$module") diff --git a/internal/build/testdata/wasm-workers/main.go b/internal/build/testdata/wasm-workers/main.go index 0463fc3cb6..cb66f06672 100644 --- a/internal/build/testdata/wasm-workers/main.go +++ b/internal/build/testdata/wasm-workers/main.go @@ -28,6 +28,7 @@ func main() { testCrossWorkerSynchronization() testInterleavedWorkerLocality() testCrossWorkerTimerWake() + testGoroutineGoexit() println("wasm workers ok") } @@ -242,3 +243,13 @@ func testCrossWorkerTimerWake() { panic("timer did not wake a worker") } } + +func testGoroutineGoexit() { + done := make(chan struct{}) + go func() { + defer close(done) + runtime.Goexit() + panic("Goexit returned") + }() + <-done +} diff --git a/internal/build/wasm_resume_test.go b/internal/build/wasm_resume_test.go index 478575da1f..fc1cbbde2a 100644 --- a/internal/build/wasm_resume_test.go +++ b/internal/build/wasm_resume_test.go @@ -48,6 +48,45 @@ func TestConfigureWasmResume(t *testing.T) { } } +func TestConfigureWasmResumeWorkers(t *testing.T) { + t.Setenv(llgoWasmResume, "1") + t.Setenv(llgoWasmWorkers, "2") + t.Setenv(llgoWasiThreads, "") + conf := &Config{Goos: "js", Goarch: "wasm"} + export := crosscompile.Export{ + LDFLAGS: []string{"-sASYNCIFY=1"}, + WasmPostLink: crosscompile.WasmPostLink{ + Asyncify: true, + }, + } + if err := configureWasmResume(conf, &export); err != nil { + t.Fatal(err) + } + workers, err := configureWasmWorkers(conf, &export) + if err != nil || workers.Count != 2 { + t.Fatalf("worker configuration = %+v, %v", workers, err) + } + if enabled, err := configureWasmGC(conf, &export); err != nil || !enabled { + t.Fatalf("GC configuration = %v, %v", enabled, err) + } + for _, tag := range []string{wasmResumeBuildTag, "llgo.wasm_workers"} { + if !slices.Contains(export.BuildTags, tag) { + t.Fatalf("build tags do not contain %q: %v", tag, export.BuildTags) + } + } + if !hasBuildTag(conf.Tags, "llgo_wasm_gc") { + t.Fatalf("GC build tag is missing from %q", conf.Tags) + } + for _, flag := range []string{"-pthread", "-sPTHREAD_POOL_SIZE=2", "-sMALLOC=none"} { + if !slices.Contains(export.LDFLAGS, flag) { + t.Fatalf("linker flags do not contain %q: %v", flag, export.LDFLAGS) + } + } + if export.WasmPostLink.Asyncify || slices.Contains(export.LDFLAGS, "-sASYNCIFY=1") { + t.Fatalf("resumable worker build retained Asyncify: %+v", export) + } +} + func TestConfigureWasmResumeRejectsUnsupportedModes(t *testing.T) { t.Setenv(llgoWasmResume, "1") for _, test := range []struct {