From cee22db05319fc39b0c6f4b5d2d8418e386eb3a6 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 17 Nov 2025 14:09:27 +0800 Subject: [PATCH] feat: add size level aggregation --- cmd/internal/flags/flags.go | 16 + doc/size-report.md | 58 +++ internal/build/build.go | 17 + internal/build/resolver.go | 109 ++++++ internal/build/size_report.go | 546 +++++++++++++++++++++++++++++ internal/build/size_report_test.go | 156 +++++++++ 6 files changed, 902 insertions(+) create mode 100644 doc/size-report.md create mode 100644 internal/build/resolver.go create mode 100644 internal/build/size_report.go create mode 100644 internal/build/size_report_test.go diff --git a/cmd/internal/flags/flags.go b/cmd/internal/flags/flags.go index 232edbd4..90652d70 100644 --- a/cmd/internal/flags/flags.go +++ b/cmd/internal/flags/flags.go @@ -36,6 +36,9 @@ var CheckLinkArgs bool var CheckLLFiles bool var GenLLFiles bool var ForceEspClang bool +var SizeReport bool +var SizeFormat string +var SizeLevel string func AddCommonFlags(fs *flag.FlagSet) { 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(&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) { @@ -79,6 +86,15 @@ func UpdateConfig(conf *build.Config) error { conf.Target = Target conf.Port = Port conf.BaudRate = BaudRate + if SizeReport || SizeFormat != "" || SizeLevel != "" { + conf.SizeReport = true + if SizeFormat != "" { + conf.SizeFormat = SizeFormat + } + if SizeLevel != "" { + conf.SizeLevel = SizeLevel + } + } switch conf.Mode { case build.ModeBuild: diff --git a/doc/size-report.md b/doc/size-report.md new file mode 100644 index 00000000..e213e906 --- /dev/null +++ b/doc/size-report.md @@ -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 ` 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
)`, and gaps become + `(padding
)` 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`. diff --git a/internal/build/build.go b/internal/build/build.go index 6021e9f7..730edb7e 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -132,6 +132,9 @@ type Config struct { CheckLinkArgs bool // check linkargs valid ForceEspClang bool // force to use esp-clang 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. // 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 @@ -205,6 +208,15 @@ func Do(args []string, conf *Config) ([]Package, error) { if conf.BuildMode == "" { 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 forceEspClang := conf.ForceEspClang || conf.Target != "" 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 { 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 if ctx.buildConf.BuildMode == BuildModeCArchive || ctx.buildConf.BuildMode == BuildModeCShared { diff --git a/internal/build/resolver.go b/internal/build/resolver.go new file mode 100644 index 00000000..7530b17e --- /dev/null +++ b/internal/build/resolver.go @@ -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 +} diff --git a/internal/build/size_report.go b/internal/build/size_report.go new file mode 100644 index 00000000..af6e537b --- /dev/null +++ b/internal/build/size_report.go @@ -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 +} diff --git a/internal/build/size_report_test.go b/internal/build/size_report_test.go new file mode 100644 index 00000000..0cd8f1fd --- /dev/null +++ b/internal/build/size_report_test.go @@ -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) + } +}