Skip to content

Commit 35a056a

Browse files
projectgusdpgeorge
authored andcommitted
esp32/tools: Add metrics_esp32 size comparison script.
Signed-off-by: Angus Gratton <[email protected]>
1 parent 10601b0 commit 35a056a

File tree

1 file changed

+192
-0
lines changed

1 file changed

+192
-0
lines changed

ports/esp32/tools/metrics_esp32.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env python
2+
# MIT license; Copyright (c) 2024 Angus Gratton
3+
#
4+
# This is a utility script for MicroPython maintainers, similar to tools/metrics.py
5+
# but particular to this port. It's for measuring the impact of an ESP-IDF update or
6+
# config change at a high level.
7+
#
8+
# Specifically, it builds the esp32 MicroPython port for a collection of boards
9+
# and outputs a Markdown table of binary sizes, static IRAM size, and static
10+
# DRAM size (the latter generally inversely correlates to free heap at runtime.)
11+
#
12+
# To use:
13+
#
14+
# 1) Need to not be in an ESP-IDF venv already (i.e. don't source export.sh),
15+
# but IDF_PATH has to be set.
16+
#
17+
# 2) Choose the versions you want to test and the board/variant pairs by
18+
# editing the tuples below.
19+
#
20+
# 3) The IDF install script sometimes fails if it has to downgrade a package
21+
# within a minor version. The "nuclear option" is to delete all the install
22+
# environments and have this script recreate them as it runs:
23+
# rm -rf ~/.espressif/python_env/*
24+
#
25+
# 4) Run this script from the ports/esp32 directory, i.e.:
26+
# ./tools/metrics_esp32.py
27+
#
28+
# 5) If all goes well, it will run for a while and then print a Markdown
29+
# formatted table of binary sizes, sorted by board+variant.
30+
#
31+
# Note that for ESP32-S3 and C3, IRAM and DRAM are exchangeable so the IRAM size
32+
# column of the table is really D/IRAM.
33+
import os
34+
import re
35+
import sys
36+
import subprocess
37+
from dataclasses import dataclass
38+
39+
IDF_VERS = ("v5.2.2",)
40+
41+
BUILDS = (
42+
("ESP32_GENERIC", ""),
43+
("ESP32_GENERIC", "D2WD"),
44+
("ESP32_GENERIC", "SPIRAM"),
45+
("ESP32_GENERIC_S3", ""),
46+
("ESP32_GENERIC_S3", "SPIRAM_OCT"),
47+
)
48+
49+
50+
@dataclass
51+
class BuildSizes:
52+
idf_ver: str
53+
board: str
54+
variant: str
55+
bin_size: str = ""
56+
dram_size: str = ""
57+
iram_size: str = ""
58+
59+
def print_summary(self, include_ver=False):
60+
print(f"BOARD={self.board} BOARD_VARIANT={self.variant}")
61+
if include_ver:
62+
print(f"IDF_VER {self.idf_ver}")
63+
print(f"Binary size: {self.bin_size}")
64+
print(f"IRAM size: {self.iram_size}")
65+
print(f"DRAM size: {self.dram_size}")
66+
67+
def print_table_heading():
68+
print(
69+
"| BOARD | BOARD_VARIANT | IDF Version | Binary Size | Static IRAM Size | Static DRAM Size |"
70+
)
71+
print(
72+
"|-------|---------------|-------------|-------------|------------------|------------------|"
73+
)
74+
75+
def print_table_row(self, print_board):
76+
print(
77+
"| "
78+
+ " | ".join(
79+
(
80+
self.board if print_board else "",
81+
self.variant if print_board else "",
82+
self.idf_ver,
83+
self.bin_size,
84+
self.iram_size,
85+
self.dram_size,
86+
)
87+
)
88+
+ " |"
89+
)
90+
91+
def __lt__(self, other):
92+
"""sort by board, then variant, then IDF version to get an easy
93+
to compare table"""
94+
return (self.board, self.variant, self.idf_ver) < (
95+
other.board,
96+
other.variant,
97+
other.idf_ver,
98+
)
99+
100+
def build_dir(self):
101+
if self.variant:
102+
return f"build-{self.board}_{self.variant}"
103+
else:
104+
return f"build-{self.board}"
105+
106+
def run_make(self, target):
107+
env = dict(os.environ)
108+
env["BOARD"] = self.board
109+
env["BOARD_VARIANT"] = self.variant
110+
111+
try:
112+
# IDF version changes as we go, so re-export the environment each time
113+
cmd = f"source $IDF_PATH/export.sh; make {target}"
114+
return subprocess.check_output(
115+
cmd, shell=True, env=env, stderr=subprocess.STDOUT
116+
).decode()
117+
except subprocess.CalledProcessError as e:
118+
err_file = f"{self.build_dir()}/make-{target}-failed-{self.idf_ver}.log"
119+
print(f"'make {target}' failed, writing to log to {err_file}", file=sys.stderr)
120+
with open(err_file, "w") as f:
121+
f.write(e.output.decode())
122+
raise
123+
124+
def make_size(self):
125+
try:
126+
size_out = self.run_make("size")
127+
# "Used static DRAM:" or "Used stat D/IRAM:"
128+
RE_DRAM = r"Used stat(?:ic)? D.*: *(\d+) bytes"
129+
RE_IRAM = r"Used static IRAM: *(\d+) bytes"
130+
RE_BIN = r"Total image size: *(\d+) bytes"
131+
self.dram_size = re.search(RE_DRAM, size_out).group(1)
132+
self.iram_size = re.search(RE_IRAM, size_out).group(1)
133+
self.bin_size = re.search(RE_BIN, size_out).group(1)
134+
except subprocess.CalledProcessError:
135+
self.bin_size = "build failed"
136+
137+
138+
def main(do_clean):
139+
if "IDF_PATH" not in os.environ:
140+
raise RuntimeError("IDF_PATH must be set")
141+
142+
sizes = []
143+
for idf_ver in IDF_VERS:
144+
switch_ver(idf_ver)
145+
for board, variant in BUILDS:
146+
print(f"Building '{board}'/'{variant}'...", file=sys.stderr)
147+
result = BuildSizes(idf_ver, board, variant)
148+
result.run_make("clean")
149+
result.make_size()
150+
result.print_summary()
151+
sizes.append(result)
152+
153+
# print everything again as a table sorted by board+variant
154+
last_bv = ""
155+
BuildSizes.print_table_heading()
156+
for build_sizes in sorted(sizes):
157+
bv = (build_sizes.board, build_sizes.variant)
158+
build_sizes.print_table_row(last_bv != bv)
159+
last_bv = bv
160+
161+
162+
def idf_git(*commands):
163+
try:
164+
subprocess.check_output(
165+
["git"] + list(commands), cwd=os.environ["IDF_PATH"], stderr=subprocess.STDOUT
166+
)
167+
except subprocess.CalledProcessError as e:
168+
print(f"git {' '.join(commands)} failed:")
169+
print(e.output.decode())
170+
raise
171+
172+
173+
def idf_install():
174+
try:
175+
subprocess.check_output(
176+
["bash", "install.sh"], cwd=os.environ["IDF_PATH"], stderr=subprocess.STDOUT
177+
)
178+
except subprocess.CalledProcessError as e:
179+
print("IDF install.sh failed:")
180+
print(e.output.decode())
181+
raise
182+
183+
184+
def switch_ver(idf_ver):
185+
print(f"Switching version to {idf_ver}...", file=sys.stderr)
186+
idf_git("switch", "--detach", idf_ver)
187+
idf_git("submodule", "update", "--init", "--recursive")
188+
idf_install()
189+
190+
191+
if __name__ == "__main__":
192+
main("--no-clean" not in sys.argv)

0 commit comments

Comments
 (0)