Skip to content

Commit b182138

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

File tree

7 files changed

+171
-29
lines changed

7 files changed

+171
-29
lines changed

builder/build.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -931,15 +931,16 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
931931
}
932932

933933
// Print code size if requested.
934-
if config.Options.PrintSizes == "short" || config.Options.PrintSizes == "full" {
934+
if config.Options.PrintSizes != "" {
935935
sizes, err := loadProgramSize(result.Executable, result.PackagePathMap)
936936
if err != nil {
937937
return err
938938
}
939-
if config.Options.PrintSizes == "short" {
939+
switch config.Options.PrintSizes {
940+
case "short":
940941
fmt.Printf(" code data bss | flash ram\n")
941942
fmt.Printf("%7d %7d %7d | %7d %7d\n", sizes.Code+sizes.ROData, sizes.Data, sizes.BSS, sizes.Flash(), sizes.RAM())
942-
} else {
943+
case "full":
943944
if !config.Debug() {
944945
fmt.Println("warning: data incomplete, remove the -no-debug flag for more detail")
945946
}
@@ -951,6 +952,13 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
951952
}
952953
fmt.Printf("------------------------------- | --------------- | -------\n")
953954
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)
955+
case "html":
956+
const filename = "size-report.html"
957+
err := writeSizeReport(sizes, filename, pkgName)
958+
if err != nil {
959+
return err
960+
}
961+
fmt.Println("Wrote size report to", filename)
954962
}
955963
}
956964

builder/size-report.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package builder
2+
3+
import (
4+
_ "embed"
5+
"fmt"
6+
"html/template"
7+
"os"
8+
)
9+
10+
//go:embed size-report.html
11+
var sizeReportBase string
12+
13+
func writeSizeReport(sizes *programSize, filename, pkgName string) error {
14+
tmpl, err := template.New("report").Parse(sizeReportBase)
15+
if err != nil {
16+
return err
17+
}
18+
19+
f, err := os.Create(filename)
20+
if err != nil {
21+
return fmt.Errorf("could not open report file: %w", err)
22+
}
23+
defer f.Close()
24+
25+
// Prepare data for the report.
26+
type sizeLine struct {
27+
Name string
28+
Size *packageSize
29+
}
30+
programData := []sizeLine{}
31+
for _, name := range sizes.sortedPackageNames() {
32+
pkgSize := sizes.Packages[name]
33+
programData = append(programData, sizeLine{
34+
Name: name,
35+
Size: pkgSize,
36+
})
37+
}
38+
sizeTotal := map[string]uint64{
39+
"code": sizes.Code,
40+
"rodata": sizes.ROData,
41+
"data": sizes.Data,
42+
"bss": sizes.BSS,
43+
"flash": sizes.Flash(),
44+
}
45+
46+
// Write the report.
47+
err = tmpl.Execute(f, map[string]any{
48+
"pkgName": pkgName,
49+
"sizes": programData,
50+
"sizeTotal": sizeTotal,
51+
})
52+
if err != nil {
53+
return fmt.Errorf("could not create report file: %w", err)
54+
}
55+
return nil
56+
}

builder/size-report.html

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>Size Report for {{.pkgName}}</title>
5+
<meta charset="utf-8"/>
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
8+
<style>
9+
10+
.table-vertical-border {
11+
border-left: calc(var(--bs-border-width) * 2) solid currentcolor;
12+
}
13+
14+
</style>
15+
</head>
16+
<body>
17+
<div class="container-xxl">
18+
<h1>Size Report for {{.pkgName}}</h1>
19+
20+
<p>How much space is used by Go packages, C libraries, and other bits to set up the program environment.</p>
21+
22+
<ul>
23+
<li><strong>Code</strong> is the actual program code (machine code instructions).</li>
24+
<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>
25+
<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>
26+
<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>
27+
</ul>
28+
29+
<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>
30+
31+
<h2>Program breakdown</h2>
32+
<div class="table-responsive">
33+
<table class="table w-auto">
34+
<thead>
35+
<tr>
36+
<th>Package</th>
37+
<th class="table-vertical-border">Code</th>
38+
<th>Read-only data</th>
39+
<th>Data</th>
40+
<th title="zero-initialized data">BSS</th>
41+
<th class="table-vertical-border" style="min-width: 16em">Binary size</th>
42+
</tr>
43+
</thead>
44+
<tbody class="table-group-divider">
45+
{{range .sizes}}
46+
<tr>
47+
<td>{{.Name}}</td>
48+
<td class="table-vertical-border">{{.Size.Code}}</td>
49+
<td>{{.Size.ROData}}</td>
50+
<td>{{.Size.Data}}</td>
51+
<td>{{.Size.BSS}}</td>
52+
<td class="table-vertical-border" style="background: linear-gradient(to right, var(--bs-info-bg-subtle) {{.Size.FlashPercent}}%, var(--bs-table-bg) {{.Size.FlashPercent}}%)">
53+
{{.Size.Flash}}
54+
</td>
55+
</tr>
56+
{{end}}
57+
</tbody>
58+
<tfoot class="table-group-divider">
59+
<tr>
60+
<th>Total</th>
61+
<td class="table-vertical-border">{{.sizeTotal.code}}</td>
62+
<td>{{.sizeTotal.rodata}}</td>
63+
<td>{{.sizeTotal.data}}</td>
64+
<td>{{.sizeTotal.bss}}</td>
65+
<td class="table-vertical-border">{{.sizeTotal.flash}}</td>
66+
</tr>
67+
</tfoot>
68+
</table>
69+
</div>
70+
</div>
71+
</body>
72+
</html>

builder/sizes.go

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const sizesDebug = false
2525

2626
// programSize contains size statistics per package of a compiled program.
2727
type programSize struct {
28-
Packages map[string]packageSize
28+
Packages map[string]*packageSize
2929
Code uint64
3030
ROData uint64
3131
Data uint64
@@ -56,10 +56,11 @@ func (ps *programSize) RAM() uint64 {
5656
// packageSize contains the size of a package, calculated from the linked object
5757
// file.
5858
type packageSize struct {
59-
Code uint64
60-
ROData uint64
61-
Data uint64
62-
BSS uint64
59+
Program *programSize
60+
Code uint64
61+
ROData uint64
62+
Data uint64
63+
BSS uint64
6364
}
6465

6566
// Flash usage in regular microcontrollers.
@@ -72,6 +73,12 @@ func (ps *packageSize) RAM() uint64 {
7273
return ps.Data + ps.BSS
7374
}
7475

76+
// Flash usage in regular microcontrollers, as a percentage of the total flash
77+
// usage of the program.
78+
func (ps *packageSize) FlashPercent() float64 {
79+
return float64(ps.Flash()) / float64(ps.Program.Flash()) * 100
80+
}
81+
7582
// A mapping of a single chunk of code or data to a file path.
7683
type addressLine struct {
7784
Address uint64
@@ -785,49 +792,48 @@ func loadProgramSize(path string, packagePathMap map[string]string) (*programSiz
785792

786793
// Now finally determine the binary/RAM size usage per package by going
787794
// through each allocated section.
788-
sizes := make(map[string]packageSize)
795+
sizes := make(map[string]*packageSize)
796+
program := &programSize{
797+
Packages: sizes,
798+
}
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+
}
789807
for _, section := range sections {
790808
switch section.Type {
791809
case memoryCode:
792810
readSection(section, addresses, func(path string, size uint64, isVariable bool) {
793-
field := sizes[path]
811+
field := getSize(path)
794812
if isVariable {
795813
field.ROData += size
796814
} else {
797815
field.Code += size
798816
}
799-
sizes[path] = field
800817
}, packagePathMap)
801818
case memoryROData:
802819
readSection(section, addresses, func(path string, size uint64, isVariable bool) {
803-
field := sizes[path]
804-
field.ROData += size
805-
sizes[path] = field
820+
getSize(path).ROData += size
806821
}, packagePathMap)
807822
case memoryData:
808823
readSection(section, addresses, func(path string, size uint64, isVariable bool) {
809-
field := sizes[path]
810-
field.Data += size
811-
sizes[path] = field
824+
getSize(path).Data += size
812825
}, packagePathMap)
813826
case memoryBSS:
814827
readSection(section, addresses, func(path string, size uint64, isVariable bool) {
815-
field := sizes[path]
816-
field.BSS += size
817-
sizes[path] = field
828+
getSize(path).BSS += size
818829
}, packagePathMap)
819830
case memoryStack:
820831
// We store the C stack as a pseudo-package.
821-
sizes["C stack"] = packageSize{
822-
BSS: section.Size,
823-
}
832+
getSize("C stack").BSS += section.Size
824833
}
825834
}
826835

827836
// ...and summarize the results.
828-
program := &programSize{
829-
Packages: sizes,
830-
}
831837
for _, pkg := range sizes {
832838
program.Code += pkg.Code
833839
program.ROData += pkg.ROData

compileopts/options.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ var (
1212
validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise"}
1313
validSchedulerOptions = []string{"none", "tasks", "asyncify"}
1414
validSerialOptions = []string{"none", "uart", "usb", "rtt"}
15-
validPrintSizeOptions = []string{"none", "short", "full"}
15+
validPrintSizeOptions = []string{"none", "short", "full", "html"}
1616
validPanicStrategyOptions = []string{"print", "trap"}
1717
validOptOptions = []string{"none", "0", "1", "2", "s", "z"}
1818
)

compileopts/options_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ func TestVerifyOptions(t *testing.T) {
1111

1212
expectedGCError := errors.New(`invalid gc option 'incorrect': valid values are none, leaking, conservative, custom, precise`)
1313
expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify`)
14-
expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full`)
14+
expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full, html`)
1515
expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`)
1616

1717
testCases := []struct {

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1509,7 +1509,7 @@ func main() {
15091509
stackSize = uint64(size)
15101510
return err
15111511
})
1512-
printSize := flag.String("size", "", "print sizes (none, short, full)")
1512+
printSize := flag.String("size", "", "print sizes (none, short, full, html)")
15131513
printStacks := flag.Bool("print-stacks", false, "print stack sizes of goroutines")
15141514
printAllocsString := flag.String("print-allocs", "", "regular expression of functions for which heap allocations should be printed")
15151515
printCommands := flag.Bool("x", false, "Print commands")

0 commit comments

Comments
 (0)