aboutsummaryrefslogtreecommitdiffhomepage
path: root/builder
diff options
context:
space:
mode:
Diffstat (limited to 'builder')
-rw-r--r--builder/size-report.html41
-rw-r--r--builder/sizes.go109
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
}