From 7e4bcc6f751e5780cf9618b7fed273f0903af0b7 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 4 Aug 2026 20:48:01 +0800 Subject: [PATCH] runtime: drain BDWGC finalizers during explicit GC --- runtime/internal/clite/bdwgc/bdwgc.go | 6 +++ runtime/internal/lib/runtime/runtime_gc.go | 16 ++++--- test/go/finalizer_llgo_test.go | 50 ++++++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 test/go/finalizer_llgo_test.go diff --git a/runtime/internal/clite/bdwgc/bdwgc.go b/runtime/internal/clite/bdwgc/bdwgc.go index 9f0bec38e6..e4d9613862 100644 --- a/runtime/internal/clite/bdwgc/bdwgc.go +++ b/runtime/internal/clite/bdwgc/bdwgc.go @@ -88,6 +88,12 @@ func RegisterFinalizerUnreachable( fn FinalizerFunc, cd c.Pointer, oldFn *FinalizerFunc, oldCd *c.Pointer) +// InvokeFinalizers runs all finalizers that BDWGC has queued as ready. +// It returns the number of finalizers that were run. +// +//go:linkname InvokeFinalizers C.GC_invoke_finalizers +func InvokeFinalizers() c.Int + // ----------------------------------------------------------------------------- //go:linkname Enable C.GC_enable diff --git a/runtime/internal/lib/runtime/runtime_gc.go b/runtime/internal/lib/runtime/runtime_gc.go index d8656f93a4..7decf5579b 100644 --- a/runtime/internal/lib/runtime/runtime_gc.go +++ b/runtime/internal/lib/runtime/runtime_gc.go @@ -36,19 +36,25 @@ func ReadMemStats(m *runtime.MemStats) { } func GC() { - bdwgc.Gcollect() - runFinalizers() - // BDW finalizers are observed on a subsequent collection cycle. + collectAndRunFinalizers() // Run one extra cycle so weak-pointer cleanup hooks (unique/weak) see // finalized state before we trigger map cleanup callbacks. - bdwgc.Gcollect() - runFinalizers() + collectAndRunFinalizers() unique_runtime_notifyMapCleanup() if poolCleanup != nil { poolCleanup() } } +func collectAndRunFinalizers() { + bdwgc.Gcollect() + // GC_gcollect only discovers unreachable finalizable objects. Explicitly + // drain BDWGC's ready queue so runtime.GC does not depend on a later + // allocation to invoke the callbacks that feed runFinalizers. + bdwgc.InvokeFinalizers() + runFinalizers() +} + func saturatingSub(x, y uintptr) uintptr { if x < y { return 0 diff --git a/test/go/finalizer_llgo_test.go b/test/go/finalizer_llgo_test.go new file mode 100644 index 0000000000..611076ac3a --- /dev/null +++ b/test/go/finalizer_llgo_test.go @@ -0,0 +1,50 @@ +//go:build llgo && !baremetal && !nogc + +package gotest + +import ( + "runtime" + "testing" + _ "unsafe" +) + +//go:linkname getBDWGCFinalizeOnDemand C.GC_get_finalize_on_demand +func getBDWGCFinalizeOnDemand() int32 + +//go:linkname setBDWGCFinalizeOnDemand C.GC_set_finalize_on_demand +func setBDWGCFinalizeOnDemand(enabled int32) + +func TestRuntimeGCDrainsBDWGCFinalizersOnDemand(t *testing.T) { + // BDWGC normally may invoke ready finalizers during a later allocation. + // On-demand mode makes runtime.GC's explicit drain observable without + // relying on allocation timing. This setting is process-global, so this + // test and the neighboring finalizer tests must remain sequential. + old := getBDWGCFinalizeOnDemand() + setBDWGCFinalizeOnDemand(1) + t.Cleanup(func() { + setBDWGCFinalizeOnDemand(old) + }) + + const n = 32 + finalized := make(chan int32, n) + created := make(chan struct{}) + go func() { + makeFinalizerTinyObjects(n, finalized) + close(created) + }() + <-created + + // InvokeFinalizers runs synchronously once BDWGC has queued an object. + // The retries only let the producer goroutine exit so that conservative + // stack and register roots no longer keep the objects reachable. + for range 8 { + runtime.Gosched() + runGCWithTimeout(t) + if len(finalized) > n/2 { + return + } + } + if got := len(finalized); got <= n/2 { + t.Fatalf("runtime.GC ran only %d/%d on-demand finalizers", got, n) + } +}