Skip to content

runtime,cl: add Go-compatible sampled memory profiling - #2027

Open
cpunion wants to merge 8 commits into
xgo-dev:mainfrom
cpunion:codex/stage5-memprofile
Open

runtime,cl: add Go-compatible sampled memory profiling#2027
cpunion wants to merge 8 commits into
xgo-dev:mainfrom
cpunion:codex/stage5-memprofile

Conversation

@cpunion

@cpunion cpunion commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Implements Go-compatible, stack-keyed sampled heap profiles for LLGo.

Problem

LLGo's runtime.MemProfile only kept size-class counters, so heap profiles had no allocation stacks or source-line attribution. Lazy frame-metadata initialization from the first sampled AllocZ/AllocU could also re-enter allocation and corrupt reflected closure calls.

Changes

  • sample allocated bytes at randomized exponential thresholds based on runtime.MemProfileRate, keyed by physical call stack
  • preserve exact allocation-line anchors in packages that read memory profiles
  • initialize frame metadata before installing the sampling hook and avoid waiting on an initialization latch from the allocation path
  • trim allocator/runtime plumbing from exposed records and make runtime and pprof buffer sizing tolerate bucket growth
  • make the sampling state concurrency-safe
  • remove only the native heapsampling.go expectations covered by this implementation

The small hasPrefix relocation only shares an existing runtime helper across native runtime files; this PR does not change wasm behavior or CI configuration.

Validation on current main

  • focused caller/memprofile compiler tests pass; packageReadsMemProfile coverage is 95.7%
  • go test ./internal/runtime ./internal/lib/runtime passes in the runtime module
  • LLGo ./test/go/memprofile passes
  • LLGo caller acceptance for memory-profile attribution, logging, panic traceback, and introspection passes
  • Go 1.26.5 GOROOT test/heapsampling.go passes on darwin/arm64

Uses the frame-pointer/funcinfo infrastructure already merged in #2024.

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.44444% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cl/instr.go 94.11% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@cpunion
cpunion force-pushed the codex/stage5-memprofile branch 4 times, most recently from c647b14 to 2b22d07 Compare July 4, 2026 12:52
@cpunion
cpunion force-pushed the codex/stage5-memprofile branch 2 times, most recently from 4695ad3 to 9d9f6b0 Compare July 8, 2026 07:02
@cpunion
cpunion force-pushed the codex/stage5-memprofile branch 3 times, most recently from e6bbe61 to 2e36dbf Compare July 9, 2026 05:52
@cpunion cpunion added go-test-compat Go standard-library and GOROOT test compatibility go1.26 Go 1.26 compatibility and support feature labels Jul 16, 2026
@cpunion
cpunion force-pushed the codex/stage5-memprofile branch from 2e36dbf to ef62b2d Compare July 18, 2026 23:37
@xushiwei xushiwei removed the feature label Jul 19, 2026
@cpunion cpunion changed the title runtime,cl: stack-keyed sampled memory profiling with gc semantics runtime,cl: add Go-compatible sampled memory profiling Jul 20, 2026
@cpunion
cpunion force-pushed the codex/stage5-memprofile branch from 3eff4be to a00bc0c Compare July 29, 2026 01:50
@cpunion
cpunion force-pushed the codex/stage5-memprofile branch from 7b22159 to 0604edf Compare August 1, 2026 15:06
@cpunion

cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased the independent memprofile root onto current main (551626a1d) and force-pushed the branch.

Conflict handling kept current-main panic/recover snapshot hooks in unwind_llgo.go, then layered the memprofile initialization/capture hooks beside them. The final xfail change removes only native heapsampling.go entries; there are no CI timing/config changes and no wasm behavior work.

Fresh validation:

  • compiler detection/instrumentation tests pass; packageReadsMemProfile coverage 95.7%
  • runtime modules build
  • LLGo memprofile and caller/panic/introspection acceptance tests pass
  • upstream heapsampling.go passes without xfail with Go 1.24.11, 1.25.0, and 1.26.5 on darwin/arm64

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

cc755a44f7d0 | workflow run | long-term charts

Program measurements

Platform Workload File size vs main Build vs main Run vs main
Linux cprintf 18736 B +1.0% (worse) 288.083 ms +1.0% (worse) 1.269 ms -6.8% (better)
Linux fmtprintf 1955112 B +4.2% (worse) 2.996 s +2.6% (worse) 3.438 ms -3.2% (better)
Linux println 68144 B +0.1% (worse) 289.544 ms +0.5% (worse) 1.602 ms -3.5% (better)
macOS cprintf 84672 B +0.0% 358.292 ms -3.0% (better) 2.728 ms -10.3% (better)
macOS fmtprintf 1922208 B +1.8% (worse) 2.232 s -20.5% (better) 11.153 ms -19.5% (better)
macOS println 122416 B +1.0% (worse) 336.726 ms -8.5% (better) 3.438 ms -0.3% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs main
Linux BenchmarkLookupPCRandom 13.360 ns/op +9.1% (worse)
Linux BenchmarkMergeCompilerFlags 150.400 ns/op +4.2% (worse)
Linux BenchmarkMergeLinkerFlags 94.180 ns/op +0.3% (worse)
Linux BenchmarkChannelBuffered 34.300 ns/op -6.1% (better)
Linux BenchmarkChannelHandoff 26706 ns/op +9.9% (worse)
Linux BenchmarkDefer 47.660 ns/op +0.9% (worse)
Linux BenchmarkDirectCall 1.556 ns/op -11.4% (better)
Linux BenchmarkGlobalRead 1.557 ns/op -11.4% (better)
Linux BenchmarkGlobalWrite 2.480 ns/op -11.7% (better)
Linux BenchmarkGoroutine 30622 ns/op -13.3% (better)
Linux BenchmarkInterfaceCall 8.100 ns/op -13.9% (better)
Linux BenchmarkRuntimeGetG 1.868 ns/op -11.5% (better)
macOS BenchmarkLookupPCRandom 10.790 ns/op -10.2% (better)
macOS BenchmarkMergeCompilerFlags 95.680 ns/op -36.8% (better)
macOS BenchmarkMergeLinkerFlags 62.410 ns/op -7.8% (better)
macOS BenchmarkChannelBuffered 20.490 ns/op -6.2% (better)
macOS BenchmarkChannelHandoff 6552 ns/op +7.6% (worse)
macOS BenchmarkDefer 38.430 ns/op +49.7% (worse)
macOS BenchmarkDirectCall 0.945 ns/op +0.1% (worse)
macOS BenchmarkGlobalRead 1.003 ns/op +6.5% (worse)
macOS BenchmarkGlobalWrite 0.943 ns/op -0.2% (better)
macOS BenchmarkGoroutine 27950 ns/op +1.7% (worse)
macOS BenchmarkInterfaceCall 4.083 ns/op -0.1% (better)
macOS BenchmarkRuntimeGetG 1.976 ns/op +4.4% (worse)

Compared only with the latest matching platform in the main series.

@cpunion
cpunion force-pushed the codex/stage5-memprofile branch 3 times, most recently from 857d2a8 to 1b77778 Compare August 3, 2026 22:54
cpunion and others added 8 commits August 10, 2026 08:30
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.
@cpunion
cpunion force-pushed the codex/stage5-memprofile branch from 1b77778 to cc755a4 Compare August 10, 2026 02:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

go1.26 Go 1.26 compatibility and support go-test-compat Go standard-library and GOROOT test compatibility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants