Skip to content

Commit 9d2f528

Browse files
aykevldeadprogram
authored andcommitted
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.
1 parent b182138 commit 9d2f528

File tree

2 files changed

+112
-38
lines changed

2 files changed

+112
-38
lines changed

Diff for: builder/size-report.html

+39-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
border-left: calc(var(--bs-border-width) * 2) solid currentcolor;
1212
}
1313

14+
/* Hover on only the rows that are clickable. */
15+
.row-package:hover > * {
16+
--bs-table-color-state: var(--bs-table-hover-color);
17+
--bs-table-bg-state: var(--bs-table-hover-bg);
18+
}
19+
1420
</style>
1521
</head>
1622
<body>
@@ -29,6 +35,9 @@ <h1>Size Report for {{.pkgName}}</h1>
2935
<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>
3036

3137
<h2>Program breakdown</h2>
38+
39+
<p>You can click on the rows below to see which files contribute to the binary size.</p>
40+
3241
<div class="table-responsive">
3342
<table class="table w-auto">
3443
<thead>
@@ -42,8 +51,8 @@ <h2>Program breakdown</h2>
4251
</tr>
4352
</thead>
4453
<tbody class="table-group-divider">
45-
{{range .sizes}}
46-
<tr>
54+
{{range $i, $pkg := .sizes}}
55+
<tr class="row-package" data-collapse=".collapse-row-{{$i}}">
4756
<td>{{.Name}}</td>
4857
<td class="table-vertical-border">{{.Size.Code}}</td>
4958
<td>{{.Size.ROData}}</td>
@@ -53,6 +62,24 @@ <h2>Program breakdown</h2>
5362
{{.Size.Flash}}
5463
</td>
5564
</tr>
65+
{{range $filename, $sizes := .Size.Sub}}
66+
<tr class="table-secondary collapse collapse-row-{{$i}}">
67+
<td class="ps-4">
68+
{{if eq $filename ""}}
69+
(unknown file)
70+
{{else}}
71+
{{$filename}}
72+
{{end}}
73+
</td>
74+
<td class="table-vertical-border">{{$sizes.Code}}</td>
75+
<td>{{$sizes.ROData}}</td>
76+
<td>{{$sizes.Data}}</td>
77+
<td>{{$sizes.BSS}}</td>
78+
<td class="table-vertical-border" style="background: linear-gradient(to right, var(--bs-info-bg-subtle) {{$sizes.FlashPercent}}%, var(--bs-table-bg) {{$sizes.FlashPercent}}%)">
79+
{{$sizes.Flash}}
80+
</td>
81+
</tr>
82+
{{end}}
5683
{{end}}
5784
</tbody>
5885
<tfoot class="table-group-divider">
@@ -68,5 +95,15 @@ <h2>Program breakdown</h2>
6895
</table>
6996
</div>
7097
</div>
98+
<script>
99+
// Make table rows toggleable to show filenames.
100+
for (let clickable of document.querySelectorAll('.row-package')) {
101+
clickable.addEventListener('click', e => {
102+
for (let row of document.querySelectorAll(clickable.dataset.collapse)) {
103+
row.classList.toggle('show');
104+
}
105+
});
106+
}
107+
</script>
71108
</body>
72109
</html>

Diff for: builder/sizes.go

+73-36
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ func (ps *programSize) RAM() uint64 {
5353
return ps.Data + ps.BSS
5454
}
5555

56+
// Return the package size information for a given package path, creating it if
57+
// it doesn't exist yet.
58+
func (ps *programSize) getPackage(path string) *packageSize {
59+
if field, ok := ps.Packages[path]; ok {
60+
return field
61+
}
62+
field := &packageSize{
63+
Program: ps,
64+
Sub: map[string]*packageSize{},
65+
}
66+
ps.Packages[path] = field
67+
return field
68+
}
69+
5670
// packageSize contains the size of a package, calculated from the linked object
5771
// file.
5872
type packageSize struct {
@@ -61,6 +75,7 @@ type packageSize struct {
6175
ROData uint64
6276
Data uint64
6377
BSS uint64
78+
Sub map[string]*packageSize
6479
}
6580

6681
// Flash usage in regular microcontrollers.
@@ -79,6 +94,25 @@ func (ps *packageSize) FlashPercent() float64 {
7994
return float64(ps.Flash()) / float64(ps.Program.Flash()) * 100
8095
}
8196

97+
// Add a single size data point to this package.
98+
// This must only be called while calculating package size, not afterwards.
99+
func (ps *packageSize) addSize(getField func(*packageSize, bool) *uint64, filename string, size uint64, isVariable bool) {
100+
if size == 0 {
101+
return
102+
}
103+
104+
// Add size for the package.
105+
*getField(ps, isVariable) += size
106+
107+
// Add size for file inside package.
108+
sub, ok := ps.Sub[filename]
109+
if !ok {
110+
sub = &packageSize{Program: ps.Program}
111+
ps.Sub[filename] = sub
112+
}
113+
*getField(sub, isVariable) += size
114+
}
115+
82116
// A mapping of a single chunk of code or data to a file path.
83117
type addressLine struct {
84118
Address uint64
@@ -796,40 +830,32 @@ func loadProgramSize(path string, packagePathMap map[string]string) (*programSiz
796830
program := &programSize{
797831
Packages: sizes,
798832
}
799-
getSize := func(path string) *packageSize {
800-
if field, ok := sizes[path]; ok {
801-
return field
802-
}
803-
field := &packageSize{Program: program}
804-
sizes[path] = field
805-
return field
806-
}
807833
for _, section := range sections {
808834
switch section.Type {
809835
case memoryCode:
810-
readSection(section, addresses, func(path string, size uint64, isVariable bool) {
811-
field := getSize(path)
836+
readSection(section, addresses, program, func(ps *packageSize, isVariable bool) *uint64 {
812837
if isVariable {
813-
field.ROData += size
814-
} else {
815-
field.Code += size
838+
return &ps.ROData
816839
}
840+
return &ps.Code
817841
}, packagePathMap)
818842
case memoryROData:
819-
readSection(section, addresses, func(path string, size uint64, isVariable bool) {
820-
getSize(path).ROData += size
843+
readSection(section, addresses, program, func(ps *packageSize, isVariable bool) *uint64 {
844+
return &ps.ROData
821845
}, packagePathMap)
822846
case memoryData:
823-
readSection(section, addresses, func(path string, size uint64, isVariable bool) {
824-
getSize(path).Data += size
847+
readSection(section, addresses, program, func(ps *packageSize, isVariable bool) *uint64 {
848+
return &ps.Data
825849
}, packagePathMap)
826850
case memoryBSS:
827-
readSection(section, addresses, func(path string, size uint64, isVariable bool) {
828-
getSize(path).BSS += size
851+
readSection(section, addresses, program, func(ps *packageSize, isVariable bool) *uint64 {
852+
return &ps.BSS
829853
}, packagePathMap)
830854
case memoryStack:
831855
// We store the C stack as a pseudo-package.
832-
getSize("C stack").BSS += section.Size
856+
program.getPackage("C stack").addSize(func(ps *packageSize, isVariable bool) *uint64 {
857+
return &ps.BSS
858+
}, "", section.Size, false)
833859
}
834860
}
835861

@@ -844,8 +870,8 @@ func loadProgramSize(path string, packagePathMap map[string]string) (*programSiz
844870
}
845871

846872
// readSection determines for each byte in this section to which package it
847-
// belongs. It reports this usage through the addSize callback.
848-
func readSection(section memorySection, addresses []addressLine, addSize func(string, uint64, bool), packagePathMap map[string]string) {
873+
// belongs.
874+
func readSection(section memorySection, addresses []addressLine, program *programSize, getField func(*packageSize, bool) *uint64, packagePathMap map[string]string) {
849875
// The addr variable tracks at which address we are while going through this
850876
// section. We start at the beginning.
851877
addr := section.Address
@@ -867,9 +893,9 @@ func readSection(section memorySection, addresses []addressLine, addSize func(st
867893
addrAligned := (addr + line.Align - 1) &^ (line.Align - 1)
868894
if line.Align > 1 && addrAligned >= line.Address {
869895
// It is, assume that's what causes the gap.
870-
addSize("(padding)", line.Address-addr, true)
896+
program.getPackage("(padding)").addSize(getField, "", line.Address-addr, true)
871897
} else {
872-
addSize("(unknown)", line.Address-addr, false)
898+
program.getPackage("(unknown)").addSize(getField, "", line.Address-addr, false)
873899
if sizesDebug {
874900
fmt.Printf("%08x..%08x %5d: unknown (gap), alignment=%d\n", addr, line.Address, line.Address-addr, line.Align)
875901
}
@@ -891,7 +917,8 @@ func readSection(section memorySection, addresses []addressLine, addSize func(st
891917
length = line.Length - (addr - line.Address)
892918
}
893919
// Finally, mark this chunk of memory as used by the given package.
894-
addSize(findPackagePath(line.File, packagePathMap), length, line.IsVariable)
920+
packagePath, filename := findPackagePath(line.File, packagePathMap)
921+
program.getPackage(packagePath).addSize(getField, filename, length, line.IsVariable)
895922
addr = line.Address + line.Length
896923
}
897924
if addr < sectionEnd {
@@ -900,9 +927,9 @@ func readSection(section memorySection, addresses []addressLine, addSize func(st
900927
if section.Align > 1 && addrAligned >= sectionEnd {
901928
// The gap is caused by the section alignment.
902929
// For example, if a .rodata section ends with a non-aligned string.
903-
addSize("(padding)", sectionEnd-addr, true)
930+
program.getPackage("(padding)").addSize(getField, "", sectionEnd-addr, true)
904931
} else {
905-
addSize("(unknown)", sectionEnd-addr, false)
932+
program.getPackage("(unknown)").addSize(getField, "", sectionEnd-addr, false)
906933
if sizesDebug {
907934
fmt.Printf("%08x..%08x %5d: unknown (end), alignment=%d\n", addr, sectionEnd, sectionEnd-addr, section.Align)
908935
}
@@ -912,17 +939,25 @@ func readSection(section memorySection, addresses []addressLine, addSize func(st
912939

913940
// findPackagePath returns the Go package (or a pseudo package) for the given
914941
// path. It uses some heuristics, for example for some C libraries.
915-
func findPackagePath(path string, packagePathMap map[string]string) string {
942+
func findPackagePath(path string, packagePathMap map[string]string) (packagePath, filename string) {
916943
// Check whether this path is part of one of the compiled packages.
917944
packagePath, ok := packagePathMap[filepath.Dir(path)]
918-
if !ok {
945+
if ok {
946+
// Directory is known as a Go package.
947+
// Add the file itself as well.
948+
filename = filepath.Base(path)
949+
} else {
919950
if strings.HasPrefix(path, filepath.Join(goenv.Get("TINYGOROOT"), "lib")) {
920951
// Emit C libraries (in the lib subdirectory of TinyGo) as a single
921-
// package, with a "C" prefix. For example: "C compiler-rt" for the
922-
// compiler runtime library from LLVM.
923-
packagePath = "C " + strings.Split(strings.TrimPrefix(path, filepath.Join(goenv.Get("TINYGOROOT"), "lib")), string(os.PathSeparator))[1]
924-
} else if strings.HasPrefix(path, filepath.Join(goenv.Get("TINYGOROOT"), "llvm-project")) {
952+
// package, with a "C" prefix. For example: "C picolibc" for the
953+
// baremetal libc.
954+
libPath := strings.TrimPrefix(path, filepath.Join(goenv.Get("TINYGOROOT"), "lib")+string(os.PathSeparator))
955+
parts := strings.SplitN(libPath, string(os.PathSeparator), 2)
956+
packagePath = "C " + parts[0]
957+
filename = parts[1]
958+
} else if prefix := filepath.Join(goenv.Get("TINYGOROOT"), "llvm-project", "compiler-rt"); strings.HasPrefix(path, prefix) {
925959
packagePath = "C compiler-rt"
960+
filename = strings.TrimPrefix(path, prefix+string(os.PathSeparator))
926961
} else if packageSymbolRegexp.MatchString(path) {
927962
// Parse symbol names like main$alloc or runtime$string.
928963
packagePath = path[:strings.LastIndex(path, "$")]
@@ -945,9 +980,11 @@ func findPackagePath(path string, packagePathMap map[string]string) string {
945980
// fixed in the compiler.
946981
packagePath = "-"
947982
} else {
948-
// This is some other path. Not sure what it is, so just emit its directory.
949-
packagePath = filepath.Dir(path) // fallback
983+
// This is some other path. Not sure what it is, so just emit its
984+
// directory as a fallback.
985+
packagePath = filepath.Dir(path)
986+
filename = filepath.Base(path)
950987
}
951988
}
952-
return packagePath
989+
return
953990
}

0 commit comments

Comments
 (0)