diff options
author | Ayke van Laethem <[email protected]> | 2024-12-17 11:30:22 +0100 |
---|---|---|
committer | Ron Evans <[email protected]> | 2024-12-19 15:08:37 +0100 |
commit | 9d2f52805b3e4d7e98c84ff3eb4f8b4fb2849369 (patch) | |
tree | 6ad0c4bc5b85a0be5a1ce89fe4ec4fb490ff2148 | |
parent | b18213805ac22ec2f1c46fbe7a6a06a2bcd0bf0f (diff) | |
download | tinygo-9d2f52805b3e4d7e98c84ff3eb4f8b4fb2849369.tar.gz tinygo-9d2f52805b3e4d7e98c84ff3eb4f8b4fb2849369.zip |
builder: show files in size report table
Show which files cause a binary size increase. This makes it easier to
see where the size is going: for example, this makes it easy to see how
much the GC contributes to code size compared to other runtime parts.
-rw-r--r-- | builder/size-report.html | 41 | ||||
-rw-r--r-- | builder/sizes.go | 109 |
2 files changed, 112 insertions, 38 deletions
diff --git a/builder/size-report.html b/builder/size-report.html index d9c4822b9..2afb5c43d 100644 --- a/builder/size-report.html +++ b/builder/size-report.html @@ -11,6 +11,12 @@ border-left: calc(var(--bs-border-width) * 2) solid currentcolor; } +/* Hover on only the rows that are clickable. */ +.row-package:hover > * { + --bs-table-color-state: var(--bs-table-hover-color); + --bs-table-bg-state: var(--bs-table-hover-bg); +} + </style> </head> <body> @@ -29,6 +35,9 @@ <p>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 <code>-no-debug</code> strips most of those.</p> <h2>Program breakdown</h2> + + <p>You can click on the rows below to see which files contribute to the binary size.</p> + <div class="table-responsive"> <table class="table w-auto"> <thead> @@ -42,8 +51,8 @@ </tr> </thead> <tbody class="table-group-divider"> - {{range .sizes}} - <tr> + {{range $i, $pkg := .sizes}} + <tr class="row-package" data-collapse=".collapse-row-{{$i}}"> <td>{{.Name}}</td> <td class="table-vertical-border">{{.Size.Code}}</td> <td>{{.Size.ROData}}</td> @@ -53,6 +62,24 @@ {{.Size.Flash}} </td> </tr> + {{range $filename, $sizes := .Size.Sub}} + <tr class="table-secondary collapse collapse-row-{{$i}}"> + <td class="ps-4"> + {{if eq $filename ""}} + (unknown file) + {{else}} + {{$filename}} + {{end}} + </td> + <td class="table-vertical-border">{{$sizes.Code}}</td> + <td>{{$sizes.ROData}}</td> + <td>{{$sizes.Data}}</td> + <td>{{$sizes.BSS}}</td> + <td class="table-vertical-border" style="background: linear-gradient(to right, var(--bs-info-bg-subtle) {{$sizes.FlashPercent}}%, var(--bs-table-bg) {{$sizes.FlashPercent}}%)"> + {{$sizes.Flash}} + </td> + </tr> + {{end}} {{end}} </tbody> <tfoot class="table-group-divider"> @@ -68,5 +95,15 @@ </table> </div> </div> + <script> +// Make table rows toggleable to show filenames. +for (let clickable of document.querySelectorAll('.row-package')) { + clickable.addEventListener('click', e => { + for (let row of document.querySelectorAll(clickable.dataset.collapse)) { + row.classList.toggle('show'); + } + }); +} + </script> </body> </html> diff --git a/builder/sizes.go b/builder/sizes.go index 7e6eefb3c..485a652d9 100644 --- a/builder/sizes.go +++ b/builder/sizes.go @@ -53,6 +53,20 @@ func (ps *programSize) RAM() uint64 { return ps.Data + ps.BSS } +// Return the package size information for a given package path, creating it if +// it doesn't exist yet. +func (ps *programSize) getPackage(path string) *packageSize { + if field, ok := ps.Packages[path]; ok { + return field + } + field := &packageSize{ + Program: ps, + Sub: map[string]*packageSize{}, + } + ps.Packages[path] = field + return field +} + // packageSize contains the size of a package, calculated from the linked object // file. type packageSize struct { @@ -61,6 +75,7 @@ type packageSize struct { ROData uint64 Data uint64 BSS uint64 + Sub map[string]*packageSize } // Flash usage in regular microcontrollers. @@ -79,6 +94,25 @@ func (ps *packageSize) FlashPercent() float64 { return float64(ps.Flash()) / float64(ps.Program.Flash()) * 100 } +// Add a single size data point to this package. +// This must only be called while calculating package size, not afterwards. +func (ps *packageSize) addSize(getField func(*packageSize, bool) *uint64, filename string, size uint64, isVariable bool) { + if size == 0 { + return + } + + // Add size for the package. + *getField(ps, isVariable) += size + + // Add size for file inside package. + sub, ok := ps.Sub[filename] + if !ok { + sub = &packageSize{Program: ps.Program} + ps.Sub[filename] = sub + } + *getField(sub, isVariable) += size +} + // A mapping of a single chunk of code or data to a file path. type addressLine struct { Address uint64 @@ -796,40 +830,32 @@ func loadProgramSize(path string, packagePathMap map[string]string) (*programSiz 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 := getSize(path) + readSection(section, addresses, program, func(ps *packageSize, isVariable bool) *uint64 { if isVariable { - field.ROData += size - } else { - field.Code += size + return &ps.ROData } + return &ps.Code }, packagePathMap) case memoryROData: - readSection(section, addresses, func(path string, size uint64, isVariable bool) { - getSize(path).ROData += size + readSection(section, addresses, program, func(ps *packageSize, isVariable bool) *uint64 { + return &ps.ROData }, packagePathMap) case memoryData: - readSection(section, addresses, func(path string, size uint64, isVariable bool) { - getSize(path).Data += size + readSection(section, addresses, program, func(ps *packageSize, isVariable bool) *uint64 { + return &ps.Data }, packagePathMap) case memoryBSS: - readSection(section, addresses, func(path string, size uint64, isVariable bool) { - getSize(path).BSS += size + readSection(section, addresses, program, func(ps *packageSize, isVariable bool) *uint64 { + return &ps.BSS }, packagePathMap) case memoryStack: // We store the C stack as a pseudo-package. - getSize("C stack").BSS += section.Size + program.getPackage("C stack").addSize(func(ps *packageSize, isVariable bool) *uint64 { + return &ps.BSS + }, "", section.Size, false) } } @@ -844,8 +870,8 @@ func loadProgramSize(path string, packagePathMap map[string]string) (*programSiz } // readSection determines for each byte in this section to which package it -// belongs. It reports this usage through the addSize callback. -func readSection(section memorySection, addresses []addressLine, addSize func(string, uint64, bool), packagePathMap map[string]string) { +// belongs. +func readSection(section memorySection, addresses []addressLine, program *programSize, getField func(*packageSize, bool) *uint64, packagePathMap map[string]string) { // The addr variable tracks at which address we are while going through this // section. We start at the beginning. addr := section.Address @@ -867,9 +893,9 @@ func readSection(section memorySection, addresses []addressLine, addSize func(st addrAligned := (addr + line.Align - 1) &^ (line.Align - 1) if line.Align > 1 && addrAligned >= line.Address { // It is, assume that's what causes the gap. - addSize("(padding)", line.Address-addr, true) + program.getPackage("(padding)").addSize(getField, "", line.Address-addr, true) } else { - addSize("(unknown)", line.Address-addr, false) + program.getPackage("(unknown)").addSize(getField, "", line.Address-addr, false) if sizesDebug { fmt.Printf("%08x..%08x %5d: unknown (gap), alignment=%d\n", addr, line.Address, line.Address-addr, line.Align) } @@ -891,7 +917,8 @@ func readSection(section memorySection, addresses []addressLine, addSize func(st length = line.Length - (addr - line.Address) } // Finally, mark this chunk of memory as used by the given package. - addSize(findPackagePath(line.File, packagePathMap), length, line.IsVariable) + packagePath, filename := findPackagePath(line.File, packagePathMap) + program.getPackage(packagePath).addSize(getField, filename, length, line.IsVariable) addr = line.Address + line.Length } if addr < sectionEnd { @@ -900,9 +927,9 @@ func readSection(section memorySection, addresses []addressLine, addSize func(st if section.Align > 1 && addrAligned >= sectionEnd { // The gap is caused by the section alignment. // For example, if a .rodata section ends with a non-aligned string. - addSize("(padding)", sectionEnd-addr, true) + program.getPackage("(padding)").addSize(getField, "", sectionEnd-addr, true) } else { - addSize("(unknown)", sectionEnd-addr, false) + program.getPackage("(unknown)").addSize(getField, "", sectionEnd-addr, false) if sizesDebug { fmt.Printf("%08x..%08x %5d: unknown (end), alignment=%d\n", addr, sectionEnd, sectionEnd-addr, section.Align) } @@ -912,17 +939,25 @@ func readSection(section memorySection, addresses []addressLine, addSize func(st // findPackagePath returns the Go package (or a pseudo package) for the given // path. It uses some heuristics, for example for some C libraries. -func findPackagePath(path string, packagePathMap map[string]string) string { +func findPackagePath(path string, packagePathMap map[string]string) (packagePath, filename string) { // Check whether this path is part of one of the compiled packages. packagePath, ok := packagePathMap[filepath.Dir(path)] - if !ok { + if ok { + // Directory is known as a Go package. + // Add the file itself as well. + filename = filepath.Base(path) + } else { if strings.HasPrefix(path, filepath.Join(goenv.Get("TINYGOROOT"), "lib")) { // Emit C libraries (in the lib subdirectory of TinyGo) as a single - // package, with a "C" prefix. For example: "C compiler-rt" for the - // compiler runtime library from LLVM. - packagePath = "C " + strings.Split(strings.TrimPrefix(path, filepath.Join(goenv.Get("TINYGOROOT"), "lib")), string(os.PathSeparator))[1] - } else if strings.HasPrefix(path, filepath.Join(goenv.Get("TINYGOROOT"), "llvm-project")) { + // package, with a "C" prefix. For example: "C picolibc" for the + // baremetal libc. + libPath := strings.TrimPrefix(path, filepath.Join(goenv.Get("TINYGOROOT"), "lib")+string(os.PathSeparator)) + parts := strings.SplitN(libPath, string(os.PathSeparator), 2) + packagePath = "C " + parts[0] + filename = parts[1] + } else if prefix := filepath.Join(goenv.Get("TINYGOROOT"), "llvm-project", "compiler-rt"); strings.HasPrefix(path, prefix) { packagePath = "C compiler-rt" + filename = strings.TrimPrefix(path, prefix+string(os.PathSeparator)) } else if packageSymbolRegexp.MatchString(path) { // Parse symbol names like main$alloc or runtime$string. packagePath = path[:strings.LastIndex(path, "$")] @@ -945,9 +980,11 @@ func findPackagePath(path string, packagePathMap map[string]string) string { // fixed in the compiler. packagePath = "-" } else { - // This is some other path. Not sure what it is, so just emit its directory. - packagePath = filepath.Dir(path) // fallback + // This is some other path. Not sure what it is, so just emit its + // directory as a fallback. + packagePath = filepath.Dir(path) + filename = filepath.Base(path) } } - return packagePath + return } |