Files
micropython/ports/esp32/tools/metrics_esp32.py
IhorNehrutsa f48b981567 esp32: Update to use ESP-IDF v5.4.1.
This version of the IDF uses about 1KB more IRAM and 1KB more DRAM on most
boards, but 6.5KB more DRAM usage on the S3.  It seems that's due to a lot
of small increases in many components.

Signed-off-by: Ihor Nehrutsa <Ihor.Nehrutsa@gmail.com>
2025-05-16 14:02:39 +10:00

225 lines
7.3 KiB
Python
Executable File

#!/usr/bin/env python
# MIT license; Copyright (c) 2024 Angus Gratton
#
# This is a utility script for MicroPython maintainers, similar to tools/metrics.py
# but particular to this port. It's for measuring the impact of an ESP-IDF update or
# config change at a high level.
#
# Specifically, it builds the esp32 MicroPython port for a collection of boards
# and outputs a Markdown table of binary sizes, static IRAM size, and static
# DRAM size (the latter generally inversely correlates to free heap at runtime.)
#
# To use:
#
# 1) Need to not be in an ESP-IDF venv already (i.e. don't source export.sh),
# but IDF_PATH has to be set.
#
# 2) Choose the versions you want to test and the board/variant pairs by
# editing the tuples below.
#
# 3) The IDF install script sometimes fails if it has to downgrade a package
# within a minor version. The "nuclear option" is to delete all the install
# environments and have this script recreate them as it runs:
# rm -rf ~/.espressif/python_env/*
#
# 4) Run this script from the ports/esp32 directory, i.e.:
# ./tools/metrics_esp32.py
#
# 5) If all goes well, it will run for a while and then print a Markdown
# formatted table of binary sizes, sorted by board+variant.
#
# Note that for ESP32-S3 and C3, IRAM and DRAM are exchangeable so the IRAM size
# column of the table is really D/IRAM.
import os
import re
import shutil
import sys
import subprocess
from dataclasses import dataclass
IDF_VERS = ("v5.4.1",)
BUILDS = (
("ESP32_GENERIC", ""),
("ESP32_GENERIC", "D2WD"),
("ESP32_GENERIC", "SPIRAM"),
("ESP32_GENERIC_S3", ""),
("ESP32_GENERIC_S3", "SPIRAM_OCT"),
)
def rmtree(path):
try:
shutil.rmtree(path)
except FileNotFoundError:
pass
@dataclass
class BuildSizes:
idf_ver: str
board: str
variant: str
bin_size: str = ""
dram_size: str = ""
iram_size: str = ""
def print_summary(self, include_ver=False):
print(f"BOARD={self.board} BOARD_VARIANT={self.variant}")
if include_ver:
print(f"IDF_VER {self.idf_ver}")
print(f"Binary size: {self.bin_size}")
print(f"IRAM size: {self.iram_size}")
print(f"DRAM size: {self.dram_size}")
def print_table_heading():
print(
"| BOARD | BOARD_VARIANT | IDF Version | Binary Size | Static IRAM Size | Static DRAM Size |"
)
print(
"|-------|---------------|-------------|-------------|------------------|------------------|"
)
def print_table_row(self, print_board):
print(
"| "
+ " | ".join(
(
self.board if print_board else "",
self.variant if print_board else "",
self.idf_ver,
self.bin_size,
self.iram_size,
self.dram_size,
)
)
+ " |"
)
def __lt__(self, other):
"""sort by board, then variant, then IDF version to get an easy
to compare table"""
return (self.board, self.variant, self.idf_ver) < (
other.board,
other.variant,
other.idf_ver,
)
def build_dir(self):
if self.variant:
return f"build-{self.board}-{self.variant}"
else:
return f"build-{self.board}"
def run_make(self, target):
env = dict(os.environ)
env["BOARD"] = self.board
env["BOARD_VARIANT"] = self.variant
try:
# IDF version changes as we go, so re-export the environment each time
cmd = f"source $IDF_PATH/export.sh; make {target}"
return subprocess.check_output(
cmd, shell=True, env=env, stderr=subprocess.STDOUT
).decode()
except subprocess.CalledProcessError as e:
err_file = f"{self.build_dir()}/make-{target}-failed-{self.idf_ver}.log"
print(f"'make {target}' failed, writing to log to {err_file}", file=sys.stderr)
with open(err_file, "w") as f:
f.write(e.output.decode())
raise
def make_size(self):
try:
size_out = self.run_make("size")
try:
# pre IDF v5.4 size output
# "Used static DRAM:" or "Used stat D/IRAM:"
RE_DRAM = r"Used stat(?:ic)? D.*: *(\d+) bytes"
RE_IRAM = r"Used static IRAM: *(\d+) bytes"
self.dram_size = re.search(RE_DRAM, size_out).group(1)
self.iram_size = re.search(RE_IRAM, size_out).group(1)
except AttributeError:
# IDF v5.4 size output is much nicer formatted
# Note the pipes in these expressions are not the ASCII/RE |
RE_DRAM = r"│ *DI?RAM *│ *(\d+)"
RE_IRAM = r"│ *IRAM *│ *(\d+)"
self.dram_size = re.search(RE_DRAM, size_out).group(1)
self.iram_size = re.search(RE_IRAM, size_out).group(1)
# This line is the same on before/after versions
RE_BIN = r"Total image size: *(\d+) bytes"
self.bin_size = re.search(RE_BIN, size_out).group(1)
except subprocess.CalledProcessError:
self.bin_size = "build failed"
def main(do_clean):
if "IDF_PATH" not in os.environ:
raise RuntimeError("IDF_PATH must be set")
if not os.path.exists("Makefile"):
raise RuntimeError(
"This script must be run from the ports/esp32 directory, i.e. as ./tools/metrics_esp32.py"
)
if "IDF_PYTHON_ENV_PATH" in os.environ:
raise RuntimeError(
"Run this script without any existing ESP-IDF environment active/exported."
)
sizes = []
for idf_ver in IDF_VERS:
switch_ver(idf_ver)
rmtree("managed_components")
for board, variant in BUILDS:
print(f"Building '{board}'/'{variant}'...", file=sys.stderr)
result = BuildSizes(idf_ver, board, variant)
# Rather than running the 'clean' target, delete the build directory to avoid
# environment version mismatches, etc.
rmtree(result.build_dir())
result.make_size()
result.print_summary()
sizes.append(result)
# print everything again as a table sorted by board+variant
last_bv = ""
BuildSizes.print_table_heading()
for build_sizes in sorted(sizes):
bv = (build_sizes.board, build_sizes.variant)
build_sizes.print_table_row(last_bv != bv)
last_bv = bv
def idf_git(*commands):
try:
subprocess.check_output(
["git"] + list(commands), cwd=os.environ["IDF_PATH"], stderr=subprocess.STDOUT
)
except subprocess.CalledProcessError as e:
print(f"git {' '.join(commands)} failed:")
print(e.output.decode())
raise
def idf_install():
try:
subprocess.check_output(
["bash", "install.sh"], cwd=os.environ["IDF_PATH"], stderr=subprocess.STDOUT
)
except subprocess.CalledProcessError as e:
print("IDF install.sh failed:")
print(e.output.decode())
raise
def switch_ver(idf_ver):
print(f"Switching version to {idf_ver}...", file=sys.stderr)
idf_git("switch", "--detach", idf_ver)
idf_git("submodule", "update", "--init", "--recursive")
idf_install()
if __name__ == "__main__":
main("--no-clean" not in sys.argv)