Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 108 additions & 36 deletions cl/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,25 +157,27 @@ 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
methodNilDerefChecks map[*ssa.UnOp]none
vargs map[*ssa.Alloc][]llssa.Expr // varargs
funcs map[*ssa.Function]llssa.Function
linkOnceFns map[*ssa.Function]none
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
methodNilDerefChecks map[*ssa.UnOp]none
vargs map[*ssa.Alloc][]llssa.Expr // varargs
funcs map[*ssa.Function]llssa.Function
linkOnceFns map[*ssa.Function]none
stackDefers map[*ssa.Function]bool
anonDefers map[*ssa.Function]bool
paramDIVars map[*types.Var]llssa.DIVar
noInlineForMemProfile bool
memProfileInstrument bool

patches Patches
blkInfos []blocks.Info
Expand Down Expand Up @@ -432,6 +434,23 @@ func hasInstantiatedRecv(recv *types.Var) bool {
return false
}

func (p *context) applyNoInline(fn llssa.Function) {
if disableInline || p.noInlineForMemProfile {
fn.Inline(llssa.NoInline)
}
if p.noInlineForMemProfile {
fn.DisableTailCalls()
}
}

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 {
Expand Down Expand Up @@ -469,10 +488,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, p.needsLinkOnce(f))
if disableInline {
fn.Inline(llssa.NoInline)
}
}
p.applyNoInline(fn)
p.funcs[f] = fn
isCgo := isCgoExternSymbol(f)
if nblk := len(f.Blocks); nblk > 0 {
Expand All @@ -499,13 +516,15 @@ 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, oldMethodNilDerefChecks := p.fn, p.goFn, p.methodNilDerefChecks
oldFn, oldGoFn, oldMethodNilDerefChecks, oldMemProfileInstrument := p.fn, p.goFn, p.methodNilDerefChecks, 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, p.methodNilDerefChecks = oldFn, oldGoFn, oldMethodNilDerefChecks
p.fn, p.goFn, p.methodNilDerefChecks, p.memProfileInstrument = oldFn, oldGoFn, oldMethodNilDerefChecks, oldMemProfileInstrument
}()
p.phis = nil
if dbgSymsEnabled {
Expand Down Expand Up @@ -627,6 +646,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")
}
Expand Down Expand Up @@ -1383,6 +1405,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
Expand Down Expand Up @@ -1683,6 +1708,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.
Expand Down Expand Up @@ -1710,18 +1781,19 @@ 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),
linkOnceFns: make(map[*ssa.Function]none),
addrOfFieldAddrs: collectAddrOfFieldSelectors(files),
prog: prog,
pkg: ret,
fset: pkgProg.Fset,
goProg: pkgProg,
goTyps: pkgTypes,
goPkg: pkg,
patches: patches,
noInlineForMemProfile: packageUsesRuntimeMemProfile(files),

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.

这个noinline的决策似乎只影响直接使用了MemProfile的包,间接的堆栈可能还是不准确?inline函数的栈和行号追踪是不是可以考虑像go一样自己建立一个表,在同一个函数内区分出来不同的部分。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@zhouguangyuan0718 是的,这里是有意的范围控制。

这个 PR 只在直接使用 runtime.MemProfile / MemProfileRate 的包里禁用 inline/tail-call 并加轻量 MemProfileEnter/Exit,目的是稳定当前 heapsampling/MemProfile 的直接归因,同时避免给所有包引入全局 instrumentation 开销。

间接包里的分配目前仍依赖 native stack fallback;如果符号或 inline 信息不足,确实不能保证像 Go 那样还原完整 inline call stack,也不能在同一个函数内区分所有 inlined call site。要做到这一点需要单独的 inline frame / line table 机制,应该放到后续 debug/lineinfo 方向处理,不放进这个 PR 扩大 scope。

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.

ok 可以理解

skips: make(map[string]none),
vargs: make(map[*ssa.Alloc][]llssa.Expr),
funcs: make(map[*ssa.Function]llssa.Function),
linkOnceFns: make(map[*ssa.Function]none),
addrOfFieldAddrs: collectAddrOfFieldSelectors(files),
loaded: map[*types.Package]*pkgInfo{
types.Unsafe: {kind: PkgDeclOnly}, // TODO(xsw): PkgNoInit or PkgDeclOnly?
},
Expand Down
4 changes: 1 addition & 3 deletions cl/instr.go
Original file line number Diff line number Diff line change
Expand Up @@ -666,9 +666,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, p.needsLinkOnce(fn))
if disableInline {
aFn.Inline(llssa.NoInline)
}
p.applyNoInline(aFn)
}
}
return
Expand Down
170 changes: 170 additions & 0 deletions cl/memprofile_instrumentation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
//go:build !llgo
// +build !llgo

/*
* 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 cl

import (
"go/ast"
"go/parser"
"go/token"
"strings"
"testing"
)

func TestPackageUsesRuntimeMemProfile(t *testing.T) {
tests := []struct {
name string
src string
want bool
}{
{
name: "no runtime import",
src: `package foo
func f() {}
`,
},
{
name: "runtime mem profile call",
src: `package foo
import "runtime"
func f() {
runtime.MemProfile(nil, false)
}
`,
want: true,
},
{
name: "runtime mem profile rate",
src: `package foo
import "runtime"
var _ = runtime.MemProfileRate
`,
want: true,
},
{
name: "renamed runtime import",
src: `package foo
import rt "runtime"
var _ = rt.MemProfileRate
`,
want: true,
},
{
name: "blank runtime import",
src: `package foo
import _ "runtime"
func f() {}
`,
},
{
name: "dot runtime import",
src: `package foo
import . "runtime"
var _ = MemProfileRate
`,
},
{
name: "other runtime selector",
src: `package foo
import "runtime"
var _ = runtime.GOOS
`,
},
{
name: "selector on non runtime value",
src: `package foo
import "runtime"
var _ = runtime.GOOS
type profiler struct{}
var p profiler
var _ = p.MemProfile
`,
},
{
name: "selector base is not identifier",
src: `package foo
import "runtime"
type profiler struct{}
var p profiler
var _ = (p).MemProfile
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "foo.go", tt.src, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
if got := packageUsesRuntimeMemProfile([]*ast.File{file}); got != tt.want {
t.Fatalf("packageUsesRuntimeMemProfile() = %v, want %v", got, tt.want)
}
})
}

badImport := &ast.File{
Imports: []*ast.ImportSpec{{
Path: &ast.BasicLit{Kind: token.STRING, Value: "runtime"},
}},
}
if packageUsesRuntimeMemProfile([]*ast.File{badImport}) {
t.Fatal("bad import literal should not enable memprofile instrumentation")
}
}

func TestMemProfileFunctionName(t *testing.T) {
tests := []struct {
name string
want string
}{
{name: "command-line-arguments.main", want: "main.main"},
{name: "command-line-arguments.profiledAlloc", want: "main.profiledAlloc"},
{name: "example.com/mod.profiledAlloc", want: "example.com/mod.profiledAlloc"},
}
for _, tt := range tests {
if got := memProfileFunctionName(tt.name); got != tt.want {
t.Fatalf("memProfileFunctionName(%q) = %q, want %q", tt.name, got, tt.want)
}
}
}

func TestCompileRuntimeMemProfileInstrumentation(t *testing.T) {
_, m := mustCompileLLPkgFromSrc(t, `package foo
import "runtime"

var _ = runtime.MemProfileRate

func sample() {
}
`)
fn := mustNamedFunction(t, m, "foo.sample")
fnIR := fn.String()
for _, want := range []string{"MemProfileEnter", "MemProfileExit"} {
if !strings.Contains(fnIR, want) {
t.Fatalf("compiled function missing %s instrumentation:\n%s", want, fnIR)
}
}
ir := m.String()
for _, want := range []string{"noinline", "disable-tail-calls"} {
if !strings.Contains(ir, want) {
t.Fatalf("compiled module missing %s attribute:\n%s", want, ir)
}
}
}
Loading
Loading