mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-07-26 06:38:42 +00:00
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:
committed by
Ron Evans
parent
b18213805a
commit
9d2f52805b
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user