diff --git a/compileopts/finalizer_coverage_test.go b/compileopts/finalizer_coverage_test.go index bf12e0c5e4..192468838a 100644 --- a/compileopts/finalizer_coverage_test.go +++ b/compileopts/finalizer_coverage_test.go @@ -21,22 +21,22 @@ func TestFinalizerRunnerSchedulerCoverage(t *testing.T) { exprs[i] = readBuildConstraint(t, filepath.Join("..", "src", "runtime", name)) } - for _, sched := range validSchedulerOptions { - // The finalizer table exists under block GCs. - // gc.conservative satisfies the GC condition in every constraint. - tags := map[string]bool{ - "gc.conservative": true, - "scheduler." + sched: true, - } - var matched []string - for i, expr := range exprs { - if expr.Eval(func(tag string) bool { return tags[tag] }) { - matched = append(matched, files[i]) + for _, gc := range []string{"conservative", "precise", "boehm"} { + for _, sched := range validSchedulerOptions { + tags := map[string]bool{ + "gc." + gc: true, + "scheduler." + sched: true, + } + var matched []string + for i, expr := range exprs { + if expr.Eval(func(tag string) bool { return tags[tag] }) { + matched = append(matched, files[i]) + } + } + if len(matched) != 1 { + t.Errorf("gc.%s scheduler.%s: spawnFinalizerRunner defined in %d files %v, want exactly 1", + gc, sched, len(matched), matched) } - } - if len(matched) != 1 { - t.Errorf("scheduler.%s: spawnFinalizerRunner defined in %d files %v, want exactly 1", - sched, len(matched), matched) } } } diff --git a/main_test.go b/main_test.go index 2a915347c0..6467271125 100644 --- a/main_test.go +++ b/main_test.go @@ -54,6 +54,88 @@ var supportedLinuxArches = map[string]string{ var sema = make(chan struct{}, runtime.NumCPU()) +func TestBoehmFinalizerInterior(t *testing.T) { + t.Parallel() + for _, test := range []struct { + target, scheduler string + }{ + {"wasm", "asyncify"}, + {"wasm", "none"}, + {"", "tasks"}, + {"", "threads"}, + {"", "none"}, + } { + if test.target == "" && runtime.GOOS != "linux" && runtime.GOOS != "darwin" { + continue + } + t.Run(test.target+"/"+test.scheduler, func(t *testing.T) { + t.Parallel() + options := optionsFromTarget(test.target, sema) + options.GC = "boehm" + options.Scheduler = test.scheduler + options.Tags = append(options.Tags, "runtime_asserts") + options.GlobalValues = map[string]map[string]string{ + "main": {"finalizerTestMode": "interior"}, + } + runTest("finalizer.go", options, t, nil, nil) + }) + } +} + +func TestBoehmFinalizerInteriorInvalid(t *testing.T) { + t.Parallel() + if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { + t.Skip("host finalizers need Linux or Darwin") + } + options := optionsFromTarget("", sema) + options.GC = "boehm" + config, err := builder.NewConfig(&options) + if err != nil { + t.Fatal(err) + } + for _, name := range []string{"interior-pointer", "interior-large"} { + t.Run(name, func(t *testing.T) { + output := &bytes.Buffer{} + _, err := buildAndRun("testdata/finalizer.go", config, output, []string{name}, nil, time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error { + cmd.Stdout = nil + cmd.Stderr = nil + data, err := cmd.CombinedOutput() + output.Write(data) + return err + }) + if err == nil || !strings.Contains(output.String(), "pointer not at beginning of allocated block") { + t.Fatalf("interior registration result: %v\n%s", err, output.String()) + } + }) + } +} + +func TestFinalizerRunnerRegistration(t *testing.T) { + t.Parallel() + for _, test := range []struct { + target, gc, scheduler string + }{ + {"wasm", "boehm", "asyncify"}, + {"wasm", "conservative", "asyncify"}, + {"", "boehm", "tasks"}, + {"", "boehm", "threads"}, + } { + if test.target == "" && runtime.GOOS != "linux" && runtime.GOOS != "darwin" { + continue + } + t.Run(test.target+"/"+test.gc+"/"+test.scheduler, func(t *testing.T) { + t.Parallel() + options := optionsFromTarget(test.target, sema) + options.GC = test.gc + options.Scheduler = test.scheduler + options.GlobalValues = map[string]map[string]string{ + "main": {"finalizerIdleMode": "registration"}, + } + runTest("finalizeridle.go", options, t, nil, nil) + }) + } +} + func TestTrimPath(t *testing.T) { t.Setenv("CGO_CFLAGS", "-iquoteinclude -includestdint.h -imacros relative.h") root := t.TempDir() @@ -418,6 +500,39 @@ func TestBuild(t *testing.T) { } }) + t.Run("boehm-finalizers", func(t *testing.T) { + t.Parallel() + options := optionsFromTarget("wasm", sema) + options.GC = "boehm" + t.Run("finalizerlarge.go-graph", func(t *testing.T) { + t.Parallel() + testOptions := compileopts.Options(options) + testOptions.GlobalValues = map[string]map[string]string{ + "main": {"finalizerLargeMode": "graph"}, + } + runTest("finalizerlarge.go", testOptions, t, nil, nil) + }) + for _, name := range []string{ + "finalizer.go", + "finalizerbits.go", + "finalizeridle.go", + "finalizerlarge.go", + "finalizerinvariants.go", + } { + name := name + t.Run(name, func(t *testing.T) { + t.Parallel() + testOptions := compileopts.Options(options) + if name == "finalizerlarge.go" { + testOptions.GlobalValues = map[string]map[string]string{ + "main": {"finalizerLargeMode": "runner-payload"}, + } + } + runTest(name, testOptions, t, nil, nil) + }) + } + }) + // Test a few build options. t.Run("build-options", func(t *testing.T) { t.Parallel() @@ -777,14 +892,6 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) { continue } } - if options.Target == "" && options.GC == "" { - switch name { - case "finalizerinvariants.go": - // Skip the default host GC because it does not implement finalizers. - // Explicit conservative GC variants cover this test. - continue - } - } if options.Target == "simavr" { switch name { case "finalizerinvariants.go": diff --git a/src/internal/reflectlite/type.go b/src/internal/reflectlite/type.go index d62e2f6e6d..ecbd4da911 100644 --- a/src/internal/reflectlite/type.go +++ b/src/internal/reflectlite/type.go @@ -171,6 +171,13 @@ type basicType struct { ptrTo *RawType } +// Go allows interior targets whose types could fit its tiny allocator. +// See runtime.SetFinalizer in https://go.dev/src/runtime/mfinal.go. +func FinalizerAllowsInterior(t *RawType) bool { + elem := t.elem() + return elem.Size() < 16 && elem.gcLayout() == gclayout.NoPtrs.AsPtr() +} + // All types that have an element type: named, chan, slice, array, map (but not // pointer because it doesn't have ptrTo). type elemType struct { diff --git a/src/runtime/gc_boehm.c b/src/runtime/gc_boehm.c index 6d4fec8ec2..666adb7822 100644 --- a/src/runtime/gc_boehm.c +++ b/src/runtime/gc_boehm.c @@ -10,6 +10,18 @@ #include void tinygo_runtime_bdwgc_callback(void); +void tinygo_runtime_bdwgc_finalizer(void *, void *); + +static void GC_CALLBACK finalizer_callback(void *obj, void *data) { + tinygo_runtime_bdwgc_finalizer(obj, data); +} + +uintptr_t tinygo_runtime_bdwgc_register_finalizer(uintptr_t obj, uintptr_t data) { + void *old_data = (void *)(uintptr_t)-1; + GC_register_finalizer_no_order((void *)obj, data ? finalizer_callback : NULL, + (void *)data, NULL, &old_data); + return (uintptr_t)old_data; +} struct descriptor_cache_entry { uintptr_t layout; @@ -74,6 +86,9 @@ static void GC_CALLBACK warn_proc(const char *msg, GC_word arg) { void tinygo_runtime_bdwgc_init(void) { GC_set_push_other_roots(callback); + GC_set_finalize_on_demand(1); + // See gc.h GC_set_java_finalization for unordered finalizer reachability. + GC_set_java_finalization(1); #if defined(__wasm__) // There are a lot of warnings on WebAssembly in the form: // diff --git a/src/runtime/gc_boehm.go b/src/runtime/gc_boehm.go index 0e91e0f09e..38c7def694 100644 --- a/src/runtime/gc_boehm.go +++ b/src/runtime/gc_boehm.go @@ -20,6 +20,7 @@ package runtime import ( "internal/gclayout" + "internal/reflectlite" "internal/task" "unsafe" ) @@ -37,6 +38,22 @@ var ( gcMallocs uint64 ) +type boehmFinalizer struct { + next *boehmFinalizer + offset uintptr + fn interface{} + ptr unsafe.Pointer // keeps the object alive after Boehm dequeues its callback +} + +var ( + finalizerPending *boehmFinalizer + numFinalizers uintptr + finalizersSinceGC uintptr + finalizerQueued bool + finalizerDraining bool + finalizerRunnerStarted bool +) + func initHeap() { libgc_init() @@ -123,8 +140,12 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer { ) } } + queued := boehmInvokeFinalizers() gcResumeWorld() gcLock.Unlock() + if queued { + wakeFinalizer() + } if ptr == nil { runtimeFatal("gc: out of memory") return nil @@ -150,8 +171,12 @@ func allocManual(size uintptr) unsafe.Pointer { gcLock.Lock() ptr := libgc_malloc_atomic_uncollectable(size) + queued := boehmInvokeFinalizers() gcResumeWorld() gcLock.Unlock() + if queued { + wakeFinalizer() + } if ptr == nil { runtimeFatal("gc: out of memory") return nil @@ -163,8 +188,12 @@ func allocManual(size uintptr) unsafe.Pointer { func free(ptr unsafe.Pointer) { gcLock.Lock() libgc_free(ptr) + queued := boehmInvokeFinalizers() gcResumeWorld() gcLock.Unlock() + if queued { + wakeFinalizer() + } } //go:noinline @@ -175,8 +204,13 @@ func freeTaskStack(ptr uintptr) { func GC() { gcLock.Lock() libgc_gcollect() + finalizersSinceGC = 0 + queued := boehmInvokeFinalizers() gcResumeWorld() gcLock.Unlock() + if queued { + wakeFinalizer() + } } // This should be stack-allocated, but we don't currently have a good way of @@ -216,11 +250,180 @@ func setHeapEnd(newHeapEnd uintptr) { } func SetFinalizer(obj interface{}, finalizer interface{}) { - // Unimplemented. - // The GC *does* support finalization, so this could be added relatively - // easily I think. + value := reflectlite.ValueOf(obj) + if value.Kind() != reflectlite.Pointer { + runtimeFatal("runtime.SetFinalizer: first argument is not a pointer") + } + if finalizer != nil && reflectlite.ValueOf(finalizer).Kind() != reflectlite.Func { + runtimeFatal("runtime.SetFinalizer: second argument is not a function") + } + objPtr := (*_interface)(unsafe.Pointer(&obj)).value + if objPtr == nil { + return + } + + var entry *boehmFinalizer + if finalizer != nil { + entry = &boehmFinalizer{fn: finalizer} + } + gcLock.Lock() + base := libgc_base(uintptr(objPtr)) + if base == 0 { + gcLock.Unlock() + return + } + if base != uintptr(objPtr) && !reflectlite.FinalizerAllowsInterior(value.RawType()) { + runtimeFatal("runtime.SetFinalizer: pointer not at beginning of allocated block") + } + offset := uintptr(objPtr) - base + old := libgc_register_finalizer(base, uintptr(unsafe.Pointer(entry))) + if old == ^uintptr(0) { + runtimeFatal("gc: cannot register finalizer") + } + head := (*boehmFinalizer)(unsafe.Pointer(old)) + replaced := false + var prev *boehmFinalizer + for n := head; n != nil; n = n.next { + if n.offset == offset { + if prev == nil { + head = n.next + } else { + prev.next = n.next + } + *n = boehmFinalizer{} + replaced = true + if entry == nil { + numFinalizers-- + if finalizersSinceGC != 0 { + finalizersSinceGC-- + } + } + break + } + prev = n + } + if entry != nil { + entry.offset = offset + entry.next = head + if !replaced { + numFinalizers++ + finalizersSinceGC++ + } + } else if head != nil { + if libgc_register_finalizer(base, uintptr(unsafe.Pointer(head))) == ^uintptr(0) { + runtimeFatal("gc: cannot register finalizer") + } + KeepAlive(head) + } + if entry != nil { + initFinalizerScheduler() + } + gcResumeWorld() + gcLock.Unlock() + KeepAlive(obj) +} + +//export tinygo_runtime_bdwgc_finalizer +func boehmQueueFinalizer(obj unsafe.Pointer, data unsafe.Pointer) { + for n := (*boehmFinalizer)(data); n != nil; { + next := n.next + numFinalizers-- + n.ptr = unsafe.Add(obj, n.offset) + n.next = finalizerPending + finalizerPending = n + finalizerQueued = true + n = next + } } +// Call with gcLock held. Callbacks only enqueue Go work; they never run user code. +func boehmInvokeFinalizers() bool { + if numFinalizers != 0 && libgc_should_invoke_finalizers() != 0 { + libgc_invoke_finalizers() + } + queued := finalizerQueued + finalizerQueued = false + return queued +} + +func finalizerPressureGC() bool { + gcLock.Lock() + trigger := finalizerGCTrigger(numFinalizers) + if trigger == 0 || finalizersSinceGC < trigger { + gcLock.Unlock() + return false + } + libgc_gcollect() + finalizersSinceGC = 0 + queued := boehmInvokeFinalizers() + gcResumeWorld() + gcLock.Unlock() + if queued { + wakeFinalizer() + } + return true +} + +func wakeFinalizer() { + if hasScheduler || hasParallelism { + gcLock.Lock() + spawn := finalizerPending != nil && !finalizerRunnerStarted + if spawn { + finalizerRunnerStarted = true + } + gcLock.Unlock() + if spawn { + spawnFinalizerRunner() + } + } else { + drainFinalizers() + } +} + +func drainFinalizers() { + if finalizerDraining { + return + } + finalizerDraining = true + for { + gcLock.Lock() + n := finalizerPending + if n != nil { + finalizerPending = n.next + } + gcLock.Unlock() + if n == nil { + break + } + ptr, fn := n.ptr, n.fn + *n = boehmFinalizer{} + callFinalizer(ptr, fn) + } + finalizerDraining = false +} + +func finalizerRunner() { + for { + drainFinalizers() + gcLock.Lock() + if finalizerPending == nil { + finalizerRunnerStarted = false + gcLock.Unlock() + return + } + gcLock.Unlock() + } +} + +//export tinygo_runtime_bdwgc_register_finalizer +func libgc_register_finalizer(uintptr, uintptr) uintptr + +//export GC_should_invoke_finalizers +func libgc_should_invoke_finalizers() int32 + +//export GC_invoke_finalizers +func libgc_invoke_finalizers() int32 + //export GC_init func libgc_init() diff --git a/src/runtime/gc_finalizer.go b/src/runtime/gc_finalizer.go index df3a4c6c6f..5d08a22e29 100644 --- a/src/runtime/gc_finalizer.go +++ b/src/runtime/gc_finalizer.go @@ -33,10 +33,6 @@ type finalizerEntry struct { fn interface{} } -// finalizerGCThreshold starts pressure GC when registrations indicate external memory pressure. -// Larger tables use a proportional threshold. Zero disables this trigger. -const finalizerGCThreshold = 32 - var ( finalizers *finalizerEntry // registered finalizers; a GC root that keeps fn values alive finalizerPending *finalizerEntry // finalizers whose object died, waiting to run @@ -55,20 +51,6 @@ var ( finalizerRunnerStarted bool ) -const finalizerGCDivisor = 2 - -// finalizerGCTrigger scales the threshold so scan work stays proportional to registrations. -// It uses finalizerGCThreshold as the minimum. -func finalizerGCTrigger() uintptr { - if finalizerGCThreshold == 0 { - return 0 - } - if proportional := numFinalizers / finalizerGCDivisor; proportional > finalizerGCThreshold { - return proportional - } - return finalizerGCThreshold -} - // finalizerBits records finalizer registrations by heap block for fast lookup. // Hold gcLock for every access because heap growth can replace the slice. var finalizerBits []byte @@ -328,18 +310,6 @@ func scanFinalizers() { } } -// callFinalizer invokes a finalizer func value on the given object pointer. -func callFinalizer(objPtr unsafe.Pointer, fn interface{}) { - // SetFinalizer already validated that fn is a func. A finalizer is - // contractually func(ptrType), and func(*T) and func(unsafe.Pointer) are - // ABI-identical in TinyGo (one pointer arg + trailing context, no result). - // reflect.Value.Call is unimplemented, so reinterpret the boxed closure and - // call it via the same closure-ABI indirect call the runtime uses elsewhere. - fnBox := (*_interface)(unsafe.Pointer(&fn)).value - f := *(*func(unsafe.Pointer))(fnBox) - f(objPtr) -} - // drainFinalizers runs every queued finalizer, with gcLock released so the // finalizers may allocate. func drainFinalizers() { @@ -397,7 +367,7 @@ func dequeueFinalizer() (*finalizerEntry, unsafe.Pointer) { // finalizerPressureGC runs a GC when registrations indicate external memory pressure. // It wakes the finalizer runner when the GC queues work. func finalizerPressureGC() bool { - trigger := finalizerGCTrigger() + trigger := finalizerGCTrigger(numFinalizers) if trigger == 0 || finalizersSinceGC < trigger { return false } diff --git a/src/runtime/gc_finalizer_common.go b/src/runtime/gc_finalizer_common.go new file mode 100644 index 0000000000..b78032fcf4 --- /dev/null +++ b/src/runtime/gc_finalizer_common.go @@ -0,0 +1,29 @@ +//go:build gc.conservative || gc.precise || gc.boehm + +package runtime + +import "unsafe" + +// finalizerGCThreshold starts pressure GC when registrations indicate external memory pressure. +// Larger tables use a proportional threshold. Zero disables this trigger. +const finalizerGCThreshold = 32 +const finalizerGCDivisor = 2 + +// finalizerGCTrigger scales the threshold so scan work stays proportional to registrations. +// It uses finalizerGCThreshold as the minimum. +func finalizerGCTrigger(count uintptr) uintptr { + if finalizerGCThreshold == 0 { + return 0 + } + if proportional := count / finalizerGCDivisor; proportional > finalizerGCThreshold { + return proportional + } + return finalizerGCThreshold +} + +// A func(*T) and a func(unsafe.Pointer) have the same TinyGo closure ABI. +func callFinalizer(objPtr unsafe.Pointer, fn interface{}) { + fnBox := (*_interface)(unsafe.Pointer(&fn)).value + f := *(*func(unsafe.Pointer))(fnBox) + f(objPtr) +} diff --git a/src/runtime/gc_finalizer_sched.go b/src/runtime/gc_finalizer_sched.go index 0a4783eb8c..a8e2a7867a 100644 --- a/src/runtime/gc_finalizer_sched.go +++ b/src/runtime/gc_finalizer_sched.go @@ -1,10 +1,12 @@ -//go:build (gc.conservative || gc.precise) && (scheduler.tasks || scheduler.asyncify) +//go:build (gc.conservative || gc.precise || gc.boehm) && (scheduler.tasks || scheduler.asyncify) package runtime // Keep this setup in a file for these schedulers so unused finalizer code can be removed. // Cooperative schedulers also install the idle GC hook. func spawnFinalizerRunner() { - finalizerIdleGC = finalizerPressureGC + initFinalizerScheduler() go finalizerRunner() } + +func initFinalizerScheduler() { finalizerIdleGC = finalizerPressureGC } diff --git a/src/runtime/gc_finalizer_sched_none.go b/src/runtime/gc_finalizer_sched_none.go index 55d8a9a055..cf797cb05b 100644 --- a/src/runtime/gc_finalizer_sched_none.go +++ b/src/runtime/gc_finalizer_sched_none.go @@ -1,6 +1,8 @@ -//go:build (gc.conservative || gc.precise) && scheduler.none +//go:build (gc.conservative || gc.precise || gc.boehm) && scheduler.none package runtime // scheduler.none has no goroutines; finalizers drain inline in wakeFinalizer. func spawnFinalizerRunner() {} + +func initFinalizerScheduler() {} diff --git a/src/runtime/gc_finalizer_sched_other.go b/src/runtime/gc_finalizer_sched_other.go index f874ec46b9..da08c8e07d 100644 --- a/src/runtime/gc_finalizer_sched_other.go +++ b/src/runtime/gc_finalizer_sched_other.go @@ -1,7 +1,9 @@ -//go:build (gc.conservative || gc.precise) && !scheduler.none && !scheduler.tasks && !scheduler.asyncify +//go:build (gc.conservative || gc.precise || gc.boehm) && !scheduler.none && !scheduler.tasks && !scheduler.asyncify package runtime // spawnFinalizerRunner is the fallback for noncooperative schedulers. // These schedulers run finalizers but do not install the idle GC hook. func spawnFinalizerRunner() { go finalizerRunner() } + +func initFinalizerScheduler() {} diff --git a/testdata/finalizer.go b/testdata/finalizer.go index c15277785a..7a8d57c0ef 100644 --- a/testdata/finalizer.go +++ b/testdata/finalizer.go @@ -1,26 +1,25 @@ 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" +import ( + "os" + "runtime" + "sync/atomic" + "time" +) type T struct{ x int } var ( - ranCount int - clearedRan int - f1Ran int - f2Ran int - sink int + ranCount int + clearedRan int + f1Ran int + f2Ran int + sink int + interiorDone [4]atomic.Int32 ) +var finalizerTestMode string + // 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 @@ -120,7 +119,104 @@ func testReplace() { } } +type interiorObject struct { + pad [128]byte + value int + data [8]byte +} + +type interiorValue uint16 + +//go:noinline +func registerInteriorFinalizers() { + p := &interiorObject{value: 41} + p.data[1] = 42 + runtime.SetFinalizer(p, func(*interiorObject) { panic("cleared base finalizer ran") }) + runtime.SetFinalizer(&p.value, func(*int) { panic("cleared interior finalizer ran") }) + runtime.SetFinalizer(&p.data[1], func(v *byte) { + if *v != 42 { + panic("wrong interior byte finalizer argument") + } + interiorDone[2].Add(1) + }) + runtime.SetFinalizer(&p.data[2], func(*byte) { panic("cleared byte finalizer ran") }) + runtime.SetFinalizer(&p.value, nil) + runtime.SetFinalizer(&p.data[2], nil) + runtime.SetFinalizer(&p.data[3], nil) + runtime.SetFinalizer(p, nil) + runtime.SetFinalizer(&p.value, func(v *int) { + if *v != 41 { + panic("wrong interior int finalizer argument") + } + interiorDone[1].Add(1) + }) + runtime.SetFinalizer(p, func(v *interiorObject) { + if v.value != 41 { + panic("wrong base finalizer argument") + } + interiorDone[0].Add(1) + }) + q := new(struct { + pad [128]byte + value interiorValue + }) + q.value = 43 + runtime.SetFinalizer(&q.value, func(v *interiorValue) { + if *v != 43 { + panic("wrong named interior finalizer argument") + } + interiorDone[3].Add(1) + }) +} + +func testInteriorFinalizers() { + registerInteriorFinalizers() + for i := 0; i < 200; i++ { + sink += scrubStack(40) + runtime.GC() + runtime.Gosched() + if runtime.GOARCH != "wasm" { + time.Sleep(time.Millisecond) + } + done := true + for j := range interiorDone { + done = done && interiorDone[j].Load() == 1 + } + if done { + return + } + } + panic("interior finalizers did not run exactly once") +} + func main() { + if finalizerTestMode == "interior" { + testInteriorFinalizers() + println("ok") + return + } + if len(os.Args) > 1 { + switch os.Args[1] { + case "interior": + testInteriorFinalizers() + println("ok") + case "interior-pointer": + p := new(struct { + pad [128]byte + target *T + }) + runtime.SetFinalizer(&p.target, func(**T) {}) + runtime.KeepAlive(p) + case "interior-large": + p := new(struct { + pad [128]byte + target [16]byte + }) + runtime.SetFinalizer(&p.target, func(*[16]byte) {}) + runtime.KeepAlive(p) + } + return + } testFires() testClear() testReplace() diff --git a/testdata/finalizeridle.go b/testdata/finalizeridle.go index f533ec77bf..88cc9a465d 100644 --- a/testdata/finalizeridle.go +++ b/testdata/finalizeridle.go @@ -4,9 +4,11 @@ package main // The wasm target provides deterministic finalization for these tests. import ( + "os" "runtime" "sync/atomic" "time" + _ "unsafe" ) // batch must exceed the finalizer registration threshold to trigger idle collection. @@ -24,6 +26,32 @@ var ( type blockedObject struct{ x int } +var finalizerIdleMode string + +//go:linkname finalizerMallocs runtime.mallocs +func finalizerMallocs() uint64 + +func testRegistrationDoesNotSpawn() { + const registrations = 512 + live := make([]*[2]int, registrations) + before := finalizerMallocs() + for i := range live { + p := new([2]int) + live[i] = p + runtime.SetFinalizer(p, func(*[2]int) { panic("live object finalized") }) + runtime.Gosched() + } + allocations := finalizerMallocs() - before + if allocations > registrations*3 { + println("registration allocations:", allocations) + panic("empty finalizer runners were allocated") + } + for _, p := range live { + runtime.SetFinalizer(p, nil) + } + runtime.KeepAlive(live) +} + //go:noinline func blockOperation(kind int, ready chan<- struct{}, ch chan struct{}) { ready <- struct{}{} @@ -174,6 +202,11 @@ func testFinishedGoroutineArgs() { } func main() { + if finalizerIdleMode == "registration" || len(os.Args) > 1 && os.Args[1] == "registration" { + testRegistrationDoesNotSpawn() + println("ok") + return + } testPermanentlyBlockedStacks() testIdleCollect() testFinishedGoroutineStacks() diff --git a/testdata/finalizerlarge.go b/testdata/finalizerlarge.go index 4ba8ddc04f..f5de454397 100644 --- a/testdata/finalizerlarge.go +++ b/testdata/finalizerlarge.go @@ -1,29 +1,151 @@ package main -import "runtime" +import ( + "os" + "runtime" + "sync/atomic" + "time" +) type largeFinalizerObject struct { data [128]byte } var largeFinalizerRan bool +var largeFinalizerSink int +var runnerCallbackDone, runnerPayloadDone atomic.Uint32 +var finalizerLargeMode string + +//go:noinline +func registerRunnerPayload() { + payload := new(largeFinalizerObject) + payload.data[0] = 42 + runtime.SetFinalizer(payload, func(*largeFinalizerObject) { runnerPayloadDone.Add(1) }) + target := new(largeFinalizerObject) + runtime.SetFinalizer(target, func(*largeFinalizerObject) { + if payload.data[0] != 42 { + panic("finalizer lost its callback payload") + } + runnerCallbackDone.Add(1) + }) +} + +func testRunnerReleasesPayload() { + registerRunnerPayload() + for i := 0; i < 200 && (runnerCallbackDone.Load() != 1 || runnerPayloadDone.Load() != 1); i++ { + largeFinalizerSink += scrubLargeFinalizerStack(40) + runtime.GC() + runtime.Gosched() + if runtime.GOARCH != "wasm" { + time.Sleep(time.Millisecond) + } + } + if runnerCallbackDone.Load() != 1 || runnerPayloadDone.Load() != 1 { + panic("finalizer runner retained its callback payload") + } +} + +//go:noinline +func scrubLargeFinalizerStack(depth int) int { + if depth == 0 { + return largeFinalizerSink + } + var buf [64]int + for i := range buf { + buf[i] = depth + i + } + largeFinalizerSink += buf[depth&63] + return scrubLargeFinalizerStack(depth-1) + buf[0] +} //go:noinline func registerLargeFinalizer() { p := new(largeFinalizerObject) + payload := new(largeFinalizerObject) + for i := range payload.data { + payload.data[i] = byte(i) + } runtime.SetFinalizer(p, func(*largeFinalizerObject) { + for i, value := range payload.data { + if value != byte(i) { + panic("finalizer closure data was collected") + } + } largeFinalizerRan = true }) } func main() { + mode := finalizerLargeMode + if len(os.Args) > 1 { + mode = os.Args[1] + } + if mode == "graph" { + testFinalizerGraph() + println("ok") + return + } registerLargeFinalizer() for i := 0; i < 100 && !largeFinalizerRan; i++ { + largeFinalizerSink += scrubLargeFinalizerStack(40) runtime.GC() runtime.Gosched() } if !largeFinalizerRan { panic("large object finalizer did not run") } + if mode == "runner-payload" { + testRunnerReleasesPayload() + } println("ok") } + +type graphNode struct { + next *graphNode + tag uint32 + data [128]byte +} + +var ( + graphFinalized int + cycleFinalized int + graphKeepAlive []*graphNode +) + +//go:noinline +func registerGraphFinalizer() { + child := &graphNode{tag: 0x12345678} + parent := &graphNode{next: child} + runtime.SetFinalizer(parent, func(p *graphNode) { + for i := 0; i < 8192; i++ { + graphKeepAlive = append(graphKeepAlive, &graphNode{tag: uint32(i)}) + } + if p.next.tag != 0x12345678 { + panic("finalizer lost a referenced object") + } + graphFinalized++ + }) +} + +//go:noinline +func registerCycleFinalizers() { + first := new(graphNode) + second := new(graphNode) + first.next = second + second.next = first + runtime.SetFinalizer(first, func(*graphNode) { cycleFinalized++ }) + runtime.SetFinalizer(second, func(*graphNode) { cycleFinalized++ }) +} + +func testFinalizerGraph() { + registerGraphFinalizer() + registerCycleFinalizers() + for i := 0; i < 100 && (graphFinalized != 1 || cycleFinalized != 2); i++ { + largeFinalizerSink += scrubLargeFinalizerStack(40) + runtime.GC() + runtime.Gosched() + } + if graphFinalized != 1 || cycleFinalized != 2 { + panic("finalizers did not run for the graph and cycle") + } +}