ssa,cl,build: release per-compile memory in in-process drivers#2048
Conversation
The golden test harness (cltest, llgen) compiles ~85 packages inside one process, and each compile's memory lived until process exit, so cl.test RSS climbed monotonically from 140MB to over 2GB. Two retainers: - The runtime-caller tracking memoization (runtimeCallerBaseCache / runtimeCallerExtendedCache) is keyed by *ssa.Package with *ssa.Function values, pinning every compiled package's go/types and go/ssa graphs forever. A heap profile at the end of the Testrt+ Testdata suites showed 1.1GB of retained front-end data (216MB alone in go/types recordTypeAndValue). Clear both maps when build.Do returns. - No LLVM Context/TargetMachine/TargetData was ever disposed, so each compile's C++-side memory also accumulated. Add Program.Dispose and call it when build.Do returns (except ModeGen, whose callers read LPkg.String() afterwards and dispose via llgen.GenFrom). Testrt+Testdata trajectory on darwin/arm64 (5s RSS samples, cold llgo cache): before 140MB -> 2051MB peak; after 127MB -> 537MB peak (-74%). Single-shot llgo CLI behavior is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
The cabi tests drive build.Do in ModeGen (whose Program the caller must dispose) in arch x mode x file loops — hundreds of compiles per process — and also parse reference .ll files into contexts that were never freed. cabi.test peaked at 2.3GB in the CI-suite co-residency profile. Dispose the program (and the parsed-IR module/context) after each check: peak RSS drops 1356MB -> 229MB on darwin/arm64, runtime unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Suite-level re-measurement done (16GB-cgroup sandbox, linux/arm64, Peak RSS per test process, before → after this PR:
Peak anonymous memory of the whole suite cgroup (the metric that actually triggers OOM/jetsam) is 2.9GB with this PR. The Relevance to the runner deaths: on 7GB macOS runners, post-#2024 co-residency (cl.test + cabi.test together >4GB anon, plus toolchain processes) sat right at the jetsam threshold; with this PR the same co-residency is ~1GB. Runtime is unchanged (cabi 74s warm-cache after vs 108s before; disposal cost is negligible). Remaining sandbox test failures are the known environment noise (root container makes chmod-000 files readable; linux/arm64 golden gaps) — identical failure set before and after. |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up to the per-Do cache clear: rather than global sync.Maps that build.Do must remember to clear, the caller-tracking memoization is now an exported cl.CallerTracking created once per compilation and passed to NewPackageExWithEmbed for every package of that compilation — the same driver-owned, compilation-scoped flow as cl.Patches (build.Do's context holds both side by side). cl keeps no package-level data state, so nothing can outlive a compile and pin its go/types and go/ssa graphs; ClearRuntimeCallerCaches is gone. NewPackage and NewPackageEx keep their signatures and compile as one-shot compilations (fresh memoization per call); passing nil to NewPackageExWithEmbed does the same. Memory behavior is identical to the cleared-globals version: Testrt+Testdata trajectory 129MB -> 562MB peak with a plateau (was 140MB -> 2051MB before the fix), cl/ssa/build/ cabi suites all pass. Discussed in xgo-dev#2049 (now closed: implemented here). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Problem
The golden test harness (cltest, llgen) compiles ~85 packages inside one process, and each compile's memory lived until process exit:
cl.testRSS climbed monotonically from 140MB to over 2GB during the Testrt+Testdata suites. At suite level (sandboxed 16GB cgroup,go test -coverprofile ./...), peak memory was 9.9GB post-#2024 (5.0GB before it) — this co-residency pressure is what killed 7GB macOS runners (jetsam SIGKILL) and starved ubuntu runner heartbeats (exit 143), see #2045.Two retainers, found via heap profile at the end of the suites (1.1GB retained):
runtimeCallerBaseCache/runtimeCallerExtendedCacheare globalsync.Maps keyed by*ssa.Packagewithmap[*ssa.Function]boolvalues — both keys and values pin the whole go/types + go/ssa graph of every package ever compiled in the process. The profile showed 216MB ingo/types.(*Checker).recordTypeAndValuealone, plus x/tools/go/ssa and go/parser data.llvm.Context/TargetMachine/TargetData(C++-side memory, invisible to Go heap profiles) accumulated for the life of the process.Fix
Program.Dispose()(context + target machine + target data, guarded, idempotent).internal/build.Dodefers it for all modes exceptModeGen(whose callers readLPkg.String()after Do returns —llgen.GenFromdisposes there instead); the cabi tests (ModeGen loops over arch×mode×file) dispose per iteration.sync.Maps with an exportedcl.CallerTracking, created once per compilation bybuild.Do's context and passed toNewPackageExWithEmbedfor every package of that compilation — the same driver-owned, compilation-scoped flow ascl.Patches. cl keeps no package-level data state, so nothing can outlive a compile and pin its front-end graphs.NewPackage/NewPackageExkeep their signatures and compile as one-shot compilations;ct == nildoes the same. (Design discussed in proposal: scope compile-level memoization to the compilation instead of package globals #2049, closed as implemented here; concurrent in-processDoremains gated only by the pre-existing cl config-flag globals, parked in the Proposal: runtime funcinfo metadata and fast FuncForPC lookup #2004 ledger.)llgoCLI behavior is unchanged (memory was reclaimed at process exit anyway).Evidence
Per-process trajectory (darwin/arm64, cold llgo cache, RSS sampled every 5s, Testrt+Testdata in one
cl.testprocess):The trajectory now plateaus instead of climbing monotonically.
Suite level (16GB-cgroup sandbox, linux/arm64,
go test -coverprofile -covermode=atomic ./...— same protocol as the 9.9GB post-#2024 number in the #2045 investigation):Peak anonymous memory of the whole suite cgroup (the metric that triggers OOM/jetsam) is 2.9GB with this PR — below pre-#2024 levels. The
memory.current~10GB readings are page cache from llgo-cache/object/coverage writes (reclaimable, not kill-relevant), so the earlier 5.0GB/9.9GB cgroup peaks over-stated the resident footprint; the real regression was the per-process accumulation fixed here. On 7GB macOS runners, post-#2024 co-residency sat at the jetsam threshold; with this PR the same co-residency is ~1GB.Validation
go test ./cl ./ssa ./internal/build ./internal/cabiall pass locally on the final version.Program.Disposenil-safety/idempotence;NewPackageExWithEmbedwith a shared non-nilCallerTracking(public API); existing caller_frame tests exercise the shared-instance cross-package memoization and the nil one-shot path.🤖 Generated with Claude Code