mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-07-26 06:38:42 +00:00
129 lines
3.4 KiB
Go
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")
|
|
}
|