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
+177
@@ -0,0 +1,177 @@
|
||||
package main
|
||||
|
||||
// Test finalizer registration, replacement, removal, and reused heap addresses.
|
||||
// The wasm target provides deterministic finalization for these tests.
|
||||
|
||||
import "runtime"
|
||||
|
||||
type box struct{ x int }
|
||||
|
||||
const batch = 32
|
||||
|
||||
var (
|
||||
reregisteredRan int
|
||||
replacedOldRan int
|
||||
replacedNewRan int
|
||||
churnRan int
|
||||
reuseFirstRan int
|
||||
reuseSecondRan int
|
||||
keptRan int
|
||||
droppedRan int
|
||||
sink int
|
||||
)
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func allocClearThenRegister() {
|
||||
p := &box{x: 1}
|
||||
runtime.SetFinalizer(p, func(*box) { panic("cleared finalizer ran") })
|
||||
runtime.SetFinalizer(p, nil)
|
||||
runtime.SetFinalizer(p, func(*box) { reregisteredRan++ })
|
||||
}
|
||||
|
||||
func testClearThenRegister() {
|
||||
allocClearThenRegister()
|
||||
for i := 0; i < 200 && reregisteredRan == 0; i++ {
|
||||
sink += scrubStack(40)
|
||||
runtime.GC()
|
||||
runtime.Gosched()
|
||||
}
|
||||
if reregisteredRan != 1 {
|
||||
panic("finalizerbits: re-registered finalizer did not run exactly once")
|
||||
}
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func allocRegisterTwice() {
|
||||
for i := 0; i < batch; i++ {
|
||||
p := &box{x: i}
|
||||
runtime.SetFinalizer(p, func(*box) { replacedOldRan++ })
|
||||
runtime.SetFinalizer(p, func(*box) { replacedNewRan++ })
|
||||
}
|
||||
}
|
||||
|
||||
func testRegisterTwiceLeavesOne() {
|
||||
allocRegisterTwice()
|
||||
for i := 0; i < 200 && replacedNewRan < batch; i++ {
|
||||
sink += scrubStack(40)
|
||||
runtime.GC()
|
||||
runtime.Gosched()
|
||||
}
|
||||
if replacedOldRan != 0 {
|
||||
panic("finalizerbits: replaced finalizer still ran")
|
||||
}
|
||||
if replacedNewRan != batch {
|
||||
panic("finalizerbits: replacement did not run exactly once per object")
|
||||
}
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func allocChurn() {
|
||||
p := &box{x: 3}
|
||||
for i := 0; i < 64; i++ {
|
||||
runtime.SetFinalizer(p, func(*box) { churnRan++ })
|
||||
runtime.SetFinalizer(p, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func testChurnLeavesNothing() {
|
||||
allocChurn()
|
||||
for i := 0; i < 200; i++ {
|
||||
sink += scrubStack(40)
|
||||
runtime.GC()
|
||||
runtime.Gosched()
|
||||
}
|
||||
if churnRan != 0 {
|
||||
panic("finalizerbits: churned register/clear left a live registration")
|
||||
}
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func allocFirstRound() {
|
||||
for i := 0; i < batch; i++ {
|
||||
p := &box{x: i}
|
||||
runtime.SetFinalizer(p, func(*box) { reuseFirstRan++ })
|
||||
}
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func allocSecondRound() {
|
||||
for i := 0; i < batch; i++ {
|
||||
p := &box{x: i}
|
||||
runtime.SetFinalizer(p, func(*box) { reuseSecondRan++ })
|
||||
}
|
||||
}
|
||||
|
||||
func testAddressReuse() {
|
||||
allocFirstRound()
|
||||
for i := 0; i < 200 && reuseFirstRan < batch; i++ {
|
||||
sink += scrubStack(40)
|
||||
runtime.GC()
|
||||
runtime.Gosched()
|
||||
}
|
||||
if reuseFirstRan != batch {
|
||||
panic("finalizerbits: first round did not run every finalizer")
|
||||
}
|
||||
allocSecondRound()
|
||||
for i := 0; i < 200 && reuseSecondRan < batch; i++ {
|
||||
sink += scrubStack(40)
|
||||
runtime.GC()
|
||||
runtime.Gosched()
|
||||
}
|
||||
if reuseSecondRan != batch {
|
||||
panic("finalizerbits: second round into reused memory lost finalizers")
|
||||
}
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func allocMixedBatch() {
|
||||
for i := 0; i < batch; i++ {
|
||||
p := &box{x: i}
|
||||
if i%2 == 0 {
|
||||
runtime.SetFinalizer(p, func(*box) { droppedRan++ })
|
||||
runtime.SetFinalizer(p, nil)
|
||||
} else {
|
||||
runtime.SetFinalizer(p, func(*box) { keptRan++ })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testMixedBatch() {
|
||||
allocMixedBatch()
|
||||
for i := 0; i < 200 && keptRan < batch/2; i++ {
|
||||
sink += scrubStack(40)
|
||||
runtime.GC()
|
||||
runtime.Gosched()
|
||||
}
|
||||
if droppedRan != 0 {
|
||||
panic("finalizerbits: a cleared finalizer inside the batch ran")
|
||||
}
|
||||
if keptRan != batch/2 {
|
||||
panic("finalizerbits: kept finalizers did not all run exactly once")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
testClearThenRegister()
|
||||
testRegisterTwiceLeavesOne()
|
||||
testChurnLeavesNothing()
|
||||
testAddressReuse()
|
||||
testMixedBatch()
|
||||
println("ok")
|
||||
}
|
||||
Reference in New Issue
Block a user