feat: add size level aggregation
This commit is contained in:
@@ -36,6 +36,9 @@ var CheckLinkArgs bool
|
|||||||
var CheckLLFiles bool
|
var CheckLLFiles bool
|
||||||
var GenLLFiles bool
|
var GenLLFiles bool
|
||||||
var ForceEspClang bool
|
var ForceEspClang bool
|
||||||
|
var SizeReport bool
|
||||||
|
var SizeFormat string
|
||||||
|
var SizeLevel string
|
||||||
|
|
||||||
func AddCommonFlags(fs *flag.FlagSet) {
|
func AddCommonFlags(fs *flag.FlagSet) {
|
||||||
fs.BoolVar(&Verbose, "v", false, "Verbose output")
|
fs.BoolVar(&Verbose, "v", false, "Verbose output")
|
||||||
@@ -51,6 +54,10 @@ func AddBuildFlags(fs *flag.FlagSet) {
|
|||||||
fs.BoolVar(&GenLLFiles, "gen-llfiles", false, "generate .ll files for pkg export")
|
fs.BoolVar(&GenLLFiles, "gen-llfiles", false, "generate .ll files for pkg export")
|
||||||
fs.BoolVar(&ForceEspClang, "force-espclang", false, "force to use esp-clang")
|
fs.BoolVar(&ForceEspClang, "force-espclang", false, "force to use esp-clang")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fs.BoolVar(&SizeReport, "size", false, "Print size report after build")
|
||||||
|
fs.StringVar(&SizeFormat, "size:format", "", "Size report format (text,json)")
|
||||||
|
fs.StringVar(&SizeLevel, "size:level", "", "Size report aggregation level (full,module,package)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddBuildModeFlags(fs *flag.FlagSet) {
|
func AddBuildModeFlags(fs *flag.FlagSet) {
|
||||||
@@ -79,6 +86,15 @@ func UpdateConfig(conf *build.Config) error {
|
|||||||
conf.Target = Target
|
conf.Target = Target
|
||||||
conf.Port = Port
|
conf.Port = Port
|
||||||
conf.BaudRate = BaudRate
|
conf.BaudRate = BaudRate
|
||||||
|
if SizeReport || SizeFormat != "" || SizeLevel != "" {
|
||||||
|
conf.SizeReport = true
|
||||||
|
if SizeFormat != "" {
|
||||||
|
conf.SizeFormat = SizeFormat
|
||||||
|
}
|
||||||
|
if SizeLevel != "" {
|
||||||
|
conf.SizeLevel = SizeLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch conf.Mode {
|
switch conf.Mode {
|
||||||
case build.ModeBuild:
|
case build.ModeBuild:
|
||||||
|
|||||||
58
doc/size-report.md
Normal file
58
doc/size-report.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Size Report Options
|
||||||
|
|
||||||
|
The `llgo build -size` flag emits a TinyGo-style table showing how much code,
|
||||||
|
rodata, data, and BSS each component contributes to the final binary. This
|
||||||
|
document captures the parsing strategy and new aggregation controls.
|
||||||
|
|
||||||
|
## Parsing Strategy
|
||||||
|
|
||||||
|
- We invoke `llvm-readelf --all <binary>` and parse the textual output with an
|
||||||
|
indentation-sensitive state machine (no JSON). Only the `Sections` and
|
||||||
|
`Symbols` blocks are inspected.
|
||||||
|
- Section metadata records the index, address, size, name, and segment. Each
|
||||||
|
section is classified into text/rodata/data/bss buckets.
|
||||||
|
- Symbols are attached to their containing sections with start addresses. By
|
||||||
|
sorting symbols and walking their ranges, we compute byte spans that can be
|
||||||
|
attributed to packages/modules.
|
||||||
|
- Sections with no symbols fall back to `(unknown <section>)`, and gaps become
|
||||||
|
`(padding <section>)` entries so totals still add up.
|
||||||
|
|
||||||
|
## Aggregation Levels
|
||||||
|
|
||||||
|
`-size:level` controls how symbol names are grouped prior to reporting:
|
||||||
|
|
||||||
|
| Level | Behavior |
|
||||||
|
|-----------|---------------------------------------------------------------------------|
|
||||||
|
| `full` | Keeps the raw owner from the symbol name (previous behavior). |
|
||||||
|
| `package` | Uses the list of packages built in `build.Do` and groups by `pkg.ID`. |
|
||||||
|
| `module`* | Default. Groups by `pkg.Module.Path` (or `pkg.ID` if the module is nil). |
|
||||||
|
|
||||||
|
Matching is performed by checking whether the demangled symbol name begins with
|
||||||
|
`pkg.ID + "."`. Symbols that do not match any package and contain `llgo` are
|
||||||
|
bucketed into `llgo-stubs`; other unmatched entries keep their original owner
|
||||||
|
names so we can inspect them later.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
llgo build -size . # module-level aggregation (default)
|
||||||
|
llgo build -size -size:level=package . # collapse by package ID
|
||||||
|
llgo build -size -size:level=full . # show raw symbol owners
|
||||||
|
llgo build -size -size:format=json . # JSON output (works with all levels)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
1. Unit tests: `go test ./internal/build -run TestParseReadelfOutput -count=1`.
|
||||||
|
2. Real binary test:
|
||||||
|
```sh
|
||||||
|
cd cl/_testgo/rewrite
|
||||||
|
../../../llgo.sh build .
|
||||||
|
LLGO_SIZE_REPORT_BIN=$(pwd)/rewrite \
|
||||||
|
go test ./internal/build -run TestParseReadelfRealBinary -count=1
|
||||||
|
```
|
||||||
|
3. Manual smoke test: `../../../llgo.sh build -size -size:level=module .` (or
|
||||||
|
`package`/`full` as desired).
|
||||||
|
|
||||||
|
The parser works across Mach-O and ELF targets as long as `llvm-readelf` is in
|
||||||
|
`PATH`.
|
||||||
@@ -132,6 +132,9 @@ type Config struct {
|
|||||||
CheckLinkArgs bool // check linkargs valid
|
CheckLinkArgs bool // check linkargs valid
|
||||||
ForceEspClang bool // force to use esp-clang
|
ForceEspClang bool // force to use esp-clang
|
||||||
Tags string
|
Tags string
|
||||||
|
SizeReport bool // print size report after successful build
|
||||||
|
SizeFormat string // size report format: text,json
|
||||||
|
SizeLevel string // size aggregation level: full,module,package
|
||||||
// GlobalRewrites specifies compile-time overrides for global string variables.
|
// GlobalRewrites specifies compile-time overrides for global string variables.
|
||||||
// Keys are fully qualified package paths (e.g. "main" or "github.com/user/pkg").
|
// Keys are fully qualified package paths (e.g. "main" or "github.com/user/pkg").
|
||||||
// Each Rewrites entry maps variable names to replacement string values. Only
|
// Each Rewrites entry maps variable names to replacement string values. Only
|
||||||
@@ -205,6 +208,15 @@ func Do(args []string, conf *Config) ([]Package, error) {
|
|||||||
if conf.BuildMode == "" {
|
if conf.BuildMode == "" {
|
||||||
conf.BuildMode = BuildModeExe
|
conf.BuildMode = BuildModeExe
|
||||||
}
|
}
|
||||||
|
if conf.SizeReport && conf.SizeFormat == "" {
|
||||||
|
conf.SizeFormat = "text"
|
||||||
|
}
|
||||||
|
if conf.SizeReport && conf.SizeLevel == "" {
|
||||||
|
conf.SizeLevel = "module"
|
||||||
|
}
|
||||||
|
if err := ensureSizeReporting(conf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
// Handle crosscompile configuration first to set correct GOOS/GOARCH
|
// Handle crosscompile configuration first to set correct GOOS/GOARCH
|
||||||
forceEspClang := conf.ForceEspClang || conf.Target != ""
|
forceEspClang := conf.ForceEspClang || conf.Target != ""
|
||||||
export, err := crosscompile.Use(conf.Goos, conf.Goarch, conf.Target, IsWasiThreadsEnabled(), forceEspClang)
|
export, err := crosscompile.Use(conf.Goos, conf.Goarch, conf.Target, IsWasiThreadsEnabled(), forceEspClang)
|
||||||
@@ -380,6 +392,11 @@ func Do(args []string, conf *Config) ([]Package, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if conf.Mode == ModeBuild && conf.SizeReport {
|
||||||
|
if err := reportBinarySize(outFmts.Out, conf.SizeFormat, conf.SizeLevel, allPkgs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate C headers for c-archive and c-shared modes before linking
|
// Generate C headers for c-archive and c-shared modes before linking
|
||||||
if ctx.buildConf.BuildMode == BuildModeCArchive || ctx.buildConf.BuildMode == BuildModeCShared {
|
if ctx.buildConf.BuildMode == BuildModeCArchive || ctx.buildConf.BuildMode == BuildModeCShared {
|
||||||
|
|||||||
109
internal/build/resolver.go
Normal file
109
internal/build/resolver.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// nameResolver maps symbol names to aggregation buckets based on the requested level.
|
||||||
|
type nameResolver struct {
|
||||||
|
level string
|
||||||
|
pkgs []Package
|
||||||
|
moduleMap map[string]string
|
||||||
|
packageMap map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNameResolver(level string, pkgs []Package) *nameResolver {
|
||||||
|
lvl := strings.ToLower(strings.TrimSpace(level))
|
||||||
|
if lvl == "" {
|
||||||
|
lvl = "module"
|
||||||
|
}
|
||||||
|
return &nameResolver{
|
||||||
|
level: lvl,
|
||||||
|
pkgs: pkgs,
|
||||||
|
moduleMap: make(map[string]string),
|
||||||
|
packageMap: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *nameResolver) resolve(sym string) string {
|
||||||
|
base := moduleNameFromSymbol(sym)
|
||||||
|
symbol := trimSymbolForMatch(sym)
|
||||||
|
switch r.level {
|
||||||
|
case "full":
|
||||||
|
return base
|
||||||
|
case "package":
|
||||||
|
if pkg := r.matchPackage(symbol); pkg != "" {
|
||||||
|
return pkg
|
||||||
|
}
|
||||||
|
case "module":
|
||||||
|
if mod := r.matchModule(symbol); mod != "" {
|
||||||
|
return mod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(symbol, "llgo") {
|
||||||
|
return "llgo-stubs"
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *nameResolver) matchPackage(symbol string) string {
|
||||||
|
if symbol == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if cached := r.packageMap[symbol]; cached != "" {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
for _, pkg := range r.pkgs {
|
||||||
|
if pkg == nil || pkg.Package == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id := pkg.PkgPath
|
||||||
|
if id == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(symbol, id+".") {
|
||||||
|
r.packageMap[symbol] = id
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *nameResolver) matchModule(symbol string) string {
|
||||||
|
if symbol == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if cached := r.moduleMap[symbol]; cached != "" {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
for _, pkg := range r.pkgs {
|
||||||
|
if pkg == nil || pkg.Package == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := pkg.PkgPath
|
||||||
|
if path == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(symbol, path+".") {
|
||||||
|
mod := path
|
||||||
|
if pkg.Module != nil && pkg.Module.Path != "" {
|
||||||
|
mod = pkg.Module.Path
|
||||||
|
}
|
||||||
|
r.moduleMap[symbol] = mod
|
||||||
|
return mod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimSymbolForMatch(sym string) string {
|
||||||
|
name := strings.TrimSpace(sym)
|
||||||
|
for len(name) > 0 && (name[0] == '_' || name[0] == '.') {
|
||||||
|
name = name[1:]
|
||||||
|
}
|
||||||
|
if idx := strings.Index(name, " "); idx >= 0 {
|
||||||
|
name = name[:idx]
|
||||||
|
}
|
||||||
|
if idx := strings.Index(name, "@"); idx >= 0 {
|
||||||
|
name = name[:idx]
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
546
internal/build/size_report.go
Normal file
546
internal/build/size_report.go
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sectionKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
sectionUnknown sectionKind = iota
|
||||||
|
sectionText
|
||||||
|
sectionROData
|
||||||
|
sectionData
|
||||||
|
sectionBSS
|
||||||
|
)
|
||||||
|
|
||||||
|
type sectionInfo struct {
|
||||||
|
Index int
|
||||||
|
Name string
|
||||||
|
Segment string
|
||||||
|
Address uint64
|
||||||
|
Size uint64
|
||||||
|
Kind sectionKind
|
||||||
|
}
|
||||||
|
|
||||||
|
type symbolInfo struct {
|
||||||
|
Name string
|
||||||
|
SectionIndex int
|
||||||
|
Address uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type readelfData struct {
|
||||||
|
sections map[int]*sectionInfo
|
||||||
|
symbols map[int][]symbolInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type moduleSize struct {
|
||||||
|
Name string
|
||||||
|
Code uint64
|
||||||
|
ROData uint64
|
||||||
|
Data uint64
|
||||||
|
BSS uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *moduleSize) Flash() uint64 {
|
||||||
|
return m.Code + m.ROData + m.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *moduleSize) RAM() uint64 {
|
||||||
|
return m.Data + m.BSS
|
||||||
|
}
|
||||||
|
|
||||||
|
type sizeReport struct {
|
||||||
|
Binary string
|
||||||
|
Modules map[string]*moduleSize
|
||||||
|
Total moduleSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *sizeReport) module(name string) *moduleSize {
|
||||||
|
if name == "" {
|
||||||
|
name = "(anonymous)"
|
||||||
|
}
|
||||||
|
if r.Modules == nil {
|
||||||
|
r.Modules = make(map[string]*moduleSize)
|
||||||
|
}
|
||||||
|
m, ok := r.Modules[name]
|
||||||
|
if !ok {
|
||||||
|
m = &moduleSize{Name: name}
|
||||||
|
r.Modules[name] = m
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *sizeReport) add(name string, kind sectionKind, size uint64) {
|
||||||
|
if size == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m := r.module(name)
|
||||||
|
switch kind {
|
||||||
|
case sectionText:
|
||||||
|
m.Code += size
|
||||||
|
r.Total.Code += size
|
||||||
|
case sectionROData:
|
||||||
|
m.ROData += size
|
||||||
|
r.Total.ROData += size
|
||||||
|
case sectionData:
|
||||||
|
m.Data += size
|
||||||
|
r.Total.Data += size
|
||||||
|
case sectionBSS:
|
||||||
|
m.BSS += size
|
||||||
|
r.Total.BSS += size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reportBinarySize(path, format, level string, pkgs []Package) error {
|
||||||
|
report, err := collectBinarySize(path, pkgs, level)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch format {
|
||||||
|
case "", "text":
|
||||||
|
printTextReport(os.Stdout, report)
|
||||||
|
case "json":
|
||||||
|
return emitJSONReport(os.Stdout, report)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown size format %q (valid: text,json)", format)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectBinarySize(path string, pkgs []Package, level string) (*sizeReport, error) {
|
||||||
|
cmd := exec.Command("llvm-readelf", "--all", path)
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("llvm-readelf stdout: %w", err)
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to execute llvm-readelf: %w", err)
|
||||||
|
}
|
||||||
|
parsed, parseErr := parseReadelfOutput(stdout)
|
||||||
|
closeErr := stdout.Close()
|
||||||
|
waitErr := cmd.Wait()
|
||||||
|
if parseErr != nil {
|
||||||
|
if waitErr != nil {
|
||||||
|
return nil, fmt.Errorf("llvm-readelf failed: %w\n%s", waitErr, stderr.String())
|
||||||
|
}
|
||||||
|
return nil, parseErr
|
||||||
|
}
|
||||||
|
if closeErr != nil {
|
||||||
|
return nil, closeErr
|
||||||
|
}
|
||||||
|
if waitErr != nil {
|
||||||
|
return nil, fmt.Errorf("llvm-readelf failed: %w\n%s", waitErr, stderr.String())
|
||||||
|
}
|
||||||
|
report := buildSizeReport(path, parsed, pkgs, level)
|
||||||
|
if report == nil || len(report.Modules) == 0 {
|
||||||
|
return nil, fmt.Errorf("size report: no allocatable sections found in %s", path)
|
||||||
|
}
|
||||||
|
return report, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseReadelfOutput(r io.Reader) (*readelfData, error) {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
scanner.Buffer(make([]byte, 0, 64*1024), 64*1024*1024)
|
||||||
|
|
||||||
|
type ctxKind int
|
||||||
|
const (
|
||||||
|
ctxRoot ctxKind = iota
|
||||||
|
ctxSections
|
||||||
|
ctxSection
|
||||||
|
ctxSymbols
|
||||||
|
ctxSymbol
|
||||||
|
)
|
||||||
|
|
||||||
|
type ctx struct {
|
||||||
|
kind ctxKind
|
||||||
|
indent int
|
||||||
|
}
|
||||||
|
|
||||||
|
stack := []ctx{{kind: ctxRoot, indent: -1}}
|
||||||
|
push := func(kind ctxKind, indent int) {
|
||||||
|
stack = append(stack, ctx{kind: kind, indent: indent})
|
||||||
|
}
|
||||||
|
pop := func(expected ctxKind, indent int) bool {
|
||||||
|
top := stack[len(stack)-1]
|
||||||
|
if top.kind != expected || top.indent != indent {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
stack = stack[:len(stack)-1]
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
current := func() ctx {
|
||||||
|
return stack[len(stack)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &readelfData{
|
||||||
|
sections: make(map[int]*sectionInfo),
|
||||||
|
symbols: make(map[int][]symbolInfo),
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentSection *sectionInfo
|
||||||
|
var currentSymbol *symbolInfo
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
raw := scanner.Text()
|
||||||
|
trimmed := strings.TrimSpace(raw)
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
indent := countLeadingSpaces(raw)
|
||||||
|
top := current()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(trimmed, "Sections [") && top.kind == ctxRoot:
|
||||||
|
push(ctxSections, indent)
|
||||||
|
continue
|
||||||
|
case strings.HasPrefix(trimmed, "Symbols [") && top.kind == ctxRoot:
|
||||||
|
push(ctxSymbols, indent)
|
||||||
|
continue
|
||||||
|
case trimmed == "Section {" && top.kind == ctxSections && indent == top.indent+2:
|
||||||
|
currentSection = §ionInfo{Index: -1}
|
||||||
|
push(ctxSection, indent)
|
||||||
|
continue
|
||||||
|
case trimmed == "Symbol {" && top.kind == ctxSymbols && indent == top.indent+2:
|
||||||
|
currentSymbol = &symbolInfo{SectionIndex: -1}
|
||||||
|
push(ctxSymbol, indent)
|
||||||
|
continue
|
||||||
|
case trimmed == "}" && pop(ctxSection, indent):
|
||||||
|
if currentSection != nil && currentSection.Index >= 0 {
|
||||||
|
currentSection.Kind = classifySection(currentSection.Name, currentSection.Segment)
|
||||||
|
data.sections[currentSection.Index] = currentSection
|
||||||
|
}
|
||||||
|
currentSection = nil
|
||||||
|
continue
|
||||||
|
case trimmed == "}" && pop(ctxSymbol, indent):
|
||||||
|
if currentSymbol != nil && currentSymbol.SectionIndex >= 0 {
|
||||||
|
data.symbols[currentSymbol.SectionIndex] = append(data.symbols[currentSymbol.SectionIndex], *currentSymbol)
|
||||||
|
}
|
||||||
|
currentSymbol = nil
|
||||||
|
continue
|
||||||
|
case trimmed == "]" && (top.kind == ctxSections || top.kind == ctxSymbols) && indent == top.indent:
|
||||||
|
stack = stack[:len(stack)-1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch top.kind {
|
||||||
|
case ctxSection:
|
||||||
|
if currentSection == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(trimmed, "Index: "):
|
||||||
|
if idx, err := strconv.Atoi(strings.TrimSpace(trimmed[len("Index: "):])); err == nil {
|
||||||
|
currentSection.Index = idx
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(trimmed, "Name: "):
|
||||||
|
currentSection.Name = parseNameField(trimmed[len("Name: "):])
|
||||||
|
case strings.HasPrefix(trimmed, "Segment: "):
|
||||||
|
currentSection.Segment = parseNameField(trimmed[len("Segment: "):])
|
||||||
|
case strings.HasPrefix(trimmed, "Address: "):
|
||||||
|
if val, err := parseUintField(trimmed[len("Address: "):]); err == nil {
|
||||||
|
currentSection.Address = val
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(trimmed, "Size: "):
|
||||||
|
if val, err := parseUintField(trimmed[len("Size: "):]); err == nil {
|
||||||
|
currentSection.Size = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ctxSymbol:
|
||||||
|
if currentSymbol == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(trimmed, "Name: "):
|
||||||
|
currentSymbol.Name = parseNameField(trimmed[len("Name: "):])
|
||||||
|
case strings.HasPrefix(trimmed, "Section: "):
|
||||||
|
name, idx := parseSectionRef(trimmed[len("Section: "):])
|
||||||
|
currentSymbol.SectionIndex = idx
|
||||||
|
if currentSymbol.Name == "" {
|
||||||
|
currentSymbol.Name = name
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(trimmed, "Value: "):
|
||||||
|
if val, err := parseUintField(trimmed[len("Value: "):]); err == nil {
|
||||||
|
currentSymbol.Address = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func countLeadingSpaces(line string) int {
|
||||||
|
count := 0
|
||||||
|
for _, ch := range line {
|
||||||
|
if ch != ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func classifySection(name, segment string) sectionKind {
|
||||||
|
ln := strings.ToLower(name)
|
||||||
|
ls := strings.ToLower(segment)
|
||||||
|
switch {
|
||||||
|
case strings.Contains(ln, "text"), strings.Contains(ln, "code"), strings.Contains(ln, "plt"):
|
||||||
|
return sectionText
|
||||||
|
case strings.Contains(ln, "rodata"), strings.Contains(ln, "const"), strings.Contains(ln, "literal"), strings.Contains(ln, "cstring"):
|
||||||
|
return sectionROData
|
||||||
|
case strings.Contains(ln, "bss"), strings.Contains(ln, "tbss"), strings.Contains(ln, "sbss"), strings.Contains(ln, "common"), strings.Contains(ln, "zerofill"):
|
||||||
|
return sectionBSS
|
||||||
|
case strings.Contains(ln, "data"), strings.Contains(ln, "got"), strings.Contains(ln, "init_array"), strings.Contains(ln, "cfstring"), strings.Contains(ln, "tdata"):
|
||||||
|
return sectionData
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case strings.Contains(ls, "__text"):
|
||||||
|
return sectionText
|
||||||
|
case strings.Contains(ls, "data_const"):
|
||||||
|
return sectionROData
|
||||||
|
case strings.Contains(ls, "__data"):
|
||||||
|
return sectionData
|
||||||
|
}
|
||||||
|
return sectionUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSizeReport(path string, data *readelfData, pkgs []Package, level string) *sizeReport {
|
||||||
|
report := &sizeReport{Binary: path, Modules: make(map[string]*moduleSize)}
|
||||||
|
if data == nil {
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
res := newNameResolver(level, pkgs)
|
||||||
|
var recognized bool
|
||||||
|
for idx, sec := range data.sections {
|
||||||
|
if sec == nil || sec.Size == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sec.Kind == sectionUnknown {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
recognized = true
|
||||||
|
end := sec.Address + sec.Size
|
||||||
|
syms := data.symbols[idx]
|
||||||
|
if len(syms) == 0 {
|
||||||
|
report.add("(unknown "+sec.Name+")", sec.Kind, sec.Size)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sort.Slice(syms, func(i, j int) bool {
|
||||||
|
if syms[i].Address == syms[j].Address {
|
||||||
|
return syms[i].Name < syms[j].Name
|
||||||
|
}
|
||||||
|
return syms[i].Address < syms[j].Address
|
||||||
|
})
|
||||||
|
cursor := sec.Address
|
||||||
|
for i := 0; i < len(syms); i++ {
|
||||||
|
sym := syms[i]
|
||||||
|
if sym.Address >= end {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr := sym.Address
|
||||||
|
if addr < sec.Address {
|
||||||
|
addr = sec.Address
|
||||||
|
}
|
||||||
|
if addr > cursor {
|
||||||
|
report.add("(padding "+sec.Name+")", sec.Kind, addr-cursor)
|
||||||
|
cursor = addr
|
||||||
|
}
|
||||||
|
next := end
|
||||||
|
for j := i + 1; j < len(syms); j++ {
|
||||||
|
if syms[j].Address > addr {
|
||||||
|
next = syms[j].Address
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if next > end {
|
||||||
|
next = end
|
||||||
|
}
|
||||||
|
if next <= addr {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mod := res.resolve(sym.Name)
|
||||||
|
report.add(mod, sec.Kind, next-addr)
|
||||||
|
cursor = next
|
||||||
|
}
|
||||||
|
if cursor < end {
|
||||||
|
report.add("(padding "+sec.Name+")", sec.Kind, end-cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !recognized {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
|
||||||
|
func emitJSONReport(w io.Writer, report *sizeReport) error {
|
||||||
|
type moduleJSON struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Code uint64 `json:"code"`
|
||||||
|
ROData uint64 `json:"rodata"`
|
||||||
|
Data uint64 `json:"data"`
|
||||||
|
BSS uint64 `json:"bss"`
|
||||||
|
Flash uint64 `json:"flash"`
|
||||||
|
RAM uint64 `json:"ram"`
|
||||||
|
}
|
||||||
|
mods := report.sortedModules()
|
||||||
|
jsonMods := make([]moduleJSON, 0, len(mods))
|
||||||
|
for _, m := range mods {
|
||||||
|
jsonMods = append(jsonMods, moduleJSON{
|
||||||
|
Name: m.Name,
|
||||||
|
Code: m.Code,
|
||||||
|
ROData: m.ROData,
|
||||||
|
Data: m.Data,
|
||||||
|
BSS: m.BSS,
|
||||||
|
Flash: m.Flash(),
|
||||||
|
RAM: m.RAM(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
payload := struct {
|
||||||
|
Binary string `json:"binary"`
|
||||||
|
Modules []moduleJSON `json:"modules"`
|
||||||
|
Total moduleJSON `json:"total"`
|
||||||
|
}{
|
||||||
|
Binary: filepath.Clean(report.Binary),
|
||||||
|
Modules: jsonMods,
|
||||||
|
Total: moduleJSON{
|
||||||
|
Name: "total",
|
||||||
|
Code: report.Total.Code,
|
||||||
|
ROData: report.Total.ROData,
|
||||||
|
Data: report.Total.Data,
|
||||||
|
BSS: report.Total.BSS,
|
||||||
|
Flash: report.Total.Flash(),
|
||||||
|
RAM: report.Total.RAM(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTextReport(w io.Writer, report *sizeReport) {
|
||||||
|
fmt.Fprintf(w, "\nSize report for %s\n", filepath.Clean(report.Binary))
|
||||||
|
fmt.Fprintln(w, " code rodata data bss | flash ram | module")
|
||||||
|
fmt.Fprintln(w, "------------------------------- | --------------- | ----------------")
|
||||||
|
for _, m := range report.sortedModules() {
|
||||||
|
fmt.Fprintf(w, "%7d %7d %7d %7d | %7d %7d | %s\n", m.Code, m.ROData, m.Data, m.BSS, m.Flash(), m.RAM(), m.Name)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w, "------------------------------- | --------------- | ----------------")
|
||||||
|
fmt.Fprintf(w, "%7d %7d %7d %7d | %7d %7d | total\n", report.Total.Code, report.Total.ROData, report.Total.Data, report.Total.BSS, report.Total.Flash(), report.Total.RAM())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *sizeReport) sortedModules() []*moduleSize {
|
||||||
|
mods := make([]*moduleSize, 0, len(r.Modules))
|
||||||
|
for _, m := range r.Modules {
|
||||||
|
mods = append(mods, m)
|
||||||
|
}
|
||||||
|
sort.Slice(mods, func(i, j int) bool {
|
||||||
|
if mods[i].Flash() == mods[j].Flash() {
|
||||||
|
return mods[i].Name < mods[j].Name
|
||||||
|
}
|
||||||
|
return mods[i].Flash() > mods[j].Flash()
|
||||||
|
})
|
||||||
|
return mods
|
||||||
|
}
|
||||||
|
|
||||||
|
func moduleNameFromSymbol(raw string) string {
|
||||||
|
name := strings.TrimSpace(raw)
|
||||||
|
name = strings.TrimPrefix(name, "_")
|
||||||
|
name = strings.TrimPrefix(name, ".")
|
||||||
|
if name == "" {
|
||||||
|
return "(anonymous)"
|
||||||
|
}
|
||||||
|
if idx := strings.Index(name, " "); idx > 0 {
|
||||||
|
name = name[:idx]
|
||||||
|
}
|
||||||
|
if idx := strings.Index(name, "@"); idx > 0 {
|
||||||
|
name = name[:idx]
|
||||||
|
}
|
||||||
|
lastDot := strings.LastIndex(name, ".")
|
||||||
|
if lastDot > 0 {
|
||||||
|
pkg := name[:lastDot]
|
||||||
|
if paren := strings.Index(pkg, "("); paren > 0 {
|
||||||
|
pkg = pkg[:paren]
|
||||||
|
}
|
||||||
|
pkg = strings.Trim(pkg, " ")
|
||||||
|
if pkg != "" {
|
||||||
|
return pkg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNameField(field string) string {
|
||||||
|
val := strings.TrimSpace(field)
|
||||||
|
if idx := strings.Index(val, "("); idx >= 0 {
|
||||||
|
val = strings.TrimSpace(val[:idx])
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSectionRef(field string) (string, int) {
|
||||||
|
name := parseNameField(field)
|
||||||
|
idx := strings.Index(field, "(")
|
||||||
|
if idx < 0 {
|
||||||
|
return name, -1
|
||||||
|
}
|
||||||
|
end := strings.Index(field[idx:], ")")
|
||||||
|
if end < 0 {
|
||||||
|
return name, -1
|
||||||
|
}
|
||||||
|
val := strings.TrimSpace(field[idx+1 : idx+end])
|
||||||
|
val = strings.TrimPrefix(val, "0x")
|
||||||
|
if val == "" {
|
||||||
|
return name, -1
|
||||||
|
}
|
||||||
|
num, err := strconv.ParseUint(val, 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
return name, -1
|
||||||
|
}
|
||||||
|
if num == 0 {
|
||||||
|
return name, -1
|
||||||
|
}
|
||||||
|
return name, int(num - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUintField(field string) (uint64, error) {
|
||||||
|
val := strings.TrimSpace(field)
|
||||||
|
if strings.HasPrefix(val, "0x") || strings.HasPrefix(val, "0X") {
|
||||||
|
return strconv.ParseUint(val[2:], 16, 64)
|
||||||
|
}
|
||||||
|
return strconv.ParseUint(val, 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureSizeReporting(conf *Config) error {
|
||||||
|
if !conf.SizeReport {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch strings.ToLower(conf.SizeLevel) {
|
||||||
|
case "", "module":
|
||||||
|
conf.SizeLevel = "module"
|
||||||
|
case "package", "full":
|
||||||
|
conf.SizeLevel = strings.ToLower(conf.SizeLevel)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid size level %q (valid: full,module,package)", conf.SizeLevel)
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("llvm-readelf"); err != nil {
|
||||||
|
return errors.New("llvm-readelf not found in PATH")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
156
internal/build/size_report_test.go
Normal file
156
internal/build/size_report_test.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
//go:build !llgo
|
||||||
|
|
||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/packages"
|
||||||
|
)
|
||||||
|
|
||||||
|
const sampleReadelf = `Sections [
|
||||||
|
Section {
|
||||||
|
Index: 0
|
||||||
|
Name: __text (5F)
|
||||||
|
Segment: __TEXT (5F)
|
||||||
|
Address: 0x1000
|
||||||
|
Size: 0x20
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
Index: 1
|
||||||
|
Name: __data
|
||||||
|
Segment: __DATA
|
||||||
|
Address: 0x2000
|
||||||
|
Size: 0x10
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
Index: 2
|
||||||
|
Name: __common
|
||||||
|
Segment: __DATA
|
||||||
|
Address: 0x3000
|
||||||
|
Size: 0x8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
Symbols [
|
||||||
|
Symbol {
|
||||||
|
Name: _main.main
|
||||||
|
Section: __text (0x1)
|
||||||
|
Value: 0x1000
|
||||||
|
}
|
||||||
|
Symbol {
|
||||||
|
Name: _runtime.init
|
||||||
|
Section: __text (0x1)
|
||||||
|
Value: 0x1010
|
||||||
|
}
|
||||||
|
Symbol {
|
||||||
|
Name: _main.dataVar
|
||||||
|
Section: __data (0x2)
|
||||||
|
Value: 0x2000
|
||||||
|
}
|
||||||
|
Symbol {
|
||||||
|
Name: _runtime.dataVar
|
||||||
|
Section: __data (0x2)
|
||||||
|
Value: 0x2008
|
||||||
|
}
|
||||||
|
Symbol {
|
||||||
|
Name: _runtime.bssVar
|
||||||
|
Section: __common (0x3)
|
||||||
|
Value: 0x3000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestParseReadelfOutput(t *testing.T) {
|
||||||
|
parsed, err := parseReadelfOutput(strings.NewReader(sampleReadelf))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseReadelfOutput: %v", err)
|
||||||
|
}
|
||||||
|
report := buildSizeReport("fake.bin", parsed, nil, "")
|
||||||
|
if report == nil {
|
||||||
|
t.Fatal("expected report")
|
||||||
|
}
|
||||||
|
modules := report.Modules
|
||||||
|
if len(modules) == 0 {
|
||||||
|
t.Fatal("expected modules in report")
|
||||||
|
}
|
||||||
|
mainMod, ok := modules["main"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected main module, got %v", modules)
|
||||||
|
}
|
||||||
|
if mainMod.Code != 0x10 {
|
||||||
|
t.Fatalf("unexpected main code size: %d", mainMod.Code)
|
||||||
|
}
|
||||||
|
if mainMod.Data != 0x8 {
|
||||||
|
t.Fatalf("unexpected main data size: %d", mainMod.Data)
|
||||||
|
}
|
||||||
|
runtimeMod := modules["runtime"]
|
||||||
|
if runtimeMod.Code != 0x10 {
|
||||||
|
t.Fatalf("unexpected runtime code size: %d", runtimeMod.Code)
|
||||||
|
}
|
||||||
|
if runtimeMod.Data != 0x8 {
|
||||||
|
t.Fatalf("unexpected runtime data size: %d", runtimeMod.Data)
|
||||||
|
}
|
||||||
|
if runtimeMod.BSS != 0x8 {
|
||||||
|
t.Fatalf("unexpected runtime bss size: %d", runtimeMod.BSS)
|
||||||
|
}
|
||||||
|
if report.Total.Flash() != 0x10+0x10+0x8+0x8 {
|
||||||
|
t.Fatalf("unexpected flash total: %d", report.Total.Flash())
|
||||||
|
}
|
||||||
|
if report.Total.RAM() != 0x8+0x8+0x8 {
|
||||||
|
t.Fatalf("unexpected ram total: %d", report.Total.RAM())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseReadelfRealBinary(t *testing.T) {
|
||||||
|
path := os.Getenv("LLGO_SIZE_REPORT_BIN")
|
||||||
|
if path == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("abs path: %v", err)
|
||||||
|
}
|
||||||
|
cmd := exec.Command("llvm-readelf", "--all", absPath)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
cmd.Stdout = &buf
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
t.Fatalf("llvm-readelf failed: %v", err)
|
||||||
|
}
|
||||||
|
parsed, err := parseReadelfOutput(bytes.NewReader(buf.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseReadelfOutput(real): %v", err)
|
||||||
|
}
|
||||||
|
if len(parsed.sections) == 0 {
|
||||||
|
t.Fatal("expected sections in real binary")
|
||||||
|
}
|
||||||
|
report := buildSizeReport(absPath, parsed, nil, "")
|
||||||
|
if len(report.Modules) == 0 {
|
||||||
|
t.Fatalf("expected modules for %s", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNameResolver(t *testing.T) {
|
||||||
|
pkgs := []Package{
|
||||||
|
&aPackage{Package: &packages.Package{PkgPath: "github.com/foo/bar", Module: &packages.Module{Path: "github.com/foo"}}},
|
||||||
|
}
|
||||||
|
symbol := "_github.com/foo/bar.Type.method"
|
||||||
|
if got := newNameResolver("package", pkgs).resolve(symbol); got != "github.com/foo/bar" {
|
||||||
|
t.Fatalf("package level want github.com/foo/bar, got %q", got)
|
||||||
|
}
|
||||||
|
if got := newNameResolver("module", pkgs).resolve(symbol); got != "github.com/foo" {
|
||||||
|
t.Fatalf("module level want github.com/foo, got %q", got)
|
||||||
|
}
|
||||||
|
full := newNameResolver("full", pkgs).resolve(symbol)
|
||||||
|
if full != "github.com/foo/bar.Type" {
|
||||||
|
t.Fatalf("full level unexpected: %q", full)
|
||||||
|
}
|
||||||
|
if got := newNameResolver("package", nil).resolve("_llgo_stub.foo"); got != "llgo-stubs" {
|
||||||
|
t.Fatalf("llgo default grouping failed: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user