mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-31 09:49:03 +00:00
31fff2c9a3
* 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
183 lines
4.3 KiB
Go
183 lines
4.3 KiB
Go
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")
|
|
}
|