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.
This commit is contained in:
Ayke van Laethem
2024-12-17 11:30:22 +01:00
committed by Ron Evans
parent b18213805a
commit 9d2f52805b
2 changed files with 112 additions and 38 deletions
+39 -2
View File
@@ -11,6 +11,12 @@
border-left: calc(var(--bs-border-width) * 2) solid currentcolor; 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> </style>
</head> </head>
<body> <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> <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> <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"> <div class="table-responsive">
<table class="table w-auto"> <table class="table w-auto">
<thead> <thead>
@@ -42,8 +51,8 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{{range .sizes}} {{range $i, $pkg := .sizes}}
<tr> <tr class="row-package" data-collapse=".collapse-row-{{$i}}">
<td>{{.Name}}</td> <td>{{.Name}}</td>
<td class="table-vertical-border">{{.Size.Code}}</td> <td class="table-vertical-border">{{.Size.Code}}</td>
<td>{{.Size.ROData}}</td> <td>{{.Size.ROData}}</td>
@@ -53,6 +62,24 @@
{{.Size.Flash}} {{.Size.Flash}}
</td> </td>
</tr> </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}} {{end}}
</tbody> </tbody>
<tfoot class="table-group-divider"> <tfoot class="table-group-divider">
@@ -68,5 +95,15 @@
</table> </table>
</div> </div>
</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> </body>
</html> </html>
+73 -36
View File
@@ -53,6 +53,20 @@ func (ps *programSize) RAM() uint64 {
return ps.Data + ps.BSS 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 // packageSize contains the size of a package, calculated from the linked object
// file. // file.
type packageSize struct { type packageSize struct {
@@ -61,6 +75,7 @@ type packageSize struct {
ROData uint64 ROData uint64
Data uint64 Data uint64
BSS uint64 BSS uint64
Sub map[string]*packageSize
} }
// Flash usage in regular microcontrollers. // Flash usage in regular microcontrollers.
@@ -79,6 +94,25 @@ func (ps *packageSize) FlashPercent() float64 {
return float64(ps.Flash()) / float64(ps.Program.Flash()) * 100 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. // A mapping of a single chunk of code or data to a file path.
type addressLine struct { type addressLine struct {
Address uint64 Address uint64
@@ -796,40 +830,32 @@ func loadProgramSize(path string, packagePathMap map[string]string) (*programSiz
program := &programSize{ program := &programSize{
Packages: sizes, 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 { for _, section := range sections {
switch section.Type { switch section.Type {
case memoryCode: case memoryCode:
readSection(section, addresses, func(path string, size uint64, isVariable bool) { readSection(section, addresses, program, func(ps *packageSize, isVariable bool) *uint64 {
field := getSize(path)
if isVariable { if isVariable {
field.ROData += size return &ps.ROData
} else {
field.Code += size
} }
return &ps.Code
}, packagePathMap) }, packagePathMap)
case memoryROData: case memoryROData:
readSection(section, addresses, func(path string, size uint64, isVariable bool) { readSection(section, addresses, program, func(ps *packageSize, isVariable bool) *uint64 {
getSize(path).ROData += size return &ps.ROData
}, packagePathMap) }, packagePathMap)
case memoryData: case memoryData:
readSection(section, addresses, func(path string, size uint64, isVariable bool) { readSection(section, addresses, program, func(ps *packageSize, isVariable bool) *uint64 {
getSize(path).Data += size return &ps.Data
}, packagePathMap) }, packagePathMap)
case memoryBSS: case memoryBSS:
readSection(section, addresses, func(path string, size uint64, isVariable bool) { readSection(section, addresses, program, func(ps *packageSize, isVariable bool) *uint64 {
getSize(path).BSS += size return &ps.BSS
}, packagePathMap) }, packagePathMap)
case memoryStack: case memoryStack:
// We store the C stack as a pseudo-package. // 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 // readSection determines for each byte in this section to which package it
// belongs. It reports this usage through the addSize callback. // belongs.
func readSection(section memorySection, addresses []addressLine, addSize func(string, uint64, bool), packagePathMap map[string]string) { 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 // The addr variable tracks at which address we are while going through this
// section. We start at the beginning. // section. We start at the beginning.
addr := section.Address addr := section.Address
@@ -867,9 +893,9 @@ func readSection(section memorySection, addresses []addressLine, addSize func(st
addrAligned := (addr + line.Align - 1) &^ (line.Align - 1) addrAligned := (addr + line.Align - 1) &^ (line.Align - 1)
if line.Align > 1 && addrAligned >= line.Address { if line.Align > 1 && addrAligned >= line.Address {
// It is, assume that's what causes the gap. // 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 { } else {
addSize("(unknown)", line.Address-addr, false) program.getPackage("(unknown)").addSize(getField, "", line.Address-addr, false)
if sizesDebug { if sizesDebug {
fmt.Printf("%08x..%08x %5d: unknown (gap), alignment=%d\n", addr, line.Address, line.Address-addr, line.Align) 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) length = line.Length - (addr - line.Address)
} }
// Finally, mark this chunk of memory as used by the given package. // 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 addr = line.Address + line.Length
} }
if addr < sectionEnd { if addr < sectionEnd {
@@ -900,9 +927,9 @@ func readSection(section memorySection, addresses []addressLine, addSize func(st
if section.Align > 1 && addrAligned >= sectionEnd { if section.Align > 1 && addrAligned >= sectionEnd {
// The gap is caused by the section alignment. // The gap is caused by the section alignment.
// For example, if a .rodata section ends with a non-aligned string. // 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 { } else {
addSize("(unknown)", sectionEnd-addr, false) program.getPackage("(unknown)").addSize(getField, "", sectionEnd-addr, false)
if sizesDebug { if sizesDebug {
fmt.Printf("%08x..%08x %5d: unknown (end), alignment=%d\n", addr, sectionEnd, sectionEnd-addr, section.Align) 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 // findPackagePath returns the Go package (or a pseudo package) for the given
// path. It uses some heuristics, for example for some C libraries. // 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. // Check whether this path is part of one of the compiled packages.
packagePath, ok := packagePathMap[filepath.Dir(path)] 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")) { if strings.HasPrefix(path, filepath.Join(goenv.Get("TINYGOROOT"), "lib")) {
// Emit C libraries (in the lib subdirectory of TinyGo) as a single // Emit C libraries (in the lib subdirectory of TinyGo) as a single
// package, with a "C" prefix. For example: "C compiler-rt" for the // package, with a "C" prefix. For example: "C picolibc" for the
// compiler runtime library from LLVM. // baremetal libc.
packagePath = "C " + strings.Split(strings.TrimPrefix(path, filepath.Join(goenv.Get("TINYGOROOT"), "lib")), string(os.PathSeparator))[1] libPath := strings.TrimPrefix(path, filepath.Join(goenv.Get("TINYGOROOT"), "lib")+string(os.PathSeparator))
} else if strings.HasPrefix(path, filepath.Join(goenv.Get("TINYGOROOT"), "llvm-project")) { 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" packagePath = "C compiler-rt"
filename = strings.TrimPrefix(path, prefix+string(os.PathSeparator))
} else if packageSymbolRegexp.MatchString(path) { } else if packageSymbolRegexp.MatchString(path) {
// Parse symbol names like main$alloc or runtime$string. // Parse symbol names like main$alloc or runtime$string.
packagePath = path[:strings.LastIndex(path, "$")] packagePath = path[:strings.LastIndex(path, "$")]
@@ -945,9 +980,11 @@ func findPackagePath(path string, packagePathMap map[string]string) string {
// fixed in the compiler. // fixed in the compiler.
packagePath = "-" packagePath = "-"
} else { } else {
// This is some other path. Not sure what it is, so just emit its directory. // This is some other path. Not sure what it is, so just emit its
packagePath = filepath.Dir(path) // fallback // directory as a fallback.
packagePath = filepath.Dir(path)
filename = filepath.Base(path)
} }
} }
return packagePath return
} }