From b18213805ac22ec2f1c46fbe7a6a06a2bcd0bf0f Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 16 Dec 2024 12:08:10 +0100 Subject: [PATCH] builder: write HTML size report This is not a big change over the existing size report with -size=full, but it is a bit more readable. More information will be added in subsequent commits. --- builder/build.go | 14 ++++++-- builder/size-report.go | 56 +++++++++++++++++++++++++++++ builder/size-report.html | 72 +++++++++++++++++++++++++++++++++++++ builder/sizes.go | 52 +++++++++++++++------------ compileopts/options.go | 2 +- compileopts/options_test.go | 2 +- main.go | 2 +- 7 files changed, 171 insertions(+), 29 deletions(-) create mode 100644 builder/size-report.go create mode 100644 builder/size-report.html diff --git a/builder/build.go b/builder/build.go index d1d2c4be6..87c342a32 100644 --- a/builder/build.go +++ b/builder/build.go @@ -931,15 +931,16 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe } // Print code size if requested. - if config.Options.PrintSizes == "short" || config.Options.PrintSizes == "full" { + if config.Options.PrintSizes != "" { sizes, err := loadProgramSize(result.Executable, result.PackagePathMap) if err != nil { return err } - if config.Options.PrintSizes == "short" { + switch config.Options.PrintSizes { + case "short": fmt.Printf(" code data bss | flash ram\n") fmt.Printf("%7d %7d %7d | %7d %7d\n", sizes.Code+sizes.ROData, sizes.Data, sizes.BSS, sizes.Flash(), sizes.RAM()) - } else { + case "full": if !config.Debug() { fmt.Println("warning: data incomplete, remove the -no-debug flag for more detail") } @@ -951,6 +952,13 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe } fmt.Printf("------------------------------- | --------------- | -------\n") fmt.Printf("%7d %7d %7d %7d | %7d %7d | total\n", sizes.Code, sizes.ROData, sizes.Data, sizes.BSS, sizes.Code+sizes.ROData+sizes.Data, sizes.Data+sizes.BSS) + case "html": + const filename = "size-report.html" + err := writeSizeReport(sizes, filename, pkgName) + if err != nil { + return err + } + fmt.Println("Wrote size report to", filename) } } diff --git a/builder/size-report.go b/builder/size-report.go new file mode 100644 index 000000000..d826f3027 --- /dev/null +++ b/builder/size-report.go @@ -0,0 +1,56 @@ +package builder + +import ( + _ "embed" + "fmt" + "html/template" + "os" +) + +//go:embed size-report.html +var sizeReportBase string + +func writeSizeReport(sizes *programSize, filename, pkgName string) error { + tmpl, err := template.New("report").Parse(sizeReportBase) + if err != nil { + return err + } + + f, err := os.Create(filename) + if err != nil { + return fmt.Errorf("could not open report file: %w", err) + } + defer f.Close() + + // Prepare data for the report. + type sizeLine struct { + Name string + Size *packageSize + } + programData := []sizeLine{} + for _, name := range sizes.sortedPackageNames() { + pkgSize := sizes.Packages[name] + programData = append(programData, sizeLine{ + Name: name, + Size: pkgSize, + }) + } + sizeTotal := map[string]uint64{ + "code": sizes.Code, + "rodata": sizes.ROData, + "data": sizes.Data, + "bss": sizes.BSS, + "flash": sizes.Flash(), + } + + // Write the report. + err = tmpl.Execute(f, map[string]any{ + "pkgName": pkgName, + "sizes": programData, + "sizeTotal": sizeTotal, + }) + if err != nil { + return fmt.Errorf("could not create report file: %w", err) + } + return nil +} diff --git a/builder/size-report.html b/builder/size-report.html new file mode 100644 index 000000000..d9c4822b9 --- /dev/null +++ b/builder/size-report.html @@ -0,0 +1,72 @@ + + + + Size Report for {{.pkgName}} + + + + + + +
+

Size Report for {{.pkgName}}

+ +

How much space is used by Go packages, C libraries, and other bits to set up the program environment.

+ + + +

The binary size consists of code, read-only data, and data. On microcontrollers, this is exactly the size of the firmware image. On other systems, there is some extra overhead: binary metadata (headers of the ELF/MachO/COFF file), debug information, exception tables, symbol names, etc. Using -no-debug strips most of those.

+ +

Program breakdown

+
+ + + + + + + + + + + + + {{range .sizes}} + + + + + + + + + {{end}} + + + + + + + + + + + +
PackageCodeRead-only dataDataBSSBinary size
{{.Name}}{{.Size.Code}}{{.Size.ROData}}{{.Size.Data}}{{.Size.BSS}} + {{.Size.Flash}} +
Total{{.sizeTotal.code}}{{.sizeTotal.rodata}}{{.sizeTotal.data}}{{.sizeTotal.bss}}{{.sizeTotal.flash}}
+
+
+ + diff --git a/builder/sizes.go b/builder/sizes.go index 3f6cc4518..7e6eefb3c 100644 --- a/builder/sizes.go +++ b/builder/sizes.go @@ -25,7 +25,7 @@ const sizesDebug = false // programSize contains size statistics per package of a compiled program. type programSize struct { - Packages map[string]packageSize + Packages map[string]*packageSize Code uint64 ROData uint64 Data uint64 @@ -56,10 +56,11 @@ func (ps *programSize) RAM() uint64 { // packageSize contains the size of a package, calculated from the linked object // file. type packageSize struct { - Code uint64 - ROData uint64 - Data uint64 - BSS uint64 + Program *programSize + Code uint64 + ROData uint64 + Data uint64 + BSS uint64 } // Flash usage in regular microcontrollers. @@ -72,6 +73,12 @@ func (ps *packageSize) RAM() uint64 { return ps.Data + ps.BSS } +// Flash usage in regular microcontrollers, as a percentage of the total flash +// usage of the program. +func (ps *packageSize) FlashPercent() float64 { + return float64(ps.Flash()) / float64(ps.Program.Flash()) * 100 +} + // A mapping of a single chunk of code or data to a file path. type addressLine struct { Address uint64 @@ -785,49 +792,48 @@ func loadProgramSize(path string, packagePathMap map[string]string) (*programSiz // Now finally determine the binary/RAM size usage per package by going // through each allocated section. - sizes := make(map[string]packageSize) + sizes := make(map[string]*packageSize) + program := &programSize{ + Packages: sizes, + } + getSize := func(path string) *packageSize { + if field, ok := sizes[path]; ok { + return field + } + field := &packageSize{Program: program} + sizes[path] = field + return field + } for _, section := range sections { switch section.Type { case memoryCode: readSection(section, addresses, func(path string, size uint64, isVariable bool) { - field := sizes[path] + field := getSize(path) if isVariable { field.ROData += size } else { field.Code += size } - sizes[path] = field }, packagePathMap) case memoryROData: readSection(section, addresses, func(path string, size uint64, isVariable bool) { - field := sizes[path] - field.ROData += size - sizes[path] = field + getSize(path).ROData += size }, packagePathMap) case memoryData: readSection(section, addresses, func(path string, size uint64, isVariable bool) { - field := sizes[path] - field.Data += size - sizes[path] = field + getSize(path).Data += size }, packagePathMap) case memoryBSS: readSection(section, addresses, func(path string, size uint64, isVariable bool) { - field := sizes[path] - field.BSS += size - sizes[path] = field + getSize(path).BSS += size }, packagePathMap) case memoryStack: // We store the C stack as a pseudo-package. - sizes["C stack"] = packageSize{ - BSS: section.Size, - } + getSize("C stack").BSS += section.Size } } // ...and summarize the results. - program := &programSize{ - Packages: sizes, - } for _, pkg := range sizes { program.Code += pkg.Code program.ROData += pkg.ROData diff --git a/compileopts/options.go b/compileopts/options.go index bc462b29b..30e0e4dbe 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -12,7 +12,7 @@ var ( validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise"} validSchedulerOptions = []string{"none", "tasks", "asyncify"} validSerialOptions = []string{"none", "uart", "usb", "rtt"} - validPrintSizeOptions = []string{"none", "short", "full"} + validPrintSizeOptions = []string{"none", "short", "full", "html"} validPanicStrategyOptions = []string{"print", "trap"} validOptOptions = []string{"none", "0", "1", "2", "s", "z"} ) diff --git a/compileopts/options_test.go b/compileopts/options_test.go index 23ffec465..ee63c4c46 100644 --- a/compileopts/options_test.go +++ b/compileopts/options_test.go @@ -11,7 +11,7 @@ func TestVerifyOptions(t *testing.T) { expectedGCError := errors.New(`invalid gc option 'incorrect': valid values are none, leaking, conservative, custom, precise`) expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify`) - expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full`) + expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full, html`) expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`) testCases := []struct { diff --git a/main.go b/main.go index 8ae5ce316..9f0cc631a 100644 --- a/main.go +++ b/main.go @@ -1509,7 +1509,7 @@ func main() { stackSize = uint64(size) return err }) - printSize := flag.String("size", "", "print sizes (none, short, full)") + 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") printCommands := flag.Bool("x", false, "Print commands")