transform: add -print-allocs-cover and restore -print-allocs reason output

PR #5220 changed -print-allocs output to the go coverage tool format, which
replaced the original human-readable explanation of why each object had to be
heap allocated. That explanation is useful on its own, so this restores it as
the default behavior of -print-allocs and moves the coverage format behind a
-print-allocs-cover flag.

Signed-off-by: Piotr Bocheński <piotr@bochen.ski>
This commit is contained in:
Piotr Bocheński
2026-06-11 21:33:05 +02:00
committed by Ron Evans
parent c33113ab5f
commit 4465360f3d
9 changed files with 130 additions and 91 deletions
+1
View File
@@ -47,6 +47,7 @@ type Options struct {
Nobounds bool
PrintSizes string
PrintAllocs *regexp.Regexp // regexp string
PrintAllocsCover bool // emit allocs in go coverage tool format
PrintStacks bool
Tags []string
GlobalValues map[string]map[string]string // map[pkgpath]map[varname]value
+10 -2
View File
@@ -1778,6 +1778,7 @@ func main() {
printSize := flag.String("size", "", "print sizes (none, short, full, html)")
printStacks := flag.Bool("print-stacks", false, "print stack sizes of goroutines")
printAllocsString := flag.String("print-allocs", "", "regular expression of functions for which heap allocations should be printed")
printAllocsCoverString := flag.String("print-allocs-cover", "", "like -print-allocs, but in go coverage tool format")
printCommands := flag.Bool("x", false, "Print commands")
flagJSON := flag.Bool("json", false, "print output in JSON format")
parallelism := flag.Int("p", runtime.GOMAXPROCS(0), "the number of build jobs that can run in parallel")
@@ -1858,8 +1859,14 @@ func main() {
}
var printAllocs *regexp.Regexp
if *printAllocsString != "" {
printAllocs, err = regexp.Compile(*printAllocsString)
printAllocsCover := false
printAllocsPattern := *printAllocsString
if *printAllocsCoverString != "" {
printAllocsPattern = *printAllocsCoverString
printAllocsCover = true
}
if printAllocsPattern != "" {
printAllocs, err = regexp.Compile(printAllocsPattern)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
@@ -1907,6 +1914,7 @@ func main() {
PrintSizes: *printSize,
PrintStacks: *printStacks,
PrintAllocs: printAllocs,
PrintAllocsCover: printAllocsCover,
Tags: []string(tags),
TestConfig: testConfig,
GlobalValues: globalVarValues,
+20 -25
View File
@@ -46,10 +46,6 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u
complex128Type := ctx.StructType([]llvm.Type{ctx.DoubleType(), ctx.DoubleType()}, false)
maxAlign := int64(targetData.ABITypeAlignment(complex128Type))
if printAllocs != nil {
fmt.Fprintln(os.Stderr, "mode: set")
}
// Find all allocator calls.
var heapallocs []llvm.Value
for _, allocator := range allocators {
@@ -61,7 +57,7 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u
if heapalloc.Operand(0).IsAConstantInt().IsNil() {
// Do not allocate variable length arrays on the stack.
if logAllocs {
logAlloc(logger, heapalloc, "size is not constant")
logger(getPosition(heapalloc), "size is not constant")
}
continue
}
@@ -70,7 +66,8 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u
if size > maxStackAlloc {
// The maximum size for a stack allocation.
if logAllocs {
logAlloc(logger, heapalloc, fmt.Sprintf("object size %d exceeds maximum stack allocation size %d", size, maxStackAlloc))
logger(getPosition(heapalloc),
fmt.Sprintf("object size %d exceeds maximum stack allocation size %d", size, maxStackAlloc))
}
continue
}
@@ -107,7 +104,7 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u
if atPos.Line != 0 {
msg = fmt.Sprintf("escapes at line %d", atPos.Line)
}
logAlloc(logger, heapalloc, msg)
logger(getPosition(heapalloc), msg)
}
continue
}
@@ -146,6 +143,22 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u
}
}
// FormatAllocReason renders the heap allocation in a human-readable format.
func FormatAllocReason(pos token.Position, reason string) string {
return fmt.Sprintf("%s: object allocated on the heap: %s", pos.String(), reason)
}
// FormatAllocCover renders the heap allocation in the go coverage tool format.
func FormatAllocCover(pos token.Position) string {
if pos.Filename == "" || pos.Line <= 0 {
return "" // no position info; a blank line would corrupt the profile
}
// Highlight the whole line: column 1 to one past the last byte (the end
// column is exclusive, so add 1 to the line length).
endCol := max(lineLengthAt(pos.Filename, pos.Line), 1) + 1
return fmt.Sprintf("%s:%d.1,%d.%d 1 0", pos.Filename, pos.Line, pos.Line, endCol)
}
// valueEscapesAt returns the instruction where the given value may escape and a
// nil llvm.Value if it definitely doesn't. The value must be an instruction.
func valueEscapesAt(value llvm.Value) llvm.Value {
@@ -189,24 +202,6 @@ func valueEscapesAt(value llvm.Value) llvm.Value {
return llvm.Value{}
}
// logAlloc prints a message to stderr explaining why the given object had to be
// allocated on the heap.
func logAlloc(logger func(token.Position, string), allocCall llvm.Value, reason string) {
pos := getPosition(allocCall)
if pos.Filename == "" || pos.Line <= 0 {
logger(pos, "")
return
}
endCol := lineLengthAt(pos.Filename, pos.Line)
if endCol < 1 {
endCol = 1
}
// Only emit the coverprofile line, without position prefix.
logger(token.Position{}, fmt.Sprintf("%s:%d.1,%d.%d 1 0", pos.Filename, pos.Line, pos.Line, endCol))
}
func lineLengthAt(filename string, lineNumber int) int {
f, err := os.Open(filename)
if err != nil {
+34 -47
View File
@@ -2,11 +2,8 @@ package transform_test
import (
"go/token"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"testing"
@@ -21,21 +18,16 @@ func TestAllocs(t *testing.T) {
})
}
type allocsTestOutput struct {
filename string
line int
msg string
}
func (out allocsTestOutput) String() string {
return out.filename + ":" + strconv.Itoa(out.line) + ": " + out.msg
}
// Test with a Go file as input (for more accurate tests).
func TestAllocs2(t *testing.T) {
t.Parallel()
mod := compileGoFileForTesting(t, "./testdata/allocs2.go")
const (
basePath = "testdata/allocs2"
goFile = basePath + ".go"
goldenFile = basePath + ".out"
)
mod := compileGoFileForTesting(t, goFile)
// Run functionattrs pass, which is necessary for escape analysis.
po := llvm.NewPassBuilderOptions()
@@ -46,39 +38,34 @@ func TestAllocs2(t *testing.T) {
}
// Run heap to stack transform.
var testOutputs []allocsTestOutput
transform.OptimizeAllocs(mod, regexp.MustCompile("."), 256, func(pos token.Position, msg string) {
testOutputs = append(testOutputs, allocsTestOutput{
filename: filepath.Base(pos.Filename),
line: pos.Line,
msg: msg,
type report struct {
pos token.Position
reason string
}
var reports []report
transform.OptimizeAllocs(mod, regexp.MustCompile("."), 256, func(pos token.Position, reason string) {
pos.Filename = goFile
reports = append(reports, report{pos, reason})
})
sort.Slice(reports, func(i, j int) bool { return reports[i].pos.Line < reports[j].pos.Line })
// Render every report in each format and diff against its golden file.
for _, format := range []struct {
name string
render func(report) string
}{
{"reason", func(r report) string { return transform.FormatAllocReason(r.pos, r.reason) }},
{"cover", func(r report) string { return transform.FormatAllocCover(r.pos) }},
} {
t.Run(format.name, func(t *testing.T) {
var got strings.Builder
for _, r := range reports {
if line := format.render(r); line != "" {
got.WriteString(line)
got.WriteByte('\n')
}
}
checkGolden(t, goldenFile+"."+format.name, got.String())
})
})
sort.Slice(testOutputs, func(i, j int) bool {
return testOutputs[i].line < testOutputs[j].line
})
testOutput := make([]string, 0)
for _, out := range testOutputs {
testOutput = append(testOutput, out.String())
}
// Load expected test output (the OUT: lines).
testInput, err := os.ReadFile("./testdata/allocs2.go")
if err != nil {
t.Fatal("could not read test input:", err)
}
var expectedTestOutput []string
for _, line := range strings.Split(strings.ReplaceAll(string(testInput), "\r\n", "\n"), "\n") {
if idx := strings.Index(line, " // OUT: "); idx > 0 {
msg := line[idx+len(" // OUT: "):]
expectedTestOutput = append(expectedTestOutput, msg)
}
}
for i := range testOutput {
if !strings.HasSuffix(testOutput[i], expectedTestOutput[i]) {
t.Errorf("output does not match expected output:\n%s\n%s\n", testOutput[i], expectedTestOutput[i])
return
}
}
}
+17 -7
View File
@@ -86,13 +86,23 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error {
}
// Run TinyGo-specific interprocedural optimizations.
OptimizeAllocs(mod, config.Options.PrintAllocs, maxStackSize, func(pos token.Position, msg string) {
if pos.Filename != "" {
fmt.Fprintf(os.Stderr, "%s:%d:%d: %s\n", pos.Filename, pos.Line, pos.Column, msg)
} else {
fmt.Fprintln(os.Stderr, msg) // No prefix!
}
})
if config.Options.PrintAllocs != nil && config.Options.PrintAllocsCover {
// The go coverage tool expects this header before any blocks.
fmt.Fprintln(os.Stderr, "mode: set")
}
OptimizeAllocs(mod, config.Options.PrintAllocs, maxStackSize,
func(pos token.Position, reason string) {
var line string
if config.Options.PrintAllocsCover {
line = FormatAllocCover(pos)
} else {
line = FormatAllocReason(pos, reason)
}
if line != "" {
fmt.Fprintln(os.Stderr, line)
}
},
)
OptimizeStringToBytes(mod)
OptimizeStringEqual(mod)
+10 -10
View File
@@ -10,7 +10,7 @@ func main() {
derefInt(&n1)
// This should eventually be modified to not escape.
n2 := 6 // OUT: allocs2.go:52.1,52.42 1 0
n2 := 6
returnIntPtr(&n2)
s1 := make([]int, 3)
@@ -20,22 +20,22 @@ func main() {
readIntSlice(s2[:])
// This should also be modified to not escape.
s3 := make([]int, 3) // OUT: allocs2.go:51.1,51.42 1 0
s3 := make([]int, 3)
returnIntSlice(s3)
useSlice(make([]int, getUnknownNumber())) // OUT: allocs2.go:48.1,48.55 1 0
useSlice(make([]int, getUnknownNumber()))
s4 := make([]byte, 300) // OUT: allocs2.go:46.1,46.56 1 0
s4 := make([]byte, 300)
readByteSlice(s4)
s5 := make([]int, 4) // OUT: allocs2.go:38.1,38.56 1 0
s5 := make([]int, 4)
_ = append(s5, 5)
s6 := make([]int, 3)
s7 := []int{1, 2, 3}
copySlice(s6, s7)
c1 := getComplex128() // OUT: allocs2.go:31.1,31.55 1 0
c1 := getComplex128()
useInterface(c1)
n3 := 5
@@ -43,13 +43,13 @@ func main() {
return n3
}()
callVariadic(3, 5, 8) // OUT: allocs2.go:28.1,28.58 1 0
callVariadic(3, 5, 8)
s8 := []int{3, 5, 8} // OUT: allocs2.go:26.1,26.76 1 0
s8 := []int{3, 5, 8}
callVariadic(s8...)
n4 := 3 // OUT: allocs2.go:23.1,23.55 1 0
n5 := 7 // OUT: allocs2.go:13.1,13.42 1 0
n4 := 3
n5 := 7
func() {
n4 = n5
}()
+10
View File
@@ -0,0 +1,10 @@
testdata/allocs2.go:13.1,13.9 1 0
testdata/allocs2.go:23.1,23.22 1 0
testdata/allocs2.go:26.1,26.43 1 0
testdata/allocs2.go:28.1,28.25 1 0
testdata/allocs2.go:31.1,31.22 1 0
testdata/allocs2.go:38.1,38.23 1 0
testdata/allocs2.go:46.1,46.23 1 0
testdata/allocs2.go:48.1,48.22 1 0
testdata/allocs2.go:51.1,51.9 1 0
testdata/allocs2.go:52.1,52.9 1 0
+10
View File
@@ -0,0 +1,10 @@
testdata/allocs2.go:13:2: object allocated on the heap: escapes at line 14
testdata/allocs2.go:23:12: object allocated on the heap: escapes at line 24
testdata/allocs2.go:26:15: object allocated on the heap: size is not constant
testdata/allocs2.go:28:12: object allocated on the heap: object size 300 exceeds maximum stack allocation size 256
testdata/allocs2.go:31:12: object allocated on the heap: escapes at line 32
testdata/allocs2.go:38:21: object allocated on the heap: escapes at line 39
testdata/allocs2.go:46:22: object allocated on the heap: escapes at line 46
testdata/allocs2.go:48:13: object allocated on the heap: escapes at line 49
testdata/allocs2.go:51:2: object allocated on the heap: escapes at line 53
testdata/allocs2.go:52:2: object allocated on the heap: escapes at line 53
+18
View File
@@ -24,6 +24,24 @@ var defaultTestConfig = &compileopts.Config{
Options: &compileopts.Options{Opt: "2"},
}
// checkGolden compares got against the golden file at path, or rewrites that
// file when the test is run with -update.
func checkGolden(t *testing.T, path, got string) {
if *update {
if err := os.WriteFile(path, []byte(got), 0o666); err != nil {
t.Fatal(err)
}
return
}
want, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if got != string(want) {
t.Errorf("%s does not match expected output:\n%s", path, got)
}
}
// testTransform runs a transformation pass on an input file (pathPrefix+".ll")
// and checks whether it matches the expected output (pathPrefix+".out.ll"). The
// output is compared with a fuzzy match that ignores some irrelevant lines such