mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-07-26 06:38:42 +00:00
builder: write HTML size report
This is not a big change over the existing size report with -size=full, but it is a bit more readable. More information will be added in subsequent commits.
This commit is contained in:
committed by
Ron Evans
parent
eeba90fd5b
commit
b18213805a
+11
-3
@@ -931,15 +931,16 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
|
||||
}
|
||||
|
||||
// Print code size if requested.
|
||||
if config.Options.PrintSizes == "short" || config.Options.PrintSizes == "full" {
|
||||
if config.Options.PrintSizes != "" {
|
||||
sizes, err := loadProgramSize(result.Executable, result.PackagePathMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config.Options.PrintSizes == "short" {
|
||||
switch config.Options.PrintSizes {
|
||||
case "short":
|
||||
fmt.Printf(" code data bss | flash ram\n")
|
||||
fmt.Printf("%7d %7d %7d | %7d %7d\n", sizes.Code+sizes.ROData, sizes.Data, sizes.BSS, sizes.Flash(), sizes.RAM())
|
||||
} else {
|
||||
case "full":
|
||||
if !config.Debug() {
|
||||
fmt.Println("warning: data incomplete, remove the -no-debug flag for more detail")
|
||||
}
|
||||
@@ -951,6 +952,13 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
|
||||
}
|
||||
fmt.Printf("------------------------------- | --------------- | -------\n")
|
||||
fmt.Printf("%7d %7d %7d %7d | %7d %7d | total\n", sizes.Code, sizes.ROData, sizes.Data, sizes.BSS, sizes.Code+sizes.ROData+sizes.Data, sizes.Data+sizes.BSS)
|
||||
case "html":
|
||||
const filename = "size-report.html"
|
||||
err := writeSizeReport(sizes, filename, pkgName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Wrote size report to", filename)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
//go:embed size-report.html
|
||||
var sizeReportBase string
|
||||
|
||||
func writeSizeReport(sizes *programSize, filename, pkgName string) error {
|
||||
tmpl, err := template.New("report").Parse(sizeReportBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open report file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Prepare data for the report.
|
||||
type sizeLine struct {
|
||||
Name string
|
||||
Size *packageSize
|
||||
}
|
||||
programData := []sizeLine{}
|
||||
for _, name := range sizes.sortedPackageNames() {
|
||||
pkgSize := sizes.Packages[name]
|
||||
programData = append(programData, sizeLine{
|
||||
Name: name,
|
||||
Size: pkgSize,
|
||||
})
|
||||
}
|
||||
sizeTotal := map[string]uint64{
|
||||
"code": sizes.Code,
|
||||
"rodata": sizes.ROData,
|
||||
"data": sizes.Data,
|
||||
"bss": sizes.BSS,
|
||||
"flash": sizes.Flash(),
|
||||
}
|
||||
|
||||
// Write the report.
|
||||
err = tmpl.Execute(f, map[string]any{
|
||||
"pkgName": pkgName,
|
||||
"sizes": programData,
|
||||
"sizeTotal": sizeTotal,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create report file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Size Report for {{.pkgName}}</title>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<style>
|
||||
|
||||
.table-vertical-border {
|
||||
border-left: calc(var(--bs-border-width) * 2) solid currentcolor;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-xxl">
|
||||
<h1>Size Report for {{.pkgName}}</h1>
|
||||
|
||||
<p>How much space is used by Go packages, C libraries, and other bits to set up the program environment.</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Code</strong> is the actual program code (machine code instructions).</li>
|
||||
<li><strong>Read-only data</strong> are read-only global variables. On most microcontrollers, these are stored in flash and do not take up any RAM.</li>
|
||||
<li><strong>Data</strong> are writable global variables with a non-zero initializer. On microcontrollers, they are copied from flash to RAM on reset.</li>
|
||||
<li><strong>BSS</strong> are writable global variables that are zero initialized. They do not take up any space in the binary, but do take up RAM. On microcontrollers, this area is zeroed on reset.</li>
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
<div class="table-responsive">
|
||||
<table class="table w-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package</th>
|
||||
<th class="table-vertical-border">Code</th>
|
||||
<th>Read-only data</th>
|
||||
<th>Data</th>
|
||||
<th title="zero-initialized data">BSS</th>
|
||||
<th class="table-vertical-border" style="min-width: 16em">Binary size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
{{range .sizes}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td class="table-vertical-border">{{.Size.Code}}</td>
|
||||
<td>{{.Size.ROData}}</td>
|
||||
<td>{{.Size.Data}}</td>
|
||||
<td>{{.Size.BSS}}</td>
|
||||
<td class="table-vertical-border" style="background: linear-gradient(to right, var(--bs-info-bg-subtle) {{.Size.FlashPercent}}%, var(--bs-table-bg) {{.Size.FlashPercent}}%)">
|
||||
{{.Size.Flash}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
<tfoot class="table-group-divider">
|
||||
<tr>
|
||||
<th>Total</th>
|
||||
<td class="table-vertical-border">{{.sizeTotal.code}}</td>
|
||||
<td>{{.sizeTotal.rodata}}</td>
|
||||
<td>{{.sizeTotal.data}}</td>
|
||||
<td>{{.sizeTotal.bss}}</td>
|
||||
<td class="table-vertical-border">{{.sizeTotal.flash}}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+29
-23
@@ -25,7 +25,7 @@ const sizesDebug = false
|
||||
|
||||
// programSize contains size statistics per package of a compiled program.
|
||||
type programSize struct {
|
||||
Packages map[string]packageSize
|
||||
Packages map[string]*packageSize
|
||||
Code uint64
|
||||
ROData uint64
|
||||
Data uint64
|
||||
@@ -56,10 +56,11 @@ func (ps *programSize) RAM() uint64 {
|
||||
// packageSize contains the size of a package, calculated from the linked object
|
||||
// file.
|
||||
type packageSize struct {
|
||||
Code uint64
|
||||
ROData uint64
|
||||
Data uint64
|
||||
BSS uint64
|
||||
Program *programSize
|
||||
Code uint64
|
||||
ROData uint64
|
||||
Data uint64
|
||||
BSS uint64
|
||||
}
|
||||
|
||||
// Flash usage in regular microcontrollers.
|
||||
@@ -72,6 +73,12 @@ func (ps *packageSize) RAM() uint64 {
|
||||
return ps.Data + ps.BSS
|
||||
}
|
||||
|
||||
// Flash usage in regular microcontrollers, as a percentage of the total flash
|
||||
// usage of the program.
|
||||
func (ps *packageSize) FlashPercent() float64 {
|
||||
return float64(ps.Flash()) / float64(ps.Program.Flash()) * 100
|
||||
}
|
||||
|
||||
// A mapping of a single chunk of code or data to a file path.
|
||||
type addressLine struct {
|
||||
Address uint64
|
||||
@@ -785,49 +792,48 @@ func loadProgramSize(path string, packagePathMap map[string]string) (*programSiz
|
||||
|
||||
// Now finally determine the binary/RAM size usage per package by going
|
||||
// through each allocated section.
|
||||
sizes := make(map[string]packageSize)
|
||||
sizes := make(map[string]*packageSize)
|
||||
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 := sizes[path]
|
||||
field := getSize(path)
|
||||
if isVariable {
|
||||
field.ROData += size
|
||||
} else {
|
||||
field.Code += size
|
||||
}
|
||||
sizes[path] = field
|
||||
}, packagePathMap)
|
||||
case memoryROData:
|
||||
readSection(section, addresses, func(path string, size uint64, isVariable bool) {
|
||||
field := sizes[path]
|
||||
field.ROData += size
|
||||
sizes[path] = field
|
||||
getSize(path).ROData += size
|
||||
}, packagePathMap)
|
||||
case memoryData:
|
||||
readSection(section, addresses, func(path string, size uint64, isVariable bool) {
|
||||
field := sizes[path]
|
||||
field.Data += size
|
||||
sizes[path] = field
|
||||
getSize(path).Data += size
|
||||
}, packagePathMap)
|
||||
case memoryBSS:
|
||||
readSection(section, addresses, func(path string, size uint64, isVariable bool) {
|
||||
field := sizes[path]
|
||||
field.BSS += size
|
||||
sizes[path] = field
|
||||
getSize(path).BSS += size
|
||||
}, packagePathMap)
|
||||
case memoryStack:
|
||||
// We store the C stack as a pseudo-package.
|
||||
sizes["C stack"] = packageSize{
|
||||
BSS: section.Size,
|
||||
}
|
||||
getSize("C stack").BSS += section.Size
|
||||
}
|
||||
}
|
||||
|
||||
// ...and summarize the results.
|
||||
program := &programSize{
|
||||
Packages: sizes,
|
||||
}
|
||||
for _, pkg := range sizes {
|
||||
program.Code += pkg.Code
|
||||
program.ROData += pkg.ROData
|
||||
|
||||
@@ -12,7 +12,7 @@ var (
|
||||
validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise"}
|
||||
validSchedulerOptions = []string{"none", "tasks", "asyncify"}
|
||||
validSerialOptions = []string{"none", "uart", "usb", "rtt"}
|
||||
validPrintSizeOptions = []string{"none", "short", "full"}
|
||||
validPrintSizeOptions = []string{"none", "short", "full", "html"}
|
||||
validPanicStrategyOptions = []string{"print", "trap"}
|
||||
validOptOptions = []string{"none", "0", "1", "2", "s", "z"}
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ func TestVerifyOptions(t *testing.T) {
|
||||
|
||||
expectedGCError := errors.New(`invalid gc option 'incorrect': valid values are none, leaking, conservative, custom, precise`)
|
||||
expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify`)
|
||||
expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full`)
|
||||
expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full, html`)
|
||||
expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`)
|
||||
|
||||
testCases := []struct {
|
||||
|
||||
@@ -1509,7 +1509,7 @@ func main() {
|
||||
stackSize = uint64(size)
|
||||
return err
|
||||
})
|
||||
printSize := flag.String("size", "", "print sizes (none, short, full)")
|
||||
printSize := flag.String("size", "", "print sizes (none, short, full, html)")
|
||||
printStacks := flag.Bool("print-stacks", false, "print stack sizes of goroutines")
|
||||
printAllocsString := flag.String("print-allocs", "", "regular expression of functions for which heap allocations should be printed")
|
||||
printCommands := flag.Bool("x", false, "Print commands")
|
||||
|
||||
Reference in New Issue
Block a user