mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-09-01 02:09:06 +00:00
runtime: run syscall/js finalizers on wasm without a manual GC (#5545)
* runtime: run syscall/js finalizers on wasm without a manual GC * runtime: address review feedback on finalizer idle GC * runtime: clear a finished task's args pointer so its arguments are collectable * runtime: skip the finalizer scan with a per-block registration bit * runtime: guard the finalizer registration bitmap with gcLock * testdata: cover finalizer invariants on every scheduler * main_test: limit the finalizer scheduler variants to linux and darwin * testdata: wait for the finalizer queue to drain before asserting * testdata: make the finalizer counters atomic and wait for a known drain count * runtime: add finalizer bookkeeping asserts under runtime_asserts * runtime: address finalizer GC review feedback * testdata: strengthen blocked stack finalizer test * runtime: fix finalizer cleanup edge cases * runtime: decouple wasm export scheduling from finalizers * runtime: avoid redundant wakeups for re-entrant wasm exports * runtime: simplify finalizer comments
This commit is contained in:
Vendored
+182
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
// Test idle finalizer collection and the lifetime of blocked and completed asyncify stacks.
|
||||
// The wasm target provides deterministic finalization for these tests.
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// batch must exceed the finalizer registration threshold to trigger idle collection.
|
||||
const batch = 64
|
||||
|
||||
var (
|
||||
ranDropped int
|
||||
ranOnStack int
|
||||
ranInArgs int
|
||||
sink int
|
||||
|
||||
blockedRan [3]atomic.Int32
|
||||
controlRan atomic.Int32
|
||||
)
|
||||
|
||||
type blockedObject struct{ x int }
|
||||
|
||||
//go:noinline
|
||||
func blockOperation(kind int, ready chan<- struct{}, ch chan struct{}) {
|
||||
ready <- struct{}{}
|
||||
switch kind {
|
||||
case 0:
|
||||
select {}
|
||||
case 1:
|
||||
ch <- struct{}{}
|
||||
case 2:
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func holdWhileBlocked(kind int, ready chan<- struct{}, ch chan struct{}) {
|
||||
p := &blockedObject{x: kind}
|
||||
runtime.SetFinalizer(p, func(*blockedObject) { blockedRan[kind].Add(1) })
|
||||
blockOperation(kind, ready, ch)
|
||||
// blockOperation can return, so p remains live on this suspended stack.
|
||||
runtime.KeepAlive(p)
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func dropProgressControl() {
|
||||
p := &blockedObject{x: 8}
|
||||
runtime.SetFinalizer(p, func(*blockedObject) { controlRan.Add(1) })
|
||||
}
|
||||
|
||||
// testPermanentlyBlockedStacks checks that blocked task stacks remain GC roots.
|
||||
// A control finalizer confirms that GC and finalizer processing made progress.
|
||||
func testPermanentlyBlockedStacks() {
|
||||
ready := make(chan struct{}, 3)
|
||||
go holdWhileBlocked(0, ready, nil) // select{}
|
||||
go holdWhileBlocked(1, ready, nil) // nil channel send
|
||||
go holdWhileBlocked(2, ready, nil) // nil channel receive
|
||||
<-ready
|
||||
<-ready
|
||||
<-ready
|
||||
|
||||
dropProgressControl()
|
||||
for i := 0; i < 100 && controlRan.Load() == 0; i++ {
|
||||
sink += scrubStack(40)
|
||||
runtime.GC()
|
||||
runtime.Gosched()
|
||||
}
|
||||
if controlRan.Load() != 1 {
|
||||
panic("control finalizer did not prove GC progress")
|
||||
}
|
||||
// Collect once more so temporary scheduler roots cannot hide an unrooted
|
||||
// blocked task during the control collection.
|
||||
runtime.GC()
|
||||
runtime.Gosched()
|
||||
for i, name := range [...]string{"select{}", "nil-channel send", "nil-channel receive"} {
|
||||
if blockedRan[i].Load() != 0 {
|
||||
panic(name + " stack-held object was finalized")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scrubStack removes stale pointers from the helper frame so collection is deterministic.
|
||||
// Call it at the same call depth as the allocation helper.
|
||||
//
|
||||
//go:noinline
|
||||
func scrubStack(depth int) int {
|
||||
if depth <= 0 {
|
||||
return sink
|
||||
}
|
||||
var buf [64]int
|
||||
for i := range buf {
|
||||
buf[i] = depth + i
|
||||
}
|
||||
sink += buf[depth&63]
|
||||
return scrubStack(depth-1) + buf[0]
|
||||
}
|
||||
|
||||
// registerAndDrop creates unreachable objects with finalizers that do not capture them.
|
||||
// This allows the idle GC to collect the objects.
|
||||
//
|
||||
//go:noinline
|
||||
func registerAndDrop() {
|
||||
for i := 0; i < batch; i++ {
|
||||
p := new([2]int)
|
||||
runtime.SetFinalizer(p, func(*[2]int) { ranDropped++ })
|
||||
}
|
||||
}
|
||||
|
||||
func testIdleCollect() {
|
||||
registerAndDrop()
|
||||
for i := 0; i < 500 && ranDropped < batch; i++ {
|
||||
sink += scrubStack(40)
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
if ranDropped != batch {
|
||||
panic("idle collection did not run every finalizer")
|
||||
}
|
||||
}
|
||||
|
||||
func testFinishedGoroutineStacks() {
|
||||
done := make(chan struct{})
|
||||
for i := 0; i < batch; i++ {
|
||||
go func() {
|
||||
p := new([2]int)
|
||||
runtime.SetFinalizer(p, func(*[2]int) { ranOnStack++ })
|
||||
// p stays on this goroutine's stack until it returns just below.
|
||||
done <- struct{}{}
|
||||
}()
|
||||
}
|
||||
for i := 0; i < batch; i++ {
|
||||
<-done
|
||||
}
|
||||
for i := 0; i < 500 && ranOnStack < batch; i++ {
|
||||
sink += scrubStack(40)
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
if ranOnStack != batch {
|
||||
panic("finished goroutine stack still pinned finalized objects")
|
||||
}
|
||||
}
|
||||
|
||||
// launchArgGoroutine passes an object through the task argument bundle.
|
||||
// The caller returns so scrubStack can remove its transient pointer.
|
||||
//
|
||||
//go:noinline
|
||||
func launchArgGoroutine(done chan struct{}) {
|
||||
p := new([2]int)
|
||||
runtime.SetFinalizer(p, func(*[2]int) { ranInArgs++ })
|
||||
go func(q *[2]int) {
|
||||
sink += q[0]
|
||||
done <- struct{}{}
|
||||
}(p)
|
||||
}
|
||||
|
||||
func testFinishedGoroutineArgs() {
|
||||
done := make(chan struct{})
|
||||
for i := 0; i < batch; i++ {
|
||||
launchArgGoroutine(done)
|
||||
}
|
||||
for i := 0; i < batch; i++ {
|
||||
<-done
|
||||
}
|
||||
for i := 0; i < 500 && ranInArgs < batch; i++ {
|
||||
sink += scrubStack(40)
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
if ranInArgs != batch {
|
||||
panic("finished goroutine args still pinned finalized objects")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
testPermanentlyBlockedStacks()
|
||||
testIdleCollect()
|
||||
testFinishedGoroutineStacks()
|
||||
testFinishedGoroutineArgs()
|
||||
println("ok")
|
||||
}
|
||||
Reference in New Issue
Block a user