From 9a881b64426871b975d2a679f1e8a827a0dd6e7d Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 04:42:02 +0800 Subject: [PATCH 1/3] compiler/wasm: make GC root ownership thread-local --- cl/gcroot_test.go | 23 +++++++++ runtime/internal/gcroot/current_stub.go | 7 +-- runtime/internal/gcroot/current_wasm.go | 8 ++-- .../internal/gcroot/current_wasm_workers.go | 12 +++++ runtime/internal/gcroot/gcroot.go | 48 ++++++++++++++----- runtime/internal/gcroot/gcroot_test.go | 34 +++++++++---- .../internal/gcroot/registry_lock_default.go | 6 +++ .../gcroot/registry_lock_wasm_workers.go | 19 ++++++++ ssa/gcroot.go | 35 +++++++++++--- ssa/gcroot_test.go | 30 ++++++++++++ ssa/package.go | 1 + 11 files changed, 189 insertions(+), 34 deletions(-) create mode 100644 runtime/internal/gcroot/current_wasm_workers.go create mode 100644 runtime/internal/gcroot/registry_lock_default.go create mode 100644 runtime/internal/gcroot/registry_lock_wasm_workers.go diff --git a/cl/gcroot_test.go b/cl/gcroot_test.go index 9d222d862b..36db62c000 100644 --- a/cl/gcroot_test.go +++ b/cl/gcroot_test.go @@ -84,6 +84,29 @@ func keep(p *int) *int { return p } } } +func TestCompileThreadLocalGCRoots(t *testing.T) { + const src = `package main + +func use(*int) + +func keep(p *int) *int { + use(p) + return p +} +` + ir := cltest.CompileIREx(t, src, "gcroot_tls.go", false, func(prog llssa.Program) { + prog.EnableGCRoots(true) + prog.EnableThreadLocalGCRoots(true) + }) + if !strings.Contains(ir, `thread_local`) || + !strings.Contains(ir, `github.com/goplus/llgo/runtime/internal/gcroot.currentRootChain`) { + t.Fatalf("thread-local compiler root chain is missing:\n%s", ir) + } + if strings.Contains(ir, `@llvm_gc_root_chain`) { + t.Fatalf("thread-local roots also emitted the single-worker chain:\n%s", ir) + } +} + func TestCompileGCRootPlanning(t *testing.T) { const pure = `package main func keep(p *int) *int { return p } diff --git a/runtime/internal/gcroot/current_stub.go b/runtime/internal/gcroot/current_stub.go index 54b0ca040c..ea2e659226 100644 --- a/runtime/internal/gcroot/current_stub.go +++ b/runtime/internal/gcroot/current_stub.go @@ -2,6 +2,7 @@ package gcroot -import "unsafe" - -var currentRootChain unsafe.Pointer +var ( + currentRootChain uintptr + activeContext uintptr +) diff --git a/runtime/internal/gcroot/current_wasm.go b/runtime/internal/gcroot/current_wasm.go index 576994966a..7db6e60c14 100644 --- a/runtime/internal/gcroot/current_wasm.go +++ b/runtime/internal/gcroot/current_wasm.go @@ -1,8 +1,10 @@ -//go:build llgo && wasm && llgo_wasm_gc +//go:build llgo && wasm && llgo_wasm_gc && !llgo.wasm_workers package gcroot -import "unsafe" +import _ "unsafe" //go:linkname currentRootChain llvm_gc_root_chain -var currentRootChain unsafe.Pointer +var currentRootChain uintptr + +var activeContext uintptr diff --git a/runtime/internal/gcroot/current_wasm_workers.go b/runtime/internal/gcroot/current_wasm_workers.go new file mode 100644 index 0000000000..e45998f9e0 --- /dev/null +++ b/runtime/internal/gcroot/current_wasm_workers.go @@ -0,0 +1,12 @@ +//go:build llgo && js && wasm && llgo_wasm_gc && llgo.wasm_workers + +package gcroot + +// The compiler uses the fully qualified currentRootChain symbol as its +// thread-local root-chain slot in multi-worker builds. +// +//llgo:tls +var ( + currentRootChain uintptr + activeContext uintptr +) diff --git a/runtime/internal/gcroot/gcroot.go b/runtime/internal/gcroot/gcroot.go index c0bc40ecc6..9946365fcc 100644 --- a/runtime/internal/gcroot/gcroot.go +++ b/runtime/internal/gcroot/gcroot.go @@ -37,35 +37,37 @@ type stackEntry struct { var ( contexts *Context - active *Context ) // CurrentChain returns the active execution owner's compiler root chain. func CurrentChain() unsafe.Pointer { - return currentRootChain + return unsafe.Pointer(currentRootChain) } // RestoreChain installs a chain captured before a non-local control transfer. func RestoreChain(chain unsafe.Pointer) { - currentRootChain = chain + currentRootChain = uintptr(chain) } // Register adds a suspended context to root enumeration. func Register(ctx *Context) { - if ctx == nil || registered(ctx) { + lockRegistry() + if ctx == nil || registeredLocked(ctx) { + unlockRegistry() panic("gcroot: invalid context registration") } ctx.next = contexts contexts = ctx + unlockRegistry() } // RegisterActive adds ctx and assigns the existing LLVM root chain to it. func RegisterActive(ctx *Context) { - if active != nil { + if activeContext != 0 { panic("gcroot: active context already registered") } Register(ctx) - active = ctx + activeContext = uintptr(unsafe.Pointer(ctx)) } // Switch saves the active chain and installs next's chain. @@ -82,37 +84,54 @@ func Switch(next *Context) { // the target-specific stack switch. Keep it free of calls and allocations so // it cannot acquire a compiler-maintained root frame of its own. func SwitchAtBoundary(next *Context) { + active := (*Context)(unsafe.Pointer(activeContext)) if active == next { return } if active != nil { - active.chain = currentRootChain + active.chain = unsafe.Pointer(currentRootChain) + } + activeContext = uintptr(unsafe.Pointer(next)) + if next == nil { + currentRootChain = 0 + } else { + currentRootChain = uintptr(next.chain) } - active = next - currentRootChain = next.chain } // AdoptCurrent marks next active after a target-specific stack switch has // already restored currentRootChain. func AdoptCurrent(next *Context) { - active = next + activeContext = uintptr(unsafe.Pointer(next)) +} + +// PublishCurrent saves the calling thread's root chain in its active context. +// A worker calls this immediately before acknowledging a stop-the-world request. +func PublishCurrent() { + active := (*Context)(unsafe.Pointer(activeContext)) + if active != nil { + active.chain = unsafe.Pointer(currentRootChain) + } } // Unregister removes a suspended context from root enumeration. func Unregister(ctx *Context) { - if ctx == nil || ctx == active { + if ctx == nil || uintptr(unsafe.Pointer(ctx)) == activeContext { panic("gcroot: invalid context unregistration") } + lockRegistry() link := &contexts for *link != nil && *link != ctx { link = &(*link).next } if *link == nil { + unlockRegistry() panic("gcroot: context is not registered") } *link = ctx.next ctx.next = nil ctx.chain = nil + unlockRegistry() } // Visit calls visitor for every root slot in every registered context. @@ -120,16 +139,19 @@ func Visit(visitor func(root *unsafe.Pointer, metadata unsafe.Pointer)) { if visitor == nil { return } + lockRegistry() + active := (*Context)(unsafe.Pointer(activeContext)) for ctx := contexts; ctx != nil; ctx = ctx.next { chain := ctx.chain if ctx == active { - chain = currentRootChain + chain = unsafe.Pointer(currentRootChain) } visitChain(chain, visitor) } + unlockRegistry() } -func registered(want *Context) bool { +func registeredLocked(want *Context) bool { for ctx := contexts; ctx != nil; ctx = ctx.next { if ctx == want { return true diff --git a/runtime/internal/gcroot/gcroot_test.go b/runtime/internal/gcroot/gcroot_test.go index 44e30672c8..0805cf6659 100644 --- a/runtime/internal/gcroot/gcroot_test.go +++ b/runtime/internal/gcroot/gcroot_test.go @@ -30,7 +30,7 @@ func TestVisitAndSwitchContexts(t *testing.T) { stackEntry: stackEntry{m: &m.frameMap}, roots: [2]unsafe.Pointer{firstValue, secondValue}, } - currentRootChain = unsafe.Pointer(&entry.stackEntry) + currentRootChain = uintptr(unsafe.Pointer(&entry.stackEntry)) var first, second Context RegisterActive(&first) @@ -49,7 +49,7 @@ func TestVisitAndSwitchContexts(t *testing.T) { } Switch(&second) - if first.chain != unsafe.Pointer(&entry.stackEntry) || currentRootChain != nil { + if first.chain != unsafe.Pointer(&entry.stackEntry) || currentRootChain != 0 { t.Fatal("Switch did not save the active chain and restore the next chain") } Unregister(&first) @@ -85,7 +85,7 @@ func TestRestoreChain(t *testing.T) { first := unsafe.Pointer(uintptr(0x11)) second := unsafe.Pointer(uintptr(0x22)) - currentRootChain = first + currentRootChain = uintptr(first) if got := CurrentChain(); got != first { t.Fatalf("CurrentChain() = %p, want %p", got, first) } @@ -102,17 +102,35 @@ func TestAdoptCurrent(t *testing.T) { var first, second Context RegisterActive(&first) Register(&second) - currentRootChain = unsafe.Pointer(uintptr(0x11)) + currentRootChain = uintptr(0x11) AdoptCurrent(&second) - if active != &second { + if activeContext != uintptr(unsafe.Pointer(&second)) { t.Fatal("AdoptCurrent did not replace the active context") } - if currentRootChain != unsafe.Pointer(uintptr(0x11)) { + if currentRootChain != uintptr(0x11) { t.Fatal("AdoptCurrent changed the chain restored by the stack switch") } } +func TestPublishAndSwitchToSystem(t *testing.T) { + resetForTest() + t.Cleanup(resetForTest) + + var ctx Context + RegisterActive(&ctx) + currentRootChain = uintptr(0x44) + PublishCurrent() + if ctx.chain != unsafe.Pointer(uintptr(0x44)) { + t.Fatalf("published chain = %p, want %p", ctx.chain, unsafe.Pointer(uintptr(0x44))) + } + + SwitchAtBoundary(nil) + if activeContext != 0 || currentRootChain != 0 { + t.Fatalf("system boundary retained active=%#x chain=%#x", activeContext, currentRootChain) + } +} + func assertPanics(t *testing.T, fn func()) { t.Helper() defer func() { @@ -125,6 +143,6 @@ func assertPanics(t *testing.T, fn func()) { func resetForTest() { contexts = nil - active = nil - currentRootChain = nil + currentRootChain = 0 + activeContext = 0 } diff --git a/runtime/internal/gcroot/registry_lock_default.go b/runtime/internal/gcroot/registry_lock_default.go new file mode 100644 index 0000000000..5a2f6d0191 --- /dev/null +++ b/runtime/internal/gcroot/registry_lock_default.go @@ -0,0 +1,6 @@ +//go:build !llgo || !js || !wasm || !llgo_wasm_gc || !llgo.wasm_workers + +package gcroot + +func lockRegistry() {} +func unlockRegistry() {} diff --git a/runtime/internal/gcroot/registry_lock_wasm_workers.go b/runtime/internal/gcroot/registry_lock_wasm_workers.go new file mode 100644 index 0000000000..d58d5246b3 --- /dev/null +++ b/runtime/internal/gcroot/registry_lock_wasm_workers.go @@ -0,0 +1,19 @@ +//go:build llgo && js && wasm && llgo_wasm_gc && llgo.wasm_workers + +package gcroot + +import "github.com/goplus/llgo/runtime/internal/clite/sync/atomic" + +var registryLock uint32 + +func lockRegistry() { + for { + if _, ok := atomic.CompareAndExchange(®istryLock, uint32(0), uint32(1)); ok { + return + } + } +} + +func unlockRegistry() { + atomic.Store(®istryLock, uint32(0)) +} diff --git a/ssa/gcroot.go b/ssa/gcroot.go index 011614a1d9..c2136a9621 100644 --- a/ssa/gcroot.go +++ b/ssa/gcroot.go @@ -19,10 +19,14 @@ package ssa import ( "go/types" + "github.com/goplus/llgo/internal/env" "github.com/xgo-dev/llvm" ) -const gcRootChainName = "llvm_gc_root_chain" +const ( + gcRootChainName = "llvm_gc_root_chain" + threadLocalRootChain = env.LLGoRuntimePkg + "/internal/gcroot.currentRootChain" +) // EnableGCRoots controls compiler-maintained GC roots. func (p Program) EnableGCRoots(enable bool) { @@ -34,6 +38,17 @@ func (p Program) GCRootsEnabled() bool { return p.enableGCRoots } +// EnableThreadLocalGCRoots gives each native thread an independent compiler +// root chain. The runtime must provide a matching native TLS variable. +func (p Program) EnableThreadLocalGCRoots(enable bool) { + p.threadLocalGCRoots = enable +} + +// ThreadLocalGCRootsEnabled reports whether root chains are thread-local. +func (p Program) ThreadLocalGCRootsEnabled() bool { + return p.threadLocalGCRoots +} + // NewGCRoots reserves count pointer roots in one compiler-maintained frame. // It must be called once, before the function emits a return. func (p Function) NewGCRoots(count int) []Expr { @@ -59,7 +74,8 @@ func (p Function) NewGCRoots(count int) []Expr { frame := llvm.CreateAlloca(b.impl, frameType) chain := p.gcRootChain() - prev := llvm.CreateLoad(b.impl, voidPtr, chain) + prevWord := llvm.CreateLoad(b.impl, prog.Uintptr().ll, chain) + prev := llvm.CreateIntToPtr(b.impl, prevWord, voidPtr) b.impl.CreateStore(prev, llvm.CreateStructGEP(b.impl, frameType, frame, 0)) frameMap := p.newGCRootMap(count) @@ -74,8 +90,8 @@ func (p Function) NewGCRoots(count int) []Expr { b.impl.CreateStore(llvm.ConstNull(voidPtr), root) roots[i] = Expr{root, prog.Pointer(prog.VoidPtr())} } - b.impl.CreateStore(frame, chain) - p.gcRootPrev = Expr{prev, prog.VoidPtr()} + b.impl.CreateStore(llvm.CreatePtrToInt(b.impl, frame, prog.Uintptr().ll), chain) + p.gcRootPrev = Expr{prevWord, prog.Uintptr()} return roots } @@ -85,12 +101,17 @@ func (b Builder) SetGCRoot(root, value Expr) { } func (p Function) gcRootChain() llvm.Value { - global := p.Pkg.mod.NamedGlobal(gcRootChainName) + name := gcRootChainName + if p.Prog.threadLocalGCRoots { + name = threadLocalRootChain + } + global := p.Pkg.mod.NamedGlobal(name) if global.IsNil() { - global = llvm.AddGlobal(p.Pkg.mod, p.Prog.tyVoidPtr(), gcRootChainName) + global = llvm.AddGlobal(p.Pkg.mod, p.Prog.Uintptr().ll, name) } - global.SetInitializer(llvm.ConstNull(p.Prog.tyVoidPtr())) + global.SetInitializer(llvm.ConstNull(p.Prog.Uintptr().ll)) global.SetLinkage(llvm.LinkOnceAnyLinkage) + global.SetThreadLocal(p.Prog.threadLocalGCRoots) global.SetAlignment(p.Prog.PointerSize()) return global } diff --git a/ssa/gcroot_test.go b/ssa/gcroot_test.go index 240cb08720..1e391577bd 100644 --- a/ssa/gcroot_test.go +++ b/ssa/gcroot_test.go @@ -82,6 +82,36 @@ func TestGCRootFrameIR(t *testing.T) { } } +func TestThreadLocalGCRootFrameIR(t *testing.T) { + prog := ssatest.NewProgram(t, &ssa.Target{GOOS: "js", GOARCH: "wasm"}) + if prog.ThreadLocalGCRootsEnabled() { + t.Fatal("thread-local GC roots enabled by default") + } + prog.EnableThreadLocalGCRoots(true) + if !prog.ThreadLocalGCRootsEnabled() { + t.Fatal("thread-local GC roots were not enabled") + } + pkg := prog.NewPackage("main", "main") + + fn := pkg.NewFunc("main.keep", ssa.NoArgsNoRet, ssa.InGo) + b := fn.MakeBody(1) + fn.NewGCRoots(1) + b.Return() + b.EndBuild() + + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + ir := pkg.String() + if !strings.Contains(ir, `thread_local`) || + !strings.Contains(ir, `github.com/goplus/llgo/runtime/internal/gcroot.currentRootChain`) { + t.Fatalf("thread-local compiler root chain is missing:\n%s", ir) + } + if strings.Contains(ir, `@llvm_gc_root_chain`) { + t.Fatalf("thread-local roots also emitted the single-worker chain:\n%s", ir) + } +} + func TestGCRootReservationAndClosureContext(t *testing.T) { prog := ssatest.NewProgram(t, &ssa.Target{GOOS: "js", GOARCH: "wasm"}) pkg := prog.NewPackage("main", "main") diff --git a/ssa/package.go b/ssa/package.go index 41d7aa15f0..00ec8af5e8 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -239,6 +239,7 @@ type aProgram struct { enableGoGlobalDCE bool enableDeadcodeDrop bool enableGCRoots bool + threadLocalGCRoots bool enableSafepoints bool disableBoundsChecks bool pthreadStackSize uint64 From c4628ffc470407a905abf1655586549d3f879df8 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 04:42:32 +0800 Subject: [PATCH 2/3] runtime/wasm: stop all workers for garbage collection --- internal/build/build.go | 11 +- internal/build/build_test.go | 27 +++- .../lib/runtime/sema_wasm_workers_llgo.go | 18 +-- .../runtime/chan_sync_wasm_workers.go | 28 ++-- runtime/internal/runtime/proc_wasm_workers.go | 61 +++++++-- .../runtime/safepoint_wasm_workers.go | 13 +- .../runtime/scheduler_waiter_wasm_workers.go | 5 +- .../internal/runtime/tinygogc/gc_tinygo.go | 11 +- .../runtime/tinygogc/gc_wasm_js_workers.go | 27 ++++ runtime/internal/runtime/tinygogc/mutex.go | 2 + .../runtime/tinygogc/mutex_wasm_workers.go | 22 +++ .../runtime/tinygogc/world_default.go | 6 + .../runtime/tinygogc/world_wasm_workers.go | 19 +++ runtime/internal/runtime/wasm_gc_stw.go | 127 ++++++++++++++++++ runtime/internal/runtime/wasm_gc_stw_stub.go | 17 +++ runtime/internal/runtime/wasm_gcroot.go | 4 + runtime/internal/runtime/wasm_gcroot_stub.go | 2 + .../wasmevent/runtime_mutex_workers.go | 22 +-- runtime/internal/wasmsync/mutex.go | 37 +++++ 19 files changed, 377 insertions(+), 82 deletions(-) create mode 100644 runtime/internal/runtime/tinygogc/gc_wasm_js_workers.go create mode 100644 runtime/internal/runtime/tinygogc/mutex_wasm_workers.go create mode 100644 runtime/internal/runtime/tinygogc/world_default.go create mode 100644 runtime/internal/runtime/tinygogc/world_wasm_workers.go create mode 100644 runtime/internal/runtime/wasm_gc_stw.go create mode 100644 runtime/internal/runtime/wasm_gc_stw_stub.go create mode 100644 runtime/internal/wasmsync/mutex.go diff --git a/internal/build/build.go b/internal/build/build.go index d412f957da..bcb17d5075 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -391,7 +391,7 @@ func Build(inv Invocation) ([]Package, error) { if err != nil { return nil, err } - wasmGC, err := configureWasmGC(conf, &export, wasmWorkers.Enabled()) + wasmGC, err := configureWasmGC(conf, &export) if err != nil { return nil, err } @@ -464,6 +464,7 @@ func Build(inv Invocation) ([]Package, error) { prog.EnableGoGlobalDCE(conf.goGlobalDCEEnabled()) prog.EnableDeadcodeDrop(conf.deadcodeDropEnabled()) prog.EnableGCRoots(wasmGC) + prog.EnableThreadLocalGCRoots(wasmGC && wasmWorkers.Enabled()) prog.EnableCooperativeSafepoints(wasmGC || wasmWorkers.Enabled()) if conf.PthreadStackSize > 0 { prog.SetPthreadStackSize(uint64(conf.PthreadStackSize)) @@ -820,7 +821,7 @@ func configureWasmWorkers(conf *Config, export *crosscompile.Export) (wasmworker return config, nil } -func configureWasmGC(conf *Config, export *crosscompile.Export, wasmWorkers bool) (bool, error) { +func configureWasmGC(conf *Config, export *crosscompile.Export) (bool, error) { explicit := hasBuildTag(conf.Tags, "llgo_wasm_gc") if conf.Goarch != "wasm" { if explicit { @@ -830,12 +831,6 @@ func configureWasmGC(conf *Config, export *crosscompile.Export, wasmWorkers bool } switch conf.Goos { case "js": - if wasmWorkers { - if explicit { - return false, errors.New("llgo_wasm_gc does not yet support multiple WebAssembly workers") - } - return false, nil - } if !slices.Contains(export.LDFLAGS, "-sMALLOC=none") { export.LDFLAGS = append(export.LDFLAGS, "-sMALLOC=none") } diff --git a/internal/build/build_test.go b/internal/build/build_test.go index cfc5c51ef5..af81a3ec48 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -345,7 +345,7 @@ func TestConfigureWasmGC(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { export := crosscompile.Export{} - enabled, err := configureWasmGC(&test.conf, &export, false) + enabled, err := configureWasmGC(&test.conf, &export) if (err != nil) != test.err { t.Fatalf("configureWasmGC error = %v, want error %v", err, test.err) } @@ -365,7 +365,7 @@ func TestConfigureWasmGC(t *testing.T) { func TestConfigureWasmGCRejectsWASIThreads(t *testing.T) { t.Setenv("LLGO_WASI_THREADS", "1") conf := Config{Goos: "wasip1", Goarch: "wasm", Tags: "llgo_wasm_gc"} - if _, err := configureWasmGC(&conf, &crosscompile.Export{}, false); err == nil { + if _, err := configureWasmGC(&conf, &crosscompile.Export{}); err == nil { t.Fatal("expected llgo_wasm_gc with WASI threads to fail") } } @@ -373,7 +373,7 @@ func TestConfigureWasmGCRejectsWASIThreads(t *testing.T) { func TestConfigureWasmGCLeavesWASIThreadsDisabled(t *testing.T) { t.Setenv("LLGO_WASI_THREADS", "1") conf := Config{Goos: "wasip1", Goarch: "wasm"} - enabled, err := configureWasmGC(&conf, &crosscompile.Export{}, false) + enabled, err := configureWasmGC(&conf, &crosscompile.Export{}) if err != nil { t.Fatal(err) } @@ -435,10 +435,23 @@ func TestConfigureWasmWorkersRejectsUnsupportedTarget(t *testing.T) { } } -func TestConfigureWasmWorkersRejectsCurrentGC(t *testing.T) { - conf := Config{Goos: "js", Goarch: "wasm", Tags: "llgo_wasm_gc"} - if _, err := configureWasmGC(&conf, &crosscompile.Export{}, true); err == nil { - t.Fatal("multi-worker wasm GC configuration succeeded before M2") +func TestConfigureWasmWorkersEnableGC(t *testing.T) { + t.Setenv(llgoWasmWorkers, "2") + conf := Config{Goos: "js", Goarch: "wasm"} + export := crosscompile.Export{} + workers, err := configureWasmWorkers(&conf, &export) + if err != nil || !workers.Enabled() { + t.Fatalf("multi-worker wasm configuration = %+v, %v", workers, err) + } + if enabled, err := configureWasmGC(&conf, &export); err != nil || !enabled { + t.Fatalf("multi-worker wasm GC configuration = %v, %v", enabled, err) + } + if !hasBuildTag(conf.Tags, "llgo_wasm_gc") || + !slices.Contains(export.BuildTags, "llgo.wasm_workers") { + t.Fatalf("multi-worker wasm GC tags = %q, %v", conf.Tags, export.BuildTags) + } + if !slices.Contains(export.LDFLAGS, "-sMALLOC=none") { + t.Fatalf("multi-worker wasm GC linker flags = %v", export.LDFLAGS) } } diff --git a/runtime/internal/lib/runtime/sema_wasm_workers_llgo.go b/runtime/internal/lib/runtime/sema_wasm_workers_llgo.go index 1bc68a69ed..6a29ed538e 100644 --- a/runtime/internal/lib/runtime/sema_wasm_workers_llgo.go +++ b/runtime/internal/lib/runtime/sema_wasm_workers_llgo.go @@ -5,28 +5,20 @@ package runtime import ( "unsafe" - psync "github.com/goplus/llgo/runtime/internal/clite/pthread/sync" latomic "github.com/goplus/llgo/runtime/internal/lib/sync/atomic" llruntime "github.com/goplus/llgo/runtime/internal/runtime" + "github.com/goplus/llgo/runtime/internal/wasmsync" ) -var semaQueuesLock = newWasmSemaMutex() -var notifyQueuesLock = newWasmSemaMutex() +var semaQueuesLock wasmSemaMutex +var notifyQueuesLock wasmSemaMutex type wasmSemaMutex struct { - mutex psync.Mutex -} - -func newWasmSemaMutex() wasmSemaMutex { - var result wasmSemaMutex - if result.mutex.Init(nil) != 0 { - panic("runtime: failed to initialize WebAssembly semaphore mutex") - } - return result + mutex wasmsync.Mutex } func (m *wasmSemaMutex) Lock() { - m.mutex.Lock() + m.mutex.Lock(llruntime.CooperativeSafepoint) } func (m *wasmSemaMutex) Unlock() { diff --git a/runtime/internal/runtime/chan_sync_wasm_workers.go b/runtime/internal/runtime/chan_sync_wasm_workers.go index 9c00ad5747..259e674472 100644 --- a/runtime/internal/runtime/chan_sync_wasm_workers.go +++ b/runtime/internal/runtime/chan_sync_wasm_workers.go @@ -3,23 +3,17 @@ package runtime import ( - "github.com/goplus/llgo/runtime/internal/clite/pthread/sync" - "github.com/goplus/llgo/runtime/internal/clite/sync/atomic" - "github.com/goplus/llgo/runtime/internal/wasmworkers" + "github.com/goplus/llgo/runtime/internal/wasmsync" ) type chanMutex struct { - mutex sync.Mutex + mutex wasmsync.Mutex } -func (m *chanMutex) init() { - if m.mutex.Init(nil) != 0 { - fatal("runtime: failed to initialize channel mutex") - } -} +func (*chanMutex) init() {} func (m *chanMutex) Lock() { - m.mutex.Lock() + m.mutex.Lock(CooperativeSafepoint) } func (m *chanMutex) Unlock() { @@ -27,8 +21,8 @@ func (m *chanMutex) Unlock() { } type chanSignal struct { - lockWord uint32 - waiter SchedulerWaiter + mutex wasmsync.Mutex + waiter SchedulerWaiter } func (s *chanSignal) init() { @@ -36,17 +30,11 @@ func (s *chanSignal) init() { } func (s *chanSignal) lock() { - for { - if _, ok := atomic.CompareAndExchange(&s.lockWord, uint32(0), uint32(1)); ok { - return - } - wasmworkers.Wait(&s.lockWord, 1, -1) - } + s.mutex.Lock(CooperativeSafepoint) } func (s *chanSignal) unlock() { - atomic.Store(&s.lockWord, uint32(0)) - wasmworkers.Wake(&s.lockWord) + s.mutex.Unlock() } func (s *chanSignal) park() { diff --git a/runtime/internal/runtime/proc_wasm_workers.go b/runtime/internal/runtime/proc_wasm_workers.go index 38f55b729d..5c6b826207 100644 --- a/runtime/internal/runtime/proc_wasm_workers.go +++ b/runtime/internal/runtime/proc_wasm_workers.go @@ -52,6 +52,7 @@ type wasmWorker struct { system wasmcontext.Context index int safepointBudget pollbudget.Budget + gc wasmWorkerGCState } var wasmMultiSched struct { @@ -68,6 +69,9 @@ var wasmMultiSched struct { func initRuntimeContext(ctx *runtimeContext, callergp *g, status uint32) *g { gp := initG(ctx, callergp, status) + if wasmGCRootEnabled { + registerWasmGCRoot(&ctx.platform.gcRoot, status == _Grunning) + } if status == _Grunning { initWasmScheduler(gp) } @@ -104,12 +108,13 @@ func initWasmScheduler(gp *g) { worker.p.m = &worker.m setpstatus(&worker.p, _Prunning) } - mainWorker := &wasmMultiSched.workers[0] setCurrentWasmWorker(mainWorker) bindWasmWorkerG(mainWorker, gp) gp.context.platform.owner = mainWorker wasmMultiSched.started = true + wasmevent.InstallWake(wakeWasmEventWorker) + wasmevent.InstallCooperativeSafepoint(CooperativeSafepoint) for i := 1; i < count; i++ { worker := &wasmMultiSched.workers[i] @@ -118,7 +123,6 @@ func initWasmScheduler(gp *g) { return } } - wasmevent.InstallWake(wakeWasmEventWorker) } //go:linkname wasmMainTask __llgo_wasm_main @@ -175,6 +179,7 @@ func initWasmWorkerSystem(worker *wasmWorker) { if !worker.system.InitCurrent(AllocRoot) { panic("runtime: failed to allocate WebAssembly system context") } + initWasmWorkerGCSystem(worker) } func runWasmWorker(worker *wasmWorker, stopAtMain bool) { @@ -184,14 +189,8 @@ func runWasmWorker(worker *wasmWorker, stopAtMain bool) { continue } casgstatus(gp, _Grunnable, _Grunning) - bindWasmWorkerG(worker, gp) - setg(gp) - worker.system.Swap( - &gp.context.platform.context, - wasmGCRootPointer(&gp.context.platform.gcRoot), - ) - setg(nil) - releaseWasmWorkerG(worker, gp) + runWasmG(worker, gp) + wasmWorkerStopForGC(worker) if readgstatus(gp) != _Gdead { continue @@ -210,6 +209,26 @@ func runWasmWorker(worker *wasmWorker, stopAtMain bool) { } } +func runWasmG(worker *wasmWorker, gp *g) { + for { + bindWasmWorkerG(worker, gp) + setg(gp) + worker.system.Swap( + &gp.context.platform.context, + wasmGCRootPointer(&gp.context.platform.gcRoot), + ) + setg(nil) + releaseWasmWorkerG(worker, gp) + if readgstatus(gp) != _Grunning { + return + } + if !wasmWorkerStopForGC(worker) { + fatal("runtime: running WebAssembly goroutine returned without a GC request") + return + } + } +} + func bindWasmWorkerG(worker *wasmWorker, gp *g) { worker.m.curg = gp gp.m = &worker.m @@ -254,7 +273,11 @@ func releaseWasmContext(gp *g) { return } ctx := gp.context - ctx.platform.context.Close(FreeRoot) + platform := &ctx.platform + if wasmGCRootEnabled { + unregisterWasmGCRoot(&platform.gcRoot) + } + platform.context.Close(FreeRoot) freeRuntimeContext(ctx) } @@ -276,7 +299,10 @@ func finishWasmG(gp *g) { atomic.Add(&wasmMultiSched.active, ^uint32(0)) wakeWasmEventWorker() worker := gp.context.platform.owner - gp.context.platform.context.Swap(&worker.system, nil) + gp.context.platform.context.Swap( + &worker.system, + wasmWorkerSystemRootPointer(worker), + ) fatal("runtime: resumed dead WebAssembly goroutine") } @@ -285,7 +311,10 @@ func goschedBackend() { worker := currentWasmWorker() casgstatus(gp, _Grunning, _Grunnable) enqueueWasmG(worker, gp) - gp.context.platform.context.Swap(&worker.system, nil) + gp.context.platform.context.Swap( + &worker.system, + wasmWorkerSystemRootPointer(worker), + ) } func gopark() { @@ -298,7 +327,10 @@ func parkWasmG(gp *g) { atomic.Add(&wasmMultiSched.active, ^uint32(0)) wakeWasmEventWorker() worker := gp.context.platform.owner - gp.context.platform.context.Swap(&worker.system, nil) + gp.context.platform.context.Swap( + &worker.system, + wasmWorkerSystemRootPointer(worker), + ) } func goready(gp *g) { @@ -360,6 +392,7 @@ func wakeWasmEventWorker() { func waitWasmWorkerRunq(worker *wasmWorker) *g { for { + wasmWorkerStopForGC(worker) if gp := popWasmWorkerRunq(worker); gp != nil { return gp } diff --git a/runtime/internal/runtime/safepoint_wasm_workers.go b/runtime/internal/runtime/safepoint_wasm_workers.go index 9c3b6db065..69d2ed49ee 100644 --- a/runtime/internal/runtime/safepoint_wasm_workers.go +++ b/runtime/internal/runtime/safepoint_wasm_workers.go @@ -8,7 +8,7 @@ const wasmSafepointQuantum = uint32(1024) func CooperativeSafepoint() { worker := currentWasmWorker() - if worker == nil || !worker.safepointBudget.Poll() { + if worker == nil || (!wasmGCRequestPending(worker) && !worker.safepointBudget.Poll()) { return } cooperativeSafepointSlow() @@ -20,6 +20,17 @@ func cooperativeSafepointSlow() { if worker == nil { return } + if wasmGCRequestPending(worker) { + if gp := getg(); gp != nil { + gp.context.platform.context.Swap( + &worker.system, + wasmWorkerSystemRootPointer(worker), + ) + } else { + wasmWorkerStopForGC(worker) + } + return + } if worker.index == 0 { wasmevent.Poll() } diff --git a/runtime/internal/runtime/scheduler_waiter_wasm_workers.go b/runtime/internal/runtime/scheduler_waiter_wasm_workers.go index f8ff38c69c..93f0b068b6 100644 --- a/runtime/internal/runtime/scheduler_waiter_wasm_workers.go +++ b/runtime/internal/runtime/scheduler_waiter_wasm_workers.go @@ -43,7 +43,10 @@ func (w *SchedulerWaiter) Park() { atomic.Add(&wasmMultiSched.active, ^uint32(0)) wakeWasmEventWorker() worker := gp.context.platform.owner - gp.context.platform.context.Swap(&worker.system, nil) + gp.context.platform.context.Swap( + &worker.system, + wasmWorkerSystemRootPointer(worker), + ) atomic.Store(&w.notified, uint32(0)) } diff --git a/runtime/internal/runtime/tinygogc/gc_tinygo.go b/runtime/internal/runtime/tinygogc/gc_tinygo.go index 65aceb822e..b742c1719f 100644 --- a/runtime/internal/runtime/tinygogc/gc_tinygo.go +++ b/runtime/internal/runtime/tinygogc/gc_tinygo.go @@ -387,19 +387,18 @@ func gc() (freeBytes uintptr) { println("running collection cycle...") } + gcStopWorld() + // Mark phase: mark all reachable objects, recursively. gcMarkReachable() finishMark() - // If we're using threads, resume all other threads before starting the - // sweep. - gcResumeWorld() - // Sweep phase: free all non-marked objects and unmark marked objects for // the next collection cycle. freeBytes = sweep() + gcResumeWorld() return } @@ -573,9 +572,5 @@ func growHeap() bool { return true } -func gcResumeWorld() { - // Nothing to do here (single threaded). -} - //go:linkname getsp llgo.stackSave func getsp() unsafe.Pointer diff --git a/runtime/internal/runtime/tinygogc/gc_wasm_js_workers.go b/runtime/internal/runtime/tinygogc/gc_wasm_js_workers.go new file mode 100644 index 0000000000..7230d18cfa --- /dev/null +++ b/runtime/internal/runtime/tinygogc/gc_wasm_js_workers.go @@ -0,0 +1,27 @@ +//go:build js && wasm && llgo_wasm_gc && llgo.wasm_workers + +package tinygogc + +import "unsafe" + +// Emscripten's pthread support calls the internal musl allocator entry points +// directly when the system allocator is disabled. + +//export __libc_malloc +func __libc_malloc(size uintptr) unsafe.Pointer { + return Alloc(size) +} + +//export __libc_calloc +func __libc_calloc(nmemb, size uintptr) unsafe.Pointer { + return wasmCalloc(nmemb, size) +} + +//export __libc_realloc +func __libc_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { + return Realloc(ptr, size) +} + +//export __libc_free +func __libc_free(ptr unsafe.Pointer) { +} diff --git a/runtime/internal/runtime/tinygogc/mutex.go b/runtime/internal/runtime/tinygogc/mutex.go index ba13813a41..030e0eca79 100644 --- a/runtime/internal/runtime/tinygogc/mutex.go +++ b/runtime/internal/runtime/tinygogc/mutex.go @@ -1,3 +1,5 @@ +//go:build !llgo || !js || !wasm || !llgo_wasm_gc || !llgo.wasm_workers + package tinygogc // TODO(MeteorsLiu): mutex lock for baremetal GC diff --git a/runtime/internal/runtime/tinygogc/mutex_wasm_workers.go b/runtime/internal/runtime/tinygogc/mutex_wasm_workers.go new file mode 100644 index 0000000000..b149e8aecb --- /dev/null +++ b/runtime/internal/runtime/tinygogc/mutex_wasm_workers.go @@ -0,0 +1,22 @@ +//go:build llgo && js && wasm && llgo_wasm_gc && llgo.wasm_workers + +package tinygogc + +import ( + _ "unsafe" + + "github.com/goplus/llgo/runtime/internal/wasmsync" +) + +type mutex = wasmsync.Mutex + +func lock(m *mutex) { + m.Lock(gcAllocatorYield) +} + +func unlock(m *mutex) { + m.Unlock() +} + +//go:linkname gcAllocatorYield github.com/goplus/llgo/runtime/internal/runtime.wasmGCAllocatorYield +func gcAllocatorYield() diff --git a/runtime/internal/runtime/tinygogc/world_default.go b/runtime/internal/runtime/tinygogc/world_default.go new file mode 100644 index 0000000000..1b19b219f4 --- /dev/null +++ b/runtime/internal/runtime/tinygogc/world_default.go @@ -0,0 +1,6 @@ +//go:build !llgo || !js || !wasm || !llgo_wasm_gc || !llgo.wasm_workers + +package tinygogc + +func gcStopWorld() {} +func gcResumeWorld() {} diff --git a/runtime/internal/runtime/tinygogc/world_wasm_workers.go b/runtime/internal/runtime/tinygogc/world_wasm_workers.go new file mode 100644 index 0000000000..cdf8dc4125 --- /dev/null +++ b/runtime/internal/runtime/tinygogc/world_wasm_workers.go @@ -0,0 +1,19 @@ +//go:build llgo && js && wasm && llgo_wasm_gc && llgo.wasm_workers + +package tinygogc + +import _ "unsafe" + +func gcStopWorld() { + wasmStopWorld() +} + +func gcResumeWorld() { + wasmResumeWorld() +} + +//go:linkname wasmStopWorld github.com/goplus/llgo/runtime/internal/runtime.wasmGCStopTheWorld +func wasmStopWorld() + +//go:linkname wasmResumeWorld github.com/goplus/llgo/runtime/internal/runtime.wasmGCResumeWorld +func wasmResumeWorld() diff --git a/runtime/internal/runtime/wasm_gc_stw.go b/runtime/internal/runtime/wasm_gc_stw.go new file mode 100644 index 0000000000..d04302e134 --- /dev/null +++ b/runtime/internal/runtime/wasm_gc_stw.go @@ -0,0 +1,127 @@ +//go:build llgo && js && wasm && llgo.wasm_workers && llgo_wasm_gc + +package runtime + +import ( + "unsafe" + + "github.com/goplus/llgo/runtime/internal/clite/sync/atomic" + "github.com/goplus/llgo/runtime/internal/wasmworkers" +) + +const noWasmGCOwner = ^uint32(0) + +type wasmWorkerGCState struct { + systemRoot wasmGCRootContext + epoch uint32 + ready uint32 +} + +var wasmGCWorld struct { + epoch uint32 + stopped uint32 + ready uint32 + owner uint32 +} + +func initWasmWorkerGCSystem(worker *wasmWorker) { + if worker.index == 0 { + registerWasmGCRoot(&worker.gc.systemRoot, false) + // FiberInitCurrent adopts this call stack as the system fiber without + // switching stacks, so its compiler root chain must remain installed. + adoptWasmGCRoot(&worker.gc.systemRoot) + } else { + registerWasmGCRoot(&worker.gc.systemRoot, true) + } + atomic.Store(&worker.gc.ready, uint32(1)) + atomic.Add(&wasmGCWorld.ready, uint32(1)) + wasmWorkerStopForGC(worker) +} + +func wasmWorkerSystemRootPointer(worker *wasmWorker) unsafe.Pointer { + return wasmGCRootPointer(&worker.gc.systemRoot) +} + +func wasmGCRequestPending(worker *wasmWorker) bool { + epoch := atomic.Load(&wasmGCWorld.epoch) + return epoch&1 != 0 && wasmGCWorldOwner(worker) != atomic.Load(&wasmGCWorld.owner) +} + +func wasmWorkerStopForGC(worker *wasmWorker) bool { + stopped := false + for { + epoch := atomic.Load(&wasmGCWorld.epoch) + if epoch&1 == 0 || wasmGCWorldOwner(worker) == atomic.Load(&wasmGCWorld.owner) { + return stopped + } + if worker.gc.epoch != epoch { + worker.gc.epoch = epoch + publishWasmGCRoot() + atomic.Add(&wasmGCWorld.stopped, uint32(1)) + wasmworkers.Wake(&wasmGCWorld.stopped) + } + for atomic.Load(&wasmGCWorld.epoch) == epoch { + wasmworkers.Wait(&wasmGCWorld.epoch, epoch, -1) + } + stopped = true + } +} + +func wasmGCStopTheWorld() { + worker := currentWasmWorker() + owner := wasmGCWorldOwner(worker) + atomic.Store(&wasmGCWorld.owner, owner) + atomic.Store(&wasmGCWorld.stopped, uint32(0)) + epoch := atomic.Add(&wasmGCWorld.epoch, uint32(1)) + 1 + if epoch&1 == 0 { + fatal("runtime: overlapping WebAssembly GC cycles") + return + } + wakeAllWasmWorkers() + + target := atomic.Load(&wasmGCWorld.ready) + if worker != nil && atomic.Load(&worker.gc.ready) != 0 { + target-- + } + for atomic.Load(&wasmGCWorld.stopped) < target { + stopped := atomic.Load(&wasmGCWorld.stopped) + wasmworkers.Wait(&wasmGCWorld.stopped, stopped, -1) + } +} + +func wasmGCResumeWorld() { + epoch := atomic.Load(&wasmGCWorld.epoch) + if epoch&1 == 0 { + fatal("runtime: resumed a running WebAssembly world") + return + } + atomic.Store(&wasmGCWorld.epoch, epoch+1) + atomic.Store(&wasmGCWorld.owner, noWasmGCOwner) + wasmworkers.Wake(&wasmGCWorld.epoch) + wakeAllWasmWorkers() +} + +func wasmGCAllocatorYield() { + worker := currentWasmWorker() + if worker == nil { + return + } + if getg() == nil { + wasmWorkerStopForGC(worker) + return + } + CooperativeSafepoint() +} + +func wasmGCWorldOwner(worker *wasmWorker) uint32 { + if worker == nil { + return noWasmGCOwner + } + return uint32(worker.index) +} + +func wakeAllWasmWorkers() { + for i := 0; i < wasmMultiSched.count; i++ { + wakeWasmWorker(&wasmMultiSched.workers[i]) + } +} diff --git a/runtime/internal/runtime/wasm_gc_stw_stub.go b/runtime/internal/runtime/wasm_gc_stw_stub.go new file mode 100644 index 0000000000..356cb34620 --- /dev/null +++ b/runtime/internal/runtime/wasm_gc_stw_stub.go @@ -0,0 +1,17 @@ +//go:build llgo && js && wasm && llgo.wasm_workers && !llgo_wasm_gc + +package runtime + +import "unsafe" + +type wasmWorkerGCState struct{} + +func initWasmWorkerGCSystem(*wasmWorker) {} + +func wasmWorkerSystemRootPointer(*wasmWorker) unsafe.Pointer { return nil } + +func wasmGCRequestPending(*wasmWorker) bool { return false } + +func wasmWorkerStopForGC(*wasmWorker) bool { return false } + +func wasmGCAllocatorYield() {} diff --git a/runtime/internal/runtime/wasm_gcroot.go b/runtime/internal/runtime/wasm_gcroot.go index 0b83b67862..90c304ff0d 100644 --- a/runtime/internal/runtime/wasm_gcroot.go +++ b/runtime/internal/runtime/wasm_gcroot.go @@ -28,6 +28,10 @@ func adoptWasmGCRoot(ctx *wasmGCRootContext) { gcroot.AdoptCurrent(ctx) } +func publishWasmGCRoot() { + gcroot.PublishCurrent() +} + func unregisterWasmGCRoot(ctx *wasmGCRootContext) { gcroot.Unregister(ctx) } diff --git a/runtime/internal/runtime/wasm_gcroot_stub.go b/runtime/internal/runtime/wasm_gcroot_stub.go index 701be4278e..6dbd86796f 100644 --- a/runtime/internal/runtime/wasm_gcroot_stub.go +++ b/runtime/internal/runtime/wasm_gcroot_stub.go @@ -14,4 +14,6 @@ func wasmGCRootPointer(*wasmGCRootContext) unsafe.Pointer { return nil } func adoptWasmGCRoot(*wasmGCRootContext) {} +func publishWasmGCRoot() {} + func unregisterWasmGCRoot(*wasmGCRootContext) {} diff --git a/runtime/internal/wasmevent/runtime_mutex_workers.go b/runtime/internal/wasmevent/runtime_mutex_workers.go index 128c6b8e3a..ac9c7a7a26 100644 --- a/runtime/internal/wasmevent/runtime_mutex_workers.go +++ b/runtime/internal/wasmevent/runtime_mutex_workers.go @@ -2,24 +2,26 @@ package wasmevent -import "github.com/goplus/llgo/runtime/internal/clite/pthread/sync" +import "github.com/goplus/llgo/runtime/internal/wasmsync" type runtimeMutex struct { - mutex sync.Mutex + mutex wasmsync.Mutex } -func newRuntimeMutex() runtimeMutex { - var result runtimeMutex - if result.mutex.Init(nil) != 0 { - panic("wasmevent: failed to initialize timer mutex") - } - return result -} +var cooperativeSafepoint func() + +func newRuntimeMutex() runtimeMutex { return runtimeMutex{} } func (m *runtimeMutex) Lock() { - m.mutex.Lock() + m.mutex.Lock(cooperativeSafepoint) } func (m *runtimeMutex) Unlock() { m.mutex.Unlock() } + +// InstallCooperativeSafepoint installs the worker scheduler poll used while +// the timer queue lock is contended. +func InstallCooperativeSafepoint(safepoint func()) { + cooperativeSafepoint = safepoint +} diff --git a/runtime/internal/wasmsync/mutex.go b/runtime/internal/wasmsync/mutex.go new file mode 100644 index 0000000000..41e338253c --- /dev/null +++ b/runtime/internal/wasmsync/mutex.go @@ -0,0 +1,37 @@ +//go:build llgo && js && wasm && llgo.wasm_workers + +// Package wasmsync provides synchronization primitives that can cooperate +// with the WebAssembly worker scheduler while contended. +package wasmsync + +import ( + "github.com/goplus/llgo/runtime/internal/clite/sync/atomic" + "github.com/goplus/llgo/runtime/internal/wasmworkers" +) + +const mutexWaitNanoseconds = int64(1_000_000) + +// Mutex is a zero-value-ready lock for worker-shared runtime state. +type Mutex struct { + state uint32 +} + +// Lock acquires m. A contended lock periodically calls yield so a worker +// blocked behind an allocator can acknowledge a stop-the-world request. +func (m *Mutex) Lock(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)) + wasmworkers.Wake(&m.state) +} From 3c184b629556d5b0b11133ffabed539960cc1d3d Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 04:42:48 +0800 Subject: [PATCH 3/3] test/wasm: cover multi-worker garbage collection --- .github/workflows/llgo.yml | 16 ++- internal/build/testdata/wasm-gc/main.go | 1 + internal/build/testdata/wasm-gc/workers.go | 103 ++++++++++++++++++ .../build/testdata/wasm-gc/workers_stub.go | 5 + 4 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 internal/build/testdata/wasm-gc/workers.go create mode 100644 internal/build/testdata/wasm-gc/workers_stub.go diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 69913350c9..1d71d62516 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -485,9 +485,10 @@ jobs: } run_wasm_workers() { + local expected="$2" local output output=$(node -e "import('$1').then(module => module.default()).catch(error => { console.error(error); process.exit(1); });" 2>&1) - grep -Fxq "wasm workers ok" <<<"$output" + grep -Fxq "$expected" <<<"$output" } run_wasi_timers() { @@ -516,10 +517,16 @@ jobs: run_wasi_timers "$RUNNER_TEMP/wasm-timers-wasip1.wasm" LLGO_WASM_WORKERS=2 GOOS=js GOARCH=wasm llgo build \ -o "$RUNNER_TEMP/wasm-workers-go.mjs" ./internal/build/testdata/wasm-workers - run_wasm_workers "$RUNNER_TEMP/wasm-workers-go.mjs" + run_wasm_workers "$RUNNER_TEMP/wasm-workers-go.mjs" "wasm workers ok" LLGO_WASM_WORKERS=2 llgo build -target wasm \ -o "$RUNNER_TEMP/wasm-workers.mjs" ./internal/build/testdata/wasm-workers - run_wasm_workers "$RUNNER_TEMP/wasm-workers.mjs" + run_wasm_workers "$RUNNER_TEMP/wasm-workers.mjs" "wasm workers ok" + LLGO_WASM_WORKERS=2 GOOS=js GOARCH=wasm llgo build \ + -o "$RUNNER_TEMP/wasm-gc-workers-go.mjs" ./internal/build/testdata/wasm-gc + run_wasm_workers "$RUNNER_TEMP/wasm-gc-workers-go.mjs" "wasm gc ok" + LLGO_WASM_WORKERS=2 llgo build -target wasm \ + -o "$RUNNER_TEMP/wasm-gc-workers.mjs" ./internal/build/testdata/wasm-gc + run_wasm_workers "$RUNNER_TEMP/wasm-gc-workers.mjs" "wasm gc ok" 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=$! @@ -533,7 +540,8 @@ jobs: curl -fsS http://127.0.0.1:8123/ >/dev/null browser="$(command -v google-chrome || command -v google-chrome-stable || command -v chromium || true)" test -n "$browser" - for module in wasm-timers.mjs wasm-workers.mjs wasm-workers-go.mjs; do + for module in wasm-timers.mjs wasm-workers.mjs wasm-workers-go.mjs \ + wasm-gc-workers.mjs wasm-gc-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-gc/main.go b/internal/build/testdata/wasm-gc/main.go index f701f0aa06..d3b6de93b5 100644 --- a/internal/build/testdata/wasm-gc/main.go +++ b/internal/build/testdata/wasm-gc/main.go @@ -27,6 +27,7 @@ func main() { testRecoveredRootChain() testReclamation() testHeapGrowth() + testMultiWorkerGC() println("wasm gc ok") } diff --git a/internal/build/testdata/wasm-gc/workers.go b/internal/build/testdata/wasm-gc/workers.go new file mode 100644 index 0000000000..dfbe71bcc3 --- /dev/null +++ b/internal/build/testdata/wasm-gc/workers.go @@ -0,0 +1,103 @@ +//go:build llgo.wasm_workers + +package main + +import ( + "runtime" + "sync/atomic" + _ "unsafe" +) + +//go:linkname schedulerProcID github.com/goplus/llgo/runtime/internal/runtime.SchedulerProcID +func schedulerProcID() int + +func testMultiWorkerGC() { + testRemoteWorkerGC() + testConcurrentWorkerAllocation() +} + +func testRemoteWorkerGC() { + const want = uint64(0x76543210) + live := &payload{value: want} + + for { + start := make(chan bool) + ready := make(chan int) + var ( + done atomic.Bool + result atomic.Uint64 + ) + go func() { + ready <- schedulerProcID() + if !<-start { + done.Store(true) + return + } + remoteLive := &payload{value: want} + for range 8 { + runtime.GC() + for i := range 512 { + garbage = &payload{value: uint64(i)} + } + garbage = nil + } + result.Store(remoteLive.value) + done.Store(true) + }() + + if proc := <-ready; proc == 0 { + start <- false + for !done.Load() { + } + continue + } + start <- true + for !done.Load() { + if live.value != want { + panic("remote GC lost an active worker root") + } + } + if result.Load() != want || live.value != want { + panic("remote worker GC lost a live root") + } + return + } +} + +func testConcurrentWorkerAllocation() { + const ( + goroutines = 4 + iterations = 4096 + liveCount = 32 + ) + start := make(chan struct{}) + done := make(chan int, goroutines) + for id := range goroutines { + go func() { + <-start + var live [liveCount]*payload + for i := range iterations { + slot := i % len(live) + live[slot] = &payload{value: uint64(id*iterations + i + 1)} + if i%1024 == 0 { + runtime.GC() + } + } + for _, value := range live { + if value == nil || value.value == 0 { + panic("concurrent allocation lost a live object") + } + } + done <- schedulerProcID() + }() + } + close(start) + + workers := make(map[int]bool) + for range goroutines { + workers[<-done] = true + } + if len(workers) < 2 { + panic("concurrent GC did not cover multiple workers") + } +} diff --git a/internal/build/testdata/wasm-gc/workers_stub.go b/internal/build/testdata/wasm-gc/workers_stub.go new file mode 100644 index 0000000000..779904071a --- /dev/null +++ b/internal/build/testdata/wasm-gc/workers_stub.go @@ -0,0 +1,5 @@ +//go:build !llgo.wasm_workers + +package main + +func testMultiWorkerGC() {}