Files
tinygo/testdata/finalizer.go
T

129 lines
3.4 KiB
Go

package main
// Tests for runtime.SetFinalizer on the block GC.
//
// This test is only run on the precise wasm/wasi targets (see the tests slice
// and the skip in main_test.go): they track stack pointers precisely and
// reliably collect a dropped object, so the finalizer deterministically fires.
// The host default GC is boehm, where SetFinalizer is a no-op, and conservative
// stack scanning on the emulated targets cannot reliably collect the object, so
// firing cannot be asserted there.
import "runtime"
type T struct{ x int }
var (
ranCount int
clearedRan int
f1Ran int
f2Ran int
sink int
)
// scrubStack overwrites the stack region used by an alloc-and-drop helper with
// non-pointer words. It must be called at the same call depth as that helper so
// this recursion reuses (and clears) the frame that just held the dropped
// pointer; otherwise a stale copy keeps the object marked and it is never
// collected. The returned value derived from buf keeps the writes live.
//
//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]
}
// allocAndDrop allocates an object, registers a finalizer, and returns without
// leaking any reference to it, so the object becomes unreachable. The finalizer
// must not capture the object (that would pin it forever): it takes the pointer
// as its argument and touches only a package global.
//
//go:noinline
func allocAndDrop() {
p := &T{x: 42}
runtime.SetFinalizer(p, func(*T) { ranCount++ })
}
//go:noinline
func allocRegisterClear() {
p := &T{x: 1}
runtime.SetFinalizer(p, func(*T) { clearedRan++ })
runtime.SetFinalizer(p, nil)
}
//go:noinline
func allocRegisterReplace() {
p := &T{x: 2}
runtime.SetFinalizer(p, func(*T) { f1Ran++ })
runtime.SetFinalizer(p, func(*T) { f2Ran++ })
}
// testFires checks that a finalizer runs after its object is collected, and
// only once. scrubStack and the alloc helper are both called here, at the same
// depth, so the scrub clears the helper's stale frame. Gosched lets the
// dedicated finalizer goroutine drain (a no-op under scheduler=none, where
// finalizers already ran inline during GC).
func testFires() {
allocAndDrop()
for i := 0; i < 100 && ranCount == 0; i++ {
sink += scrubStack(40)
runtime.GC()
runtime.Gosched()
}
if ranCount == 0 {
panic("finalizer: never ran after object became unreachable")
}
for i := 0; i < 100; i++ {
sink += scrubStack(40)
runtime.GC()
runtime.Gosched()
}
if ranCount != 1 {
panic("finalizer: ran more than once")
}
}
// testClear checks that SetFinalizer(obj, nil) removes a finalizer.
func testClear() {
allocRegisterClear()
for i := 0; i < 100; i++ {
sink += scrubStack(40)
runtime.GC()
runtime.Gosched()
}
if clearedRan != 0 {
panic("finalizer: ran after being cleared with nil")
}
}
// testReplace checks that re-registering replaces the finalizer: only the latest
// one runs, and only once.
func testReplace() {
allocRegisterReplace()
for i := 0; i < 100 && f2Ran == 0; i++ {
sink += scrubStack(40)
runtime.GC()
runtime.Gosched()
}
if f1Ran != 0 {
panic("finalizer: replaced finalizer f1 still ran")
}
if f2Ran != 1 {
panic("finalizer: replacement finalizer f2 did not run exactly once")
}
}
func main() {
testFires()
testClear()
testReplace()
println("ok")
}