feat: add size level aggregation

This commit is contained in:
Li Jie
2025-11-17 14:09:27 +08:00
parent b25ae1b4e7
commit cee22db053
6 changed files with 902 additions and 0 deletions

View File

@@ -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
View 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`.

View File

@@ -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
View 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
}

View 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 = &sectionInfo{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
}

View 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)
}
}