Skip to content

Commit

Permalink
builder: show files in size report table
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
aykevl authored and deadprogram committed Dec 19, 2024
1 parent b182138 commit 9d2f528
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 38 deletions.
41 changes: 39 additions & 2 deletions builder/size-report.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -29,6 +35,9 @@ <h1>Size Report for {{.pkgName}}</h1>
<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>
Expand All @@ -42,8 +51,8 @@ <h2>Program breakdown</h2>
</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>
Expand All @@ -53,6 +62,24 @@ <h2>Program breakdown</h2>
{{.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">
Expand All @@ -68,5 +95,15 @@ <h2>Program breakdown</h2>
</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>
109 changes: 73 additions & 36 deletions builder/sizes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -61,6 +75,7 @@ type packageSize struct {
ROData uint64
Data uint64
BSS uint64
Sub map[string]*packageSize
}

// Flash usage in regular microcontrollers.
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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
Expand All @@ -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)
}
Expand All @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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, "$")]
Expand All @@ -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
}

0 comments on commit 9d2f528

Please sign in to comment.