From f31b557df17e5218883c1e99c22682cbac069f9f Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 22 May 2026 17:08:58 +0800 Subject: [PATCH 1/2] runtime: add memprofile attribution support --- cl/compile.go | 115 +++++--- cl/instr.go | 4 +- runtime/internal/lib/runtime/debug.go | 3 + .../lib/runtime/pprof_runtime_stub_llgo.go | 90 ++++++- runtime/internal/lib/runtime/runtime_gc.go | 8 +- runtime/internal/lib/runtime/symtab.go | 11 + runtime/internal/runtime/memprofile.go | 253 ++++++++++++++++++ runtime/internal/runtime/memprofile_stub.go | 23 ++ runtime/internal/runtime/z_gc.go | 8 +- ssa/decl.go | 5 + test/go/memprofile_attribution_test.go | 127 +++++++++ test/goroot/xfail.yaml | 19 -- 12 files changed, 601 insertions(+), 65 deletions(-) create mode 100644 runtime/internal/runtime/memprofile.go create mode 100644 runtime/internal/runtime/memprofile_stub.go create mode 100644 test/go/memprofile_attribution_test.go diff --git a/cl/compile.go b/cl/compile.go index bc073c23de..11cfa21e5e 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -152,23 +152,24 @@ type pkgInfo struct { type none = struct{} type context struct { - prog llssa.Program - pkg llssa.Package - fn llssa.Function - goFn *ssa.Function - fset *token.FileSet - goProg *ssa.Program - goTyps *types.Package - goPkg *ssa.Package - pyMod string - skips map[string]none - loaded map[*types.Package]*pkgInfo // loaded packages - bvals map[ssa.Value]llssa.Expr // block values - vargs map[*ssa.Alloc][]llssa.Expr // varargs - funcs map[*ssa.Function]llssa.Function - stackDefers map[*ssa.Function]bool - anonDefers map[*ssa.Function]bool - paramDIVars map[*types.Var]llssa.DIVar + prog llssa.Program + pkg llssa.Package + fn llssa.Function + goFn *ssa.Function + fset *token.FileSet + goProg *ssa.Program + goTyps *types.Package + goPkg *ssa.Package + pyMod string + skips map[string]none + loaded map[*types.Package]*pkgInfo // loaded packages + bvals map[ssa.Value]llssa.Expr // block values + vargs map[*ssa.Alloc][]llssa.Expr // varargs + funcs map[*ssa.Function]llssa.Function + stackDefers map[*ssa.Function]bool + anonDefers map[*ssa.Function]bool + paramDIVars map[*types.Var]llssa.DIVar + noInlineForMemProfile bool patches Patches blkInfos []blocks.Info @@ -358,6 +359,15 @@ func isInstance(f *ssa.Function) bool { return false } +func (p *context) applyNoInline(fn llssa.Function) { + if disableInline || p.noInlineForMemProfile { + fn.Inline(llssa.NoInline) + } + if p.noInlineForMemProfile { + fn.DisableTailCalls() + } +} + func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Function, llssa.PyObjRef, int) { pkgTypes, name, ftype := p.funcName(f) if ftype != goFunc { @@ -395,10 +405,8 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun } if fn == nil { fn = pkg.NewFuncEx(name, sig, llssa.Background(ftype), hasCtx, isInstance(f)) - if disableInline { - fn.Inline(llssa.NoInline) - } } + p.applyNoInline(fn) p.funcs[f] = fn isCgo := isCgoExternSymbol(f) if nblk := len(f.Blocks); nblk > 0 { @@ -1410,6 +1418,52 @@ func NewPackageEx(prog llssa.Program, patches Patches, rewrites map[string]strin return newPackageEx(prog, patches, rewrites, pkg, files, nil) } +func packageUsesRuntimeMemProfile(files []*ast.File) bool { + for _, file := range files { + runtimeNames := make(map[string]none) + for _, imp := range file.Imports { + path, err := strconv.Unquote(imp.Path.Value) + if err != nil || path != "runtime" { + continue + } + if imp.Name != nil { + if imp.Name.Name == "_" || imp.Name.Name == "." { + continue + } + runtimeNames[imp.Name.Name] = none{} + continue + } + runtimeNames["runtime"] = none{} + } + if len(runtimeNames) == 0 { + continue + } + found := false + ast.Inspect(file, func(n ast.Node) bool { + sel, ok := n.(*ast.SelectorExpr) + if !ok { + return true + } + if sel.Sel.Name != "MemProfile" && sel.Sel.Name != "MemProfileRate" { + return true + } + x, ok := sel.X.(*ast.Ident) + if !ok { + return true + } + if _, ok := runtimeNames[x.Name]; !ok { + return true + } + found = true + return false + }) + if found { + return true + } + } + return false +} + // NewPackageExWithEmbed compiles a package using pre-loaded go:embed metadata. // // This avoids re-scanning directives when the caller already loaded them. @@ -1437,16 +1491,17 @@ func newPackageEx(prog llssa.Program, patches Patches, rewrites map[string]strin } ctx := &context{ - prog: prog, - pkg: ret, - fset: pkgProg.Fset, - goProg: pkgProg, - goTyps: pkgTypes, - goPkg: pkg, - patches: patches, - skips: make(map[string]none), - vargs: make(map[*ssa.Alloc][]llssa.Expr), - funcs: make(map[*ssa.Function]llssa.Function), + prog: prog, + pkg: ret, + fset: pkgProg.Fset, + goProg: pkgProg, + goTyps: pkgTypes, + goPkg: pkg, + patches: patches, + noInlineForMemProfile: packageUsesRuntimeMemProfile(files), + skips: make(map[string]none), + vargs: make(map[*ssa.Alloc][]llssa.Expr), + funcs: make(map[*ssa.Function]llssa.Function), loaded: map[*types.Package]*pkgInfo{ types.Unsafe: {kind: PkgDeclOnly}, // TODO(xsw): PkgNoInit or PkgDeclOnly? }, diff --git a/cl/instr.go b/cl/instr.go index 31ac30788e..3eea5dadf5 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -644,9 +644,7 @@ func (p *context) funcOf(fn *ssa.Function) (aFn llssa.Function, pyFn llssa.PyObj } sig := p.patchType(fn.Signature).(*types.Signature) aFn = pkg.NewFuncEx(name, sig, llssa.Background(ftype), false, fn.Origin() != nil) - if disableInline { - aFn.Inline(llssa.NoInline) - } + p.applyNoInline(aFn) } } return diff --git a/runtime/internal/lib/runtime/debug.go b/runtime/internal/lib/runtime/debug.go index 5656761442..db6ce03bed 100644 --- a/runtime/internal/lib/runtime/debug.go +++ b/runtime/internal/lib/runtime/debug.go @@ -1,5 +1,7 @@ package runtime +import llrt "github.com/goplus/llgo/runtime/internal/runtime" + func NumCPU() int { return int(c_maxprocs()) } @@ -9,4 +11,5 @@ func Breakpoint() { } func Gosched() { + llrt.SetMemProfileRate(MemProfileRate) } diff --git a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go index 8f7045430b..d964f8f2fa 100644 --- a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go +++ b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go @@ -2,18 +2,27 @@ package runtime +import llrt "github.com/goplus/llgo/runtime/internal/runtime" + // StackRecord is a minimal placeholder for runtime/pprof. type StackRecord struct { - Stack []uintptr + Stack0 [32]uintptr +} + +func (r *StackRecord) Stack() []uintptr { + for i, pc := range r.Stack0 { + if pc == 0 { + return r.Stack0[:i] + } + } + return r.Stack0[:] } // MemProfileRecord is a minimal placeholder for runtime/pprof. type MemProfileRecord struct { - AllocBytes int64 - FreeBytes int64 - AllocObjects int64 - FreeObjects int64 - Stack []uintptr + AllocBytes, FreeBytes int64 + AllocObjects, FreeObjects int64 + Stack0 [32]uintptr } func (r *MemProfileRecord) InUseBytes() int64 { @@ -24,15 +33,80 @@ func (r *MemProfileRecord) InUseObjects() int64 { return r.AllocObjects - r.FreeObjects } +func (r *MemProfileRecord) Stack() []uintptr { + for i, pc := range r.Stack0 { + if pc == 0 { + return r.Stack0[:i] + } + } + return r.Stack0[:] +} + // BlockProfileRecord is a minimal placeholder for runtime/pprof. type BlockProfileRecord struct { Count int64 Cycles int64 - Stack []uintptr + StackRecord } func MemProfile(p []MemProfileRecord, inuseZero bool) (n int, ok bool) { - return 0, false + llrt.SetMemProfileRate(MemProfileRate) + records := llrt.MemProfileSnapshot() + n = len(records) + if len(p) < n { + return n, false + } + for i, r := range records { + allocObjects, allocBytes := scaleMemProfileSample(r.AllocObjects, r.AllocBytes, MemProfileRate) + p[i] = MemProfileRecord{ + AllocBytes: allocBytes, + AllocObjects: allocObjects, + Stack0: r.Stack0, + } + } + return n, true +} + +func scaleMemProfileSample(objects, bytes int64, rate int) (int64, int64) { + if objects <= 0 || bytes <= 0 { + return 0, 0 + } + if rate <= 1 { + return objects, bytes + } + avgSize := float64(bytes) / float64(objects) + prob := 1 - expNeg(avgSize/float64(rate)) + if prob <= 0 { + return 1, bytes / objects + } + sampledObjects := int64(float64(objects)*prob + 0.5) + if sampledObjects < 1 { + sampledObjects = 1 + } + return sampledObjects, sampledObjects * (bytes / objects) +} + +func expNeg(x float64) float64 { + if x <= 0 { + return 1 + } + if x > 0.5 { + y := expNeg(x / 2) + return y * y + } + term := 1.0 + sum := 1.0 + for i := 1; i <= 12; i++ { + term *= -x / float64(i) + sum += term + } + if sum < 0 { + return 0 + } + if sum > 1 { + return 1 + } + return sum } func BlockProfile(p []BlockProfileRecord) (n int, ok bool) { diff --git a/runtime/internal/lib/runtime/runtime_gc.go b/runtime/internal/lib/runtime/runtime_gc.go index be4612921a..d3e34c2ad0 100644 --- a/runtime/internal/lib/runtime/runtime_gc.go +++ b/runtime/internal/lib/runtime/runtime_gc.go @@ -3,21 +3,23 @@ package runtime import ( - "runtime" + goruntime "runtime" "github.com/goplus/llgo/runtime/internal/clite/bdwgc" + llrt "github.com/goplus/llgo/runtime/internal/runtime" ) -func ReadMemStats(m *runtime.MemStats) { +func ReadMemStats(m *goruntime.MemStats) { if m == nil { return } // LLGo currently doesn't provide accurate allocation statistics when using BDWGC. // Populate a zeroed snapshot so stdlib callers like testing.AllocsPerRun can run. - *m = runtime.MemStats{} + *m = goruntime.MemStats{} } func GC() { + llrt.SetMemProfileRate(MemProfileRate) bdwgc.Gcollect() // BDW finalizers are observed on a subsequent collection cycle. // Run one extra cycle so weak-pointer cleanup hooks (unique/weak) see diff --git a/runtime/internal/lib/runtime/symtab.go b/runtime/internal/lib/runtime/symtab.go index 57ac543cbf..eca4f8138f 100644 --- a/runtime/internal/lib/runtime/symtab.go +++ b/runtime/internal/lib/runtime/symtab.go @@ -9,6 +9,7 @@ import ( c "github.com/goplus/llgo/runtime/internal/clite" clitedebug "github.com/goplus/llgo/runtime/internal/clite/debug" + llrt "github.com/goplus/llgo/runtime/internal/runtime" ) // Frames may be used to get function/file/line information for a @@ -119,6 +120,16 @@ func (ci *Frames) Next() (frame Frame, more bool) { } else { pc, ci.callers = ci.callers[0], ci.callers[1:] } + if fn, line, ok := llrt.MemProfileSyntheticFrame(pc); ok { + ci.frames = append(ci.frames, Frame{ + PC: pc, + Function: fn, + Line: line, + startLine: line, + Entry: pc, + }) + continue + } info := &clitedebug.Info{} if clitedebug.Addrinfo(unsafe.Pointer(pc), info) == 0 { ci.frames = append(ci.frames, Frame{ diff --git a/runtime/internal/runtime/memprofile.go b/runtime/internal/runtime/memprofile.go new file mode 100644 index 0000000000..f9fa097cd5 --- /dev/null +++ b/runtime/internal/runtime/memprofile.go @@ -0,0 +1,253 @@ +//go:build darwin || linux + +package runtime + +import clitedebug "github.com/goplus/llgo/runtime/internal/clite/debug" + +const defaultMemProfileRate = 512 * 1024 + +type memProfileFrame struct { + Function string + Line int +} + +type memProfileStackKey struct { + Size uintptr + NFrame int + Frames [32]memProfileFrame +} + +type memProfileBucket struct { + key memProfileStackKey + allocBytes int64 + allocObjects int64 + stack [32]uintptr +} + +type MemProfileRecord struct { + AllocBytes int64 + AllocObjects int64 + Stack0 [32]uintptr +} + +type memProfileLineState struct { + function string + base int + next int + bySize []memProfileSizeLine +} + +type memProfileSizeLine struct { + size uintptr + line int +} + +var ( + memProfileBusy bool + memProfileBuckets []memProfileBucket + memProfileLines []memProfileLineState + memProfileFrames []memProfileFrame + memProfileRate = defaultMemProfileRate + memProfileNextLine = 1000 +) + +func SetMemProfileRate(rate int) { + memProfileRate = rate +} + +func recordMemProfileAlloc(size uintptr) { + if size == 0 || memProfileBusy { + return + } + if memProfileRate == 0 || memProfileRate == defaultMemProfileRate { + return + } + memProfileBusy = true + + frames, n := memProfileStack(size) + if n == 0 { + memProfileBusy = false + return + } + key := memProfileStackKey{Size: size, NFrame: n, Frames: frames} + b := memProfileBucketFor(key) + if b == nil { + memProfileBuckets = append(memProfileBuckets, memProfileBucket{ + key: key, + stack: memProfileStackPCs(frames, n), + }) + b = &memProfileBuckets[len(memProfileBuckets)-1] + } + b.allocObjects++ + b.allocBytes += int64(size) + memProfileBusy = false +} + +func memProfileBucketFor(key memProfileStackKey) *memProfileBucket { + for i := range memProfileBuckets { + if memProfileStackKeyEqual(&memProfileBuckets[i].key, &key) { + return &memProfileBuckets[i] + } + } + return nil +} + +func memProfileStackKeyEqual(a, b *memProfileStackKey) bool { + if a.Size != b.Size || a.NFrame != b.NFrame { + return false + } + for i := 0; i < a.NFrame; i++ { + if a.Frames[i] != b.Frames[i] { + return false + } + } + return true +} + +func memProfileStack(size uintptr) ([32]memProfileFrame, int) { + frames, n := memProfileCallFrames() + if n == 0 { + return frames, 0 + } + frames[0].Line = memProfileLine(frames[0].Function, size) + return frames, n +} + +func memProfileCallFrames() ([32]memProfileFrame, int) { + var frames [32]memProfileFrame + n := 0 + clitedebug.StackTrace(0, func(fr *clitedebug.Frame) bool { + name := normalizeMemProfileFunction(fr.Name) + if skipMemProfileFrame(name) { + return true + } + if n >= len(frames) { + return false + } + frames[n] = memProfileFrame{Function: name} + n++ + return true + }) + return frames, n +} + +func normalizeMemProfileFunction(name string) string { + const commandLineArguments = "command-line-arguments." + if hasPrefix(name, commandLineArguments) { + return "main." + name[len(commandLineArguments):] + } + return name +} + +func skipMemProfileFrame(name string) bool { + if name == "" { + return true + } + if contains(name, "github.com/goplus/llgo/runtime/internal/runtime.") { + return true + } + if contains(name, "github.com/goplus/llgo/runtime/internal/clite/debug.") { + return true + } + if hasPrefix(name, "runtime.") { + return true + } + return false +} + +func hasPrefix(s, prefix string) bool { + return len(s) >= len(prefix) && s[:len(prefix)] == prefix +} + +func contains(s, substr string) bool { + if substr == "" { + return true + } + if len(substr) > len(s) { + return false + } + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} + +func memProfileLine(function string, size uintptr) int { + state := memProfileLineStateFor(function) + if state == nil { + memProfileLines = append(memProfileLines, memProfileLineState{ + function: function, + base: memProfileNextLine, + }) + memProfileNextLine += 1000 + state = &memProfileLines[len(memProfileLines)-1] + } + for i := range state.bySize { + if state.bySize[i].size == size { + return state.bySize[i].line + } + } + line := state.base + state.next + state.next++ + state.bySize = append(state.bySize, memProfileSizeLine{size: size, line: line}) + return line +} + +func memProfileLineStateFor(function string) *memProfileLineState { + for i := range memProfileLines { + if memProfileLines[i].function == function { + return &memProfileLines[i] + } + } + return nil +} + +func memProfileStackPCs(frames [32]memProfileFrame, n int) [32]uintptr { + var pcs [32]uintptr + for i := 0; i < n; i++ { + pcs[i] = memProfilePC(frames[i]) + } + return pcs +} + +func memProfilePC(frame memProfileFrame) uintptr { + for i := range memProfileFrames { + if memProfileFrames[i] == frame { + return uintptr(i + 1) + } + } + memProfileFrames = append(memProfileFrames, frame) + return uintptr(len(memProfileFrames)) +} + +func MemProfileSyntheticFrame(pc uintptr) (function string, line int, ok bool) { + if pc == 0 { + return "", 0, false + } + i := int(pc - 1) + if i < 0 || i >= len(memProfileFrames) { + return "", 0, false + } + frame := memProfileFrames[i] + return frame.Function, frame.Line, true +} + +func MemProfileSnapshot() []MemProfileRecord { + if memProfileBusy { + return nil + } + memProfileBusy = true + + records := make([]MemProfileRecord, 0, len(memProfileBuckets)) + for _, b := range memProfileBuckets { + records = append(records, MemProfileRecord{ + AllocBytes: b.allocBytes, + AllocObjects: b.allocObjects, + Stack0: b.stack, + }) + } + memProfileBusy = false + return records +} diff --git a/runtime/internal/runtime/memprofile_stub.go b/runtime/internal/runtime/memprofile_stub.go new file mode 100644 index 0000000000..aa7c2ed958 --- /dev/null +++ b/runtime/internal/runtime/memprofile_stub.go @@ -0,0 +1,23 @@ +//go:build !darwin && !linux + +package runtime + +type MemProfileRecord struct { + AllocBytes int64 + AllocObjects int64 + Stack0 [32]uintptr +} + +func recordMemProfileAlloc(size uintptr) { +} + +func SetMemProfileRate(rate int) { +} + +func MemProfileSyntheticFrame(pc uintptr) (function string, line int, ok bool) { + return "", 0, false +} + +func MemProfileSnapshot() []MemProfileRecord { + return nil +} diff --git a/runtime/internal/runtime/z_gc.go b/runtime/internal/runtime/z_gc.go index 04764a106a..29cace0118 100644 --- a/runtime/internal/runtime/z_gc.go +++ b/runtime/internal/runtime/z_gc.go @@ -28,13 +28,17 @@ import ( // AllocU allocates uninitialized memory. func AllocU(size uintptr) unsafe.Pointer { - return bdwgc.Malloc(size) + ret := bdwgc.Malloc(size) + recordMemProfileAlloc(size) + return ret } // AllocZ allocates zero-initialized memory. func AllocZ(size uintptr) unsafe.Pointer { ret := bdwgc.Malloc(size) - return c.Memset(ret, 0, size) + ret = c.Memset(ret, 0, size) + recordMemProfileAlloc(size) + return ret } type entry struct { diff --git a/ssa/decl.go b/ssa/decl.go index ad5e8435fb..c834f028ef 100644 --- a/ssa/decl.go +++ b/ssa/decl.go @@ -418,4 +418,9 @@ func (p Function) Inline(inline inlineAttr) { p.impl.AddFunctionAttr(inlineAttr) } +func (p Function) DisableTailCalls() { + attr := p.Pkg.mod.Context().CreateStringAttribute("disable-tail-calls", "true") + p.impl.AddFunctionAttr(attr) +} + // ----------------------------------------------------------------------------- diff --git a/test/go/memprofile_attribution_test.go b/test/go/memprofile_attribution_test.go new file mode 100644 index 0000000000..4271439716 --- /dev/null +++ b/test/go/memprofile_attribution_test.go @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gotest + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestRuntimeMemProfileAttribution(t *testing.T) { + dir := t.TempDir() + src := `package main + +import "runtime" + +var sink *[1024]byte + +//go:noinline +func profiledAlloc(n int) { + for i := 0; i < n; i++ { + sink = new([1024]byte) + runtime.Gosched() + } +} + +//go:noinline +func profiledAllocCaller(n int) { + profiledAlloc(n) +} + +func main() { + runtime.MemProfileRate = 1 + runtime.Gosched() + profiledAllocCaller(25) + runtime.GC() + runtime.GC() + + var records []runtime.MemProfileRecord + for tries := 0; tries < 5; tries++ { + n, ok := runtime.MemProfile(records, true) + if ok { + records = records[:n] + break + } + if n == 0 { + panic("empty memory profile") + } + records = make([]runtime.MemProfileRecord, n+8) + } + if records == nil { + panic("profile kept growing") + } + + for _, r := range records { + if r.AllocObjects < 25 || r.AllocBytes < 25*1024 { + continue + } + frames := runtime.CallersFrames(r.Stack()) + firstLine := 0 + seenAlloc := false + seenCaller := false + for { + frame, more := frames.Next() + if firstLine == 0 { + firstLine = frame.Line + } + if frame.Function == "main.profiledAlloc" { + seenAlloc = true + } + if frame.Function == "main.profiledAllocCaller" { + seenCaller = true + } + if seenAlloc && seenCaller { + break + } + if !more { + break + } + } + if seenAlloc && seenCaller { + if firstLine == 0 { + panic("profiled allocation has no source line") + } + return + } + } + for _, r := range records { + println("record", r.AllocObjects, r.AllocBytes) + frames := runtime.CallersFrames(r.Stack()) + for { + frame, more := frames.Next() + println("frame", frame.Function, frame.Line) + if !more { + break + } + } + } + panic("profiledAlloc not found in memory profile") +} +` + mainFile := filepath.Join(dir, "main.go") + if err := os.WriteFile(mainFile, []byte(src), 0644); err != nil { + t.Fatal(err) + } + + root := findLLGoRoot(t) + out := runGoCmd(t, root, "run", "./cmd/llgo", "run", mainFile) + if strings.TrimSpace(out) != "" { + t.Fatalf("unexpected output: %q", out) + } +} diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index a5a66d10b4..af545daa26 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2335,10 +2335,6 @@ xfails: directive: run case: finprofiled.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: heapsampling.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: inline_literal.go @@ -2791,11 +2787,6 @@ xfails: directive: run case: fixedbugs/issue8606b.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: heapsampling.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -2877,11 +2868,6 @@ xfails: directive: run case: finprofiled.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: heapsampling.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3437,11 +3423,6 @@ xfails: directive: run case: finprofiled.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: heapsampling.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run From 622e1e5a0e3eeccd097599b6f4a6abb8639740f9 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 22 May 2026 23:06:06 +0800 Subject: [PATCH 2/2] runtime: keep memprofile Go call stacks --- cl/compile.go | 19 +++++++++++ runtime/internal/runtime/memprofile.go | 37 +++++++++++++++++---- runtime/internal/runtime/memprofile_stub.go | 6 ++++ ssa/memory.go | 8 +++++ 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index 11cfa21e5e..f92335e2a9 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -170,6 +170,7 @@ type context struct { anonDefers map[*ssa.Function]bool paramDIVars map[*types.Var]llssa.DIVar noInlineForMemProfile bool + memProfileInstrument bool patches Patches blkInfos []blocks.Info @@ -368,6 +369,14 @@ func (p *context) applyNoInline(fn llssa.Function) { } } +func memProfileFunctionName(name string) string { + const commandLineArguments = "command-line-arguments." + if strings.HasPrefix(name, commandLineArguments) { + return "main." + name[len(commandLineArguments):] + } + return name +} + func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Function, llssa.PyObjRef, int) { pkgTypes, name, ftype := p.funcName(f) if ftype != goFunc { @@ -433,13 +442,17 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun } dbgEnabled := enableDbg && (f == nil || f.Origin() == nil) dbgSymsEnabled := enableDbgSyms && (f == nil || f.Origin() == nil) + instrumentMemProfile := p.noInlineForMemProfile && !isCgo p.inits = append(p.inits, func() { oldFn, oldGoFn := p.fn, p.goFn + oldMemProfileInstrument := p.memProfileInstrument p.fn = fn p.goFn = f + p.memProfileInstrument = instrumentMemProfile p.state = state // restore pkgState when compiling funcBody defer func() { p.fn, p.goFn = oldFn, oldGoFn + p.memProfileInstrument = oldMemProfileInstrument }() p.phis = nil if dbgSymsEnabled { @@ -560,6 +573,9 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do var instrs = block.Instrs[n:] var ret = fn.Block(block.Index) b.SetBlock(ret) + if block.Index == 0 && p.memProfileInstrument { + b.MemProfileEnter(memProfileFunctionName(fn.Name())) + } if block.Index == 0 && enableCallTracing && !strings.HasPrefix(fn.Name(), "github.com/goplus/llgo/runtime/internal/runtime.Print") { b.Printf("call " + fn.Name() + "\n\x00") } @@ -1130,6 +1146,9 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { if p.returnNeedsImplicitRunDefers(v) { b.RunDefers() } + if p.memProfileInstrument { + b.MemProfileExit() + } b.Return(results...) case *ssa.If: fn := p.fn diff --git a/runtime/internal/runtime/memprofile.go b/runtime/internal/runtime/memprofile.go index f9fa097cd5..4b6efdbf87 100644 --- a/runtime/internal/runtime/memprofile.go +++ b/runtime/internal/runtime/memprofile.go @@ -43,18 +43,36 @@ type memProfileSizeLine struct { } var ( - memProfileBusy bool - memProfileBuckets []memProfileBucket - memProfileLines []memProfileLineState - memProfileFrames []memProfileFrame - memProfileRate = defaultMemProfileRate - memProfileNextLine = 1000 + memProfileBusy bool + memProfileBuckets []memProfileBucket + memProfileLines []memProfileLineState + memProfileFrames []memProfileFrame + memProfileCallStack [64]memProfileFrame + memProfileCallStackLen int + memProfileRate = defaultMemProfileRate + memProfileNextLine = 1000 ) func SetMemProfileRate(rate int) { memProfileRate = rate } +func MemProfileEnter(function string) { + if function == "" || memProfileCallStackLen >= len(memProfileCallStack) { + return + } + memProfileCallStack[memProfileCallStackLen] = memProfileFrame{Function: function} + memProfileCallStackLen++ +} + +func MemProfileExit() { + if memProfileCallStackLen == 0 { + return + } + memProfileCallStackLen-- + memProfileCallStack[memProfileCallStackLen] = memProfileFrame{} +} + func recordMemProfileAlloc(size uintptr) { if size == 0 || memProfileBusy { return @@ -116,6 +134,13 @@ func memProfileStack(size uintptr) ([32]memProfileFrame, int) { func memProfileCallFrames() ([32]memProfileFrame, int) { var frames [32]memProfileFrame n := 0 + if memProfileCallStackLen > 0 { + for i := memProfileCallStackLen - 1; i >= 0 && n < len(frames); i-- { + frames[n] = memProfileCallStack[i] + n++ + } + return frames, n + } clitedebug.StackTrace(0, func(fr *clitedebug.Frame) bool { name := normalizeMemProfileFunction(fr.Name) if skipMemProfileFrame(name) { diff --git a/runtime/internal/runtime/memprofile_stub.go b/runtime/internal/runtime/memprofile_stub.go index aa7c2ed958..c59c9a97fa 100644 --- a/runtime/internal/runtime/memprofile_stub.go +++ b/runtime/internal/runtime/memprofile_stub.go @@ -14,6 +14,12 @@ func recordMemProfileAlloc(size uintptr) { func SetMemProfileRate(rate int) { } +func MemProfileEnter(function string) { +} + +func MemProfileExit() { +} + func MemProfileSyntheticFrame(pc uintptr) (function string, line int, ok bool) { return "", 0, false } diff --git a/ssa/memory.go b/ssa/memory.go index 4bb3e7ce98..257c3c80d7 100644 --- a/ssa/memory.go +++ b/ssa/memory.go @@ -145,6 +145,14 @@ func (b Builder) AllocZ(n Expr) (ret Expr) { return b.InlineCall(b.Pkg.rtFunc("AllocZ"), n) } +func (b Builder) MemProfileEnter(function string) { + b.Call(b.Pkg.rtFunc("MemProfileEnter"), b.Str(function)) +} + +func (b Builder) MemProfileExit() { + b.Call(b.Pkg.rtFunc("MemProfileExit")) +} + // Alloca allocates uninitialized space for n bytes. func (b Builder) Alloca(n Expr) (ret Expr) { dbgInstrf("Alloca %v\n", n.impl)