Skip to content

runtime: drain BDWGC finalizers during explicit GC - #2283

Merged
xushiwei merged 1 commit into
xgo-dev:mainfrom
cpunion:codex/fix-finalizer-flake-20260804
Aug 9, 2026
Merged

runtime: drain BDWGC finalizers during explicit GC#2283
xushiwei merged 1 commit into
xgo-dev:mainfrom
cpunion:codex/fix-finalizer-flake-20260804

Conversation

@cpunion

@cpunion cpunion commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • bind BDWGC GC_invoke_finalizers
  • explicitly drain ready BDWGC callbacks after each GC_gcollect in runtime.GC
  • add an LLGo-only regression that forces finalize_on_demand, removing later-allocation timing from the assertion

Root cause

GC_gcollect discovers unreachable finalizable objects, while BDWGC documents that GC_invoke_finalizers is normally called implicitly only during some later allocations. LLGo called runFinalizers immediately after collection without first moving BDWGC ready callbacks into the Go finalizer queue. The test therefore depended on unrelated allocation and scheduling timing, producing both 12/32 and 0/32 CI failures before an unchanged rerun passed.

The regression enables BDWGC on-demand finalization temporarily. Before this fix, eight explicit runtime.GC calls deterministically execute 0/32 finalizers. With the fix, the test passes without extending a wall-clock deadline.

This is separate from conservative dead stack/register roots tracked by #2036; it does not broaden that compiler/runtime liveness scope.

Fixes #2275

Validation

  • deterministic on-demand regression fails before the runtime change with 0/32 and passes after it
  • finalizer, cancellation, and on-demand tests pass for 200 repetitions
  • complete LLGo test/go passes
  • test/std/weak GC test passes for 100 repetitions
  • LLGo memprofile tests pass
  • runtime internal/clite/bdwgc and internal/lib/runtime packages build with host Go
  • host test/go finalizer selection passes; LLGo-only regression is correctly excluded
  • git diff --check passes

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

The fix is well-targeted: explicitly draining BDWGC's ready queue via GC_invoke_finalizers makes runtime.GC() finalizer delivery deterministic instead of depending on a later allocation, and extracting collectAndRunFinalizers() cleanly removes the prior duplication. The new on-demand test is a good regression guard, and the //go:linkname signatures match the BDWGC C ABI (GC_invoke_finalizersint(void), GC_get/set_finalize_on_demand). No performance regression: the two added GC_invoke_finalizers calls are effectively O(1) when the ready queue is empty; the dominant cost (two full GC_gcollect cycles) is unchanged.

Findings below are all minor / maintainability — nothing blocking. See inline comments for concrete locations.

Additional note without a reliable single diff line:

  • Unlocked finalizerState.head/tail accessruntime/internal/lib/runtime/mfinal.go:132-138 (writer setFinalizerCallback) and mfinal.go:154-161 (reader runFinalizers) mutate/read the finalizer linked list without the mutex, by design (the comment at mfinal.go:128-129 notes callbacks fire during collection). This PR doesn't weaken that assumption — calling InvokeFinalizers() immediately before runFinalizers() on the same goroutine actually makes timing more deterministic. The whole scheme still relies on BDWGC only ever running these callbacks single-threaded (world stopped). Not a defect introduced here; flagging so the invariant is intentional. A one-line comment stating "head/tail are only touched from the collecting/GC goroutine" would prevent a future footgun.

Comment thread runtime/internal/clite/bdwgc/bdwgc.go
Comment thread test/go/finalizer_llgo_test.go
Comment thread test/go/finalizer_llgo_test.go
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

7e4bcc6f751e | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Build vs base Run vs base
Linux cprintf 18456 B +0.0% 327.068 ms +14.5% (worse) 1.337 ms +5.7% (worse)
Linux fmtprintf 1830032 B +0.0% (worse) 3.063 s +2.2% (worse) 3.747 ms +21.9% (worse)
Linux println 68008 B +0.0% 330.639 ms +13.0% (worse) 1.755 ms +9.8% (worse)
macOS cprintf 84672 B +0.0% 646.154 ms +87.3% (worse) 4.215 ms +18.3% (worse)
macOS fmtprintf 1869328 B +0.0% 4.264 s -7.1% (better) 14.008 ms -39.8% (better)
macOS println 121200 B +0.0% 694.515 ms +85.2% (worse) 8.097 ms +99.3% (worse)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 13.240 ns/op -0.9% (better)
Linux BenchmarkMergeCompilerFlags 157.800 ns/op +2.6% (worse)
Linux BenchmarkMergeLinkerFlags 97.640 ns/op +3.0% (worse)
Linux BenchmarkChannelBuffered 35.190 ns/op +4.5% (worse)
Linux BenchmarkChannelHandoff 27062 ns/op +6.1% (worse)
Linux BenchmarkDefer 47.830 ns/op -2.5% (better)
Linux BenchmarkDirectCall 1.556 ns/op -0.1% (better)
Linux BenchmarkGlobalRead 1.558 ns/op +0.1% (worse)
Linux BenchmarkGlobalWrite 2.487 ns/op +0.3% (worse)
Linux BenchmarkGoroutine 40353 ns/op +22.3% (worse)
Linux BenchmarkInterfaceCall 7.790 ns/op -3.8% (better)
Linux BenchmarkRuntimeGetG 1.868 ns/op -14.3% (better)
macOS BenchmarkLookupPCRandom 15.620 ns/op +2.2% (worse)
macOS BenchmarkMergeCompilerFlags 153.300 ns/op -10.8% (better)
macOS BenchmarkMergeLinkerFlags 100.300 ns/op -14.8% (better)
macOS BenchmarkChannelBuffered 36.730 ns/op +25.8% (worse)
macOS BenchmarkChannelHandoff 7087 ns/op -29.7% (better)
macOS BenchmarkDefer 38.270 ns/op +12.8% (worse)
macOS BenchmarkDirectCall 1.118 ns/op -12.9% (better)
macOS BenchmarkGlobalRead 1.230 ns/op -20.8% (better)
macOS BenchmarkGlobalWrite 1.087 ns/op -31.9% (better)
macOS BenchmarkGoroutine 72978 ns/op +59.8% (worse)
macOS BenchmarkInterfaceCall 6.852 ns/op +27.4% (worse)
macOS BenchmarkRuntimeGetG 2.759 ns/op +10.3% (worse)

Compared with 6670dae3884d measured in the same runner job.

@cpunion
cpunion force-pushed the codex/fix-finalizer-flake-20260804 branch from 1c68f52 to 7e4bcc6 Compare August 4, 2026 12:59
@cpunion

cpunion commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Regarding the review note about finalizerState.head/tail: I did not add the suggested “only touched from the collecting/GC goroutine” comment because that is not a guarantee BDWGC provides. Its documented default behavior may invoke ready finalizers implicitly from an allocation path when finalize_on_demand is disabled. PR #2283 does not change that pre-existing queue/synchronization model, so I am keeping this focused fix from codifying an inaccurate invariant or broadening into a separate synchronization change.

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@xushiwei
xushiwei merged commit cdd8611 into xgo-dev:main Aug 9, 2026
42 checks passed
visualfc pushed a commit to visualfc/llgo that referenced this pull request Aug 9, 2026
…e-20260804

runtime: drain BDWGC finalizers during explicit GC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test/go: TestRuntimeSetFinalizerTinyObjects is flaky

2 participants