runtime,cl: add Go-compatible sampled memory profiling - #2027
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
c647b14 to
2b22d07
Compare
4695ad3 to
9d9f6b0
Compare
e6bbe61 to
2e36dbf
Compare
2e36dbf to
ef62b2d
Compare
3eff4be to
a00bc0c
Compare
7b22159 to
0604edf
Compare
|
Rebased the independent memprofile root onto current Conflict handling kept current-main panic/recover snapshot hooks in Fresh validation:
|
LLGo baseline benchmarks
Program measurements
Core language and compiler benchmarks
Compared with |
857d2a8 to
1b77778
Compare
fefa1e0 to
2983928
Compare
Replaces the size-class counters with gc-shaped heap profiling: sampled allocations are attributed to physical call stacks at exact statement lines, and records hold RAW sampled counts — consumers (pprof, goroot heapsampling.go) apply the Poisson correction themselves, exactly as with gc. - Sampling mirrors gc's mcache.nextSample: bytes count down to an exponentially distributed threshold (mean MemProfileRate), sample once on crossing, redraw. The memoryless distribution is load-bearing: with any bounded-support threshold a near-periodic allocation pattern phase-locks the sample points onto the large sites (observed 1.6x per-site skew on heapsampling's interleaved sizes). ln() is a small local approximation — the runtime core cannot import math. - Stacks come from the FP walk at sample time (fpCallers via a hook the public runtime registers), bucketed by stack hash; allocator plumbing (including __llgo_stub. wrapper frames of the hook) is trimmed at read time. A reentrancy flag spans the whole decision path: threshold drawing and bucket allocation themselves allocate, and a recursive sample overflows the stack. - Heap allocations get statement anchors in tracked functions, and a package that reads the memory profile (runtime.MemProfile / MemProfileRate under either the "runtime" or the patched lib-runtime spelling) pins all its trackable functions: per-site attribution loses sites to inlining otherwise. Profiling packages are rare and accuracy beats inlining there; gc gets both via its inline tree (P4). goroot heapsampling.go passes on darwin/arm64 and linux/arm64 (the latter was a pre-existing platform gap). Depends on --icf=none from the line-directive PR: heapsampling's three identical wrapper functions must keep distinct pcs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An acceptance regression asserts exact per-line attribution at rate=1 (raw counts are exact there), and a cl unit test covers the memprofile-package pinning criterion under both runtime spellings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… capture path The frame-table init allocates; when one of those allocations crossed the sampling threshold, captureMemProfileStack -> fpCallers re-entered initRuntimeFuncPCFramesSlow on the thread that already held the Busy latch and usleep-spun forever. First testing.callerName call of a test binary triggers the init, so whole test binaries hung at startup — which packages hit it depends on the deterministic threshold sequence meeting the binary's pre-init allocation volume: net/rpc and net/rpc/jsonrpc under go1.24 stdlib, net/http/expvar/cookiejar under go1.26 (CI shard timeouts on ubuntu, both attempts). Entering an Uninit latch from the capture path is safe (the whole sample runs under memProfileInSample, so init's own allocations cannot re-sample); only Busy must not be waited on. Drop that one sample.
2983928 to
26ddb05
Compare
|
Benchmark publishing is now pinned to Validation used real paired LLGo artifacts. The v1.0.5 renderer reports the measured base SHA and signed unit/percentage deltas (for example, The live bot comment on this PR is still produced by the publisher currently on |
Closes #2351.
Problem
LLGo's
runtime.MemProfileonly kept size-class counters, so native heap profiles lacked allocation stacks and source-line attribution. The initial stack-sampling implementation also put avoidable TLS, locking, buffer-allocation, and frame-retention costs on profiled programs.Design
(physical stack, allocation size), matching Go's Poisson-correction model.runtime.MemProfilereuses the caller's compatible record buffer, andruntime/pprofkeeps sampling paused through its buffer allocation and snapshot materialization.runtime.MemProfile,runtime.MemProfileRate, orruntime/pprofomits both allocator recording calls and native profile setup at code generation. Frame-table initialization, hook installation, capture, and recorder bodies are then unreachable and dead-stripped; allocator names, signatures, and ABI do not change.c-archiveandc-sharedconservatively retain profiling because future external calls are not visible at executable analysis time.c-archive/c-sharedBenefits
On an Apple M4 Max (Darwin/arm64, Go 1.26.5), a GC-disabled allocation microbenchmark used 21 rotated rounds; each process ran 5 warm-up and 61 measured batches of 250,000 escaping 16-byte allocations. The table reports medians of process medians; signed differences subtract those displayed medians, while percentages are paired within each round.
0f480253): unconditional size-class recorderThe standard Go toolchain has no no-consumer specialization, so rate 0 is its closest disabled path. Against Go's corresponding rate-0/rate-0/default paths, LLGo's omitted/rate-0/default medians differ by +5.35/+6.30/+7.19 ns/op and the paired changes are +39.9%/+48.7%/+53.0%. This gap includes the allocator and runtime, not just profiling; the profiling-specific comparisons are the within-toolchain deltas above. LLGo used
GC_disable; Go useddebug.SetGCPercent(-1).A final same-runner CI comparison (run, full report) built both revisions first, then alternated base/head order for every workload and sample. Allocation rows below are differences between 18-process medians;
InterfaceCalluses the median of seven pairwise signed differences and percentage changes.InterfaceCallvs PR baseThe large Darwin
InterfaceCallimprovement was consistent in all seven pairs and comes from shrinking the coldIfacePtrDatabody after the old inlined recorder path was removed. Other unrelated microbenchmark pair ranges crossed zero; scheduler-heavy goroutine measurements remained noisy.Final no-consumer binaries remove the profiler code. Minimal Linux
cprintfis nevertheless +256 B on disk because 16 additional pre-DCE funcinfo symbol-index records (16 B each) remain in.rodata;.relro_paddingshrinks by the same 256 B, so.text, loaded-section total, and page count are unchanged. Filtering dead funcinfo records is follow-up work.Validation
./test/go/memprofile: concurrent snapshots, reflect closures, tiny allocations, same-stack mixed sizes, and pprof outputtest/heapsampling.gocl,ssa, andinternal/buildtests, including cache separation, library selection, cross-package attribution, and one native TLS resolutionruntime.MemProfileconsumerc-archiveandc-sharedretentionBare-metal target coverage is left to CI. Allocator-specific tuning, sampled-object free accounting, native-equivalent stack capture for wasm/bare-metal, and dead funcinfo filtering remain follow-up work.