mirror of
https://github.com/hathach/tinyusb.git
synced 2026-08-18 11:02:16 +00:00
Merge remote-tracking branch 'origin/master' into add-ch58x-usbfs
This commit is contained in:
@ -105,16 +105,14 @@ def print_build_result(board, build_target, status, duration):
|
||||
# -----------------------------
|
||||
# CMake
|
||||
# -----------------------------
|
||||
def cmake_board(board, build_args, build_flags_on, build_targets):
|
||||
def cmake_board(board, build_args, build_name, build_cflags, build_targets):
|
||||
ret = [0, 0, 0]
|
||||
start_time = time.monotonic()
|
||||
|
||||
build_dir = f'cmake-build/cmake-build-{board}'
|
||||
build_dir = f'cmake-build/cmake-build-{build_name or board}'
|
||||
build_flags = []
|
||||
if len(build_flags_on) > 0:
|
||||
cli_flags = ' '.join(f'-D{flag}=1' for flag in build_flags_on)
|
||||
build_flags.append(f'-DCFLAGS_CLI={cli_flags}')
|
||||
build_dir += '-f1_' + '_'.join(build_flags_on)
|
||||
if build_cflags:
|
||||
build_flags.append('-DCFLAGS_CLI=' + ' '.join(build_cflags))
|
||||
|
||||
family = find_family(board)
|
||||
if family == 'espressif':
|
||||
@ -194,13 +192,13 @@ def make_board(board, build_args, build_targets):
|
||||
# -----------------------------
|
||||
# Build Family
|
||||
# -----------------------------
|
||||
def build_boards_list(boards, build_defines, build_system, build_flags_on, build_targets):
|
||||
def build_boards_list(boards, build_defines, build_system, build_name, build_cflags, build_targets):
|
||||
ret = [0, 0, 0]
|
||||
for b in boards:
|
||||
r = [0, 0, 0]
|
||||
if build_system == 'cmake':
|
||||
build_args = [f'-D{d}' for d in build_defines]
|
||||
r = cmake_board(b, build_args, build_flags_on, build_targets)
|
||||
r = cmake_board(b, build_args, build_name, build_cflags, build_targets)
|
||||
elif build_system == 'make':
|
||||
build_args = ' '.join(f'{d}' for d in build_defines)
|
||||
r = make_board(b, build_args, build_targets)
|
||||
@ -261,7 +259,10 @@ def main():
|
||||
parser.add_argument('-t', '--toolchain', default='gcc', help='Toolchain to use, default is gcc')
|
||||
parser.add_argument('-s', '--build-system', default='cmake', help='Build system to use, default is cmake')
|
||||
parser.add_argument('-D', '--define-symbol', action='append', default=[], help='Define to pass to build system')
|
||||
parser.add_argument('-f1', '--build-flags-on', action='append', default=[], help='Build flag to pass to build system')
|
||||
parser.add_argument('--build-name', default=None,
|
||||
help='Override build dir name (cmake-build-<name>); default is the board name. Used for HIL variants.')
|
||||
parser.add_argument('--cflag', action='append', default=[],
|
||||
help='Raw compiler flag appended to CFLAGS_CLI, e.g. --cflag=-DCFG_TUD_DWC2_DMA_ENABLE=1 (repeatable)')
|
||||
parser.add_argument('--one-random', action='store_true', default=False,
|
||||
help='Build only one random board of each specified family')
|
||||
parser.add_argument('--one-first', action='store_true', default=False,
|
||||
@ -277,7 +278,8 @@ def main():
|
||||
toolchain = args.toolchain
|
||||
build_system = args.build_system
|
||||
build_defines = args.define_symbol
|
||||
build_flags_on = args.build_flags_on
|
||||
build_name = args.build_name
|
||||
build_cflags = args.cflag
|
||||
one_random = args.one_random
|
||||
one_first = args.one_first
|
||||
build_targets = args.target if args.target else ['all']
|
||||
@ -290,6 +292,12 @@ def main():
|
||||
print("Please specify families or board to build")
|
||||
return 1
|
||||
|
||||
# --build-name renames the single shared build dir, so building more than one
|
||||
# board with it would clobber/mix artifacts
|
||||
if build_name and (len(families) > 0 or len(boards) != 1):
|
||||
print("--build-name requires exactly one board (-b) and no families")
|
||||
return 1
|
||||
|
||||
print(build_separator)
|
||||
print(build_format.format('Board', 'Target', '\033[39mResult\033[0m', 'Time'))
|
||||
total_time = time.monotonic()
|
||||
@ -310,7 +318,7 @@ def main():
|
||||
all_boards.extend(get_family_boards(f, one_random, one_first))
|
||||
|
||||
# build all boards
|
||||
result = build_boards_list(all_boards, build_defines, build_system, build_flags_on, build_targets)
|
||||
result = build_boards_list(all_boards, build_defines, build_system, build_name, build_cflags, build_targets)
|
||||
|
||||
total_time = time.monotonic() - total_time
|
||||
print(build_separator)
|
||||
|
||||
@ -6,6 +6,7 @@ fro
|
||||
hsi
|
||||
inout
|
||||
mot
|
||||
ore
|
||||
pris
|
||||
ptd
|
||||
ser
|
||||
|
||||
@ -32,7 +32,7 @@ def main():
|
||||
"hidden": True,
|
||||
"description": r"Configure preset for the ${presetName} board",
|
||||
"generator": "Ninja Multi-Config",
|
||||
"binaryDir": r"${sourceDir}/build/${presetName}",
|
||||
"binaryDir": r"${sourceDir}/cmake-build-${presetName}",
|
||||
"cacheVariables": {
|
||||
"CMAKE_DEFAULT_BUILD_TYPE": "RelWithDebInfo",
|
||||
"BOARD": r"${presetName}"
|
||||
@ -41,7 +41,7 @@ def main():
|
||||
"hidden": True,
|
||||
"description": r"Configure preset for the ${presetName} board",
|
||||
"generator": "Ninja",
|
||||
"binaryDir": r"${sourceDir}/build/${presetName}",
|
||||
"binaryDir": r"${sourceDir}/cmake-build-${presetName}",
|
||||
"cacheVariables": {
|
||||
"BOARD": r"${presetName}"
|
||||
}}]
|
||||
|
||||
@ -8,8 +8,11 @@ from multiprocessing import Pool
|
||||
# Mandatory Dependencies that is always fetched
|
||||
# path, url, commit, family (Alphabet sorted by path)
|
||||
deps_mandatory = {
|
||||
'lib/fatfs': ['https://github.com/abbrev/fatfs.git',
|
||||
'30ca13c62615df0d2e9104ab41256985b96590c1',
|
||||
'all'],
|
||||
'lib/FreeRTOS-Kernel': ['https://github.com/FreeRTOS/FreeRTOS-Kernel.git',
|
||||
'cc0e0707c0c748713485b870bb980852b210877f',
|
||||
'9b777ae5c5b8e9e456065a00294d1e5f5f9facf5',
|
||||
'all'],
|
||||
'lib/lwip': ['https://github.com/lwip-tcpip/lwip.git',
|
||||
'159e31b689577dbf69cf0683bbaffbd71fa5ee10',
|
||||
@ -79,6 +82,9 @@ deps_optional = {
|
||||
'hw/mcu/nxp/mcux-devices-rt': ['https://github.com/nxp-mcuxpresso/mcux-devices-rt',
|
||||
'dba2b523c9df61f3330bd186242f8210a8e47c45',
|
||||
'imxrt'],
|
||||
'hw/mcu/raspberry_pi/FreeRTOS-Kernel': ['https://github.com/raspberrypi/FreeRTOS-Kernel.git',
|
||||
'4f7299d6ea746b27a9dd19e87af568e34bd65b15',
|
||||
'rp2040'],
|
||||
'hw/mcu/raspberry_pi/Pico-PIO-USB': ['https://github.com/sekigon-gonnoc/Pico-PIO-USB.git',
|
||||
'675543bcc9baa8170f868ab7ba316d418dbcf41f',
|
||||
'rp2040'],
|
||||
@ -157,6 +163,9 @@ deps_optional = {
|
||||
'hw/mcu/st/cmsis-device-wba': ['https://github.com/STMicroelectronics/cmsis-device-wba.git',
|
||||
'647d8522e5fd15049e9a1cc30ed19d85e5911eaf',
|
||||
'stm32wba'],
|
||||
'hw/mcu/st/stm32c5xx-dfp': ['https://github.com/STMicroelectronics/stm32c5xx-dfp.git',
|
||||
'6d0940882511d9430f83af9bd3da6bcb77f79239',
|
||||
'stm32c5'],
|
||||
'hw/mcu/st/stm32-mfxstm32l152': ['https://github.com/STMicroelectronics/stm32-mfxstm32l152.git',
|
||||
'7f4389efee9c6a655b55e5df3fceef5586b35f9b',
|
||||
'stm32h7'],
|
||||
@ -226,6 +235,9 @@ deps_optional = {
|
||||
'hw/mcu/st/stm32wbaxx_hal_driver': ['https://github.com/STMicroelectronics/stm32wbaxx_hal_driver.git',
|
||||
'9442fbb71f855ff2e64fbf662b7726beba511a24',
|
||||
'stm32wba'],
|
||||
'hw/mcu/st/stm32c5xx-drivers': ['https://github.com/STMicroelectronics/stm32c5xx-drivers.git',
|
||||
'79b901285a7efeaf87c4c25db81d24cb5d8c9465',
|
||||
'stm32c5'],
|
||||
'hw/mcu/ti': ['https://github.com/hathach/ti_driver.git',
|
||||
'083944907e7d08fcb1f614b47598ce45935b8da1',
|
||||
'msp430 msp432e4 tm4c'],
|
||||
@ -281,12 +293,17 @@ deps_optional = {
|
||||
'tm4c '],
|
||||
'lib/CMSIS_6': ['https://github.com/ARM-software/CMSIS_6.git',
|
||||
'6f0a58d01aa9bd2feba212097f9afe7acd991d52',
|
||||
'imxrt kinetis_k32l ra stm32n6 lpc51 lpc55 mcx'],
|
||||
'imxrt kinetis_k32l ra stm32n6 lpc51 lpc55 mcx stm32c5'],
|
||||
'lib/sct_neopixel': ['https://github.com/gsteiert/sct_neopixel.git',
|
||||
'e73e04ca63495672d955f9268e003cffe168fcd8',
|
||||
'lpc55'],
|
||||
}
|
||||
|
||||
# Files to remove after cloning to avoid conflicts with TinyUSB's custom versions
|
||||
deps_remove_files = {
|
||||
'lib/fatfs': ['source/ffconf.h'],
|
||||
}
|
||||
|
||||
# combined 2 deps
|
||||
deps_all = {**deps_mandatory, **deps_optional}
|
||||
|
||||
@ -332,6 +349,13 @@ def get_a_dep(d):
|
||||
run_cmd(f"{git_cmd} fetch --depth 1 origin {commit}")
|
||||
run_cmd(f"{git_cmd} checkout FETCH_HEAD")
|
||||
|
||||
# Remove files that conflict with TinyUSB's custom versions
|
||||
if d in deps_remove_files:
|
||||
for f in deps_remove_files[d]:
|
||||
fp = p / f
|
||||
if fp.exists():
|
||||
fp.unlink()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@ -351,6 +375,8 @@ def main():
|
||||
parser.add_argument('-b', '--board', action='append', default=[], help='Boards to fetch')
|
||||
parser.add_argument('-D', '--define', action='append', default=[], help='Have no effect')
|
||||
parser.add_argument('-f1', '--build-flags-on', action='append', default=[], help='Have no effect')
|
||||
parser.add_argument('--build-name', default=None, help='Have no effect')
|
||||
parser.add_argument('--cflag', action='append', default=[], help='Have no effect')
|
||||
args = parser.parse_args()
|
||||
|
||||
families = args.families
|
||||
|
||||
@ -166,6 +166,9 @@ def compute_avg(all_json_data):
|
||||
file_accumulator[fname]["symbols"][name].append(sym.get("size", 0))
|
||||
sections_map = f.get("sections") or {}
|
||||
for sname, ssize in sections_map.items():
|
||||
# linkermap -v produces nested dicts {subsection: size}, flatten to total
|
||||
if isinstance(ssize, dict):
|
||||
ssize = sum(ssize.values())
|
||||
file_accumulator[fname]["sections"][sname].append(ssize)
|
||||
|
||||
# Build json_average with averaged values
|
||||
@ -209,7 +212,7 @@ def compute_avg(all_json_data):
|
||||
|
||||
|
||||
def compare_files(base_file, new_file, filters=None):
|
||||
"""Compare two CSV or JSON inputs and generate difference report."""
|
||||
"""Compare two CSV or JSON inputs and generate a difference report."""
|
||||
filters = filters or []
|
||||
|
||||
base_avg = compute_avg(combine_files([base_file], filters))
|
||||
@ -381,7 +384,7 @@ def render_combine_table(json_data, sort_order='name+'):
|
||||
def write_combine_markdown(json_data, path, sort_order='name+', title="TinyUSB Average Code Size Metrics"):
|
||||
"""Write averaged size data to a markdown file."""
|
||||
|
||||
md_lines = [f"# {title}", ""]
|
||||
md_lines = [f"## {title}", ""]
|
||||
md_lines.extend(render_combine_table(json_data, sort_order))
|
||||
md_lines.append("")
|
||||
|
||||
@ -397,7 +400,7 @@ def write_combine_markdown(json_data, path, sort_order='name+', title="TinyUSB A
|
||||
def write_compare_markdown(comparison, path, sort_order='size'):
|
||||
"""Write comparison data to markdown file."""
|
||||
md_lines = [
|
||||
"# Size Difference Report",
|
||||
"## Size Difference Report",
|
||||
"",
|
||||
"Because TinyUSB code size varies by port and configuration, the metrics below represent the averaged totals across all example builds.",
|
||||
"",
|
||||
@ -412,7 +415,7 @@ def write_compare_markdown(comparison, path, sort_order='size'):
|
||||
md_lines.append(f"<details><summary>{title}</summary>")
|
||||
md_lines.append("")
|
||||
else:
|
||||
md_lines.append(f"## {title}")
|
||||
md_lines.append(f"### {title}")
|
||||
|
||||
md_lines.extend(render_compare_table(_build_rows(rows, sort_order), include_sum=True))
|
||||
md_lines.append("")
|
||||
@ -422,7 +425,7 @@ def write_compare_markdown(comparison, path, sort_order='size'):
|
||||
md_lines.append("")
|
||||
|
||||
render("Changes >1% in size", significant)
|
||||
render("Changes <1% in size", minor)
|
||||
render("Changes <1% in size", minor, collapsed=True)
|
||||
render("No changes", unchanged, collapsed=True)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
|
||||
350
tools/metrics_compare_base.py
Normal file
350
tools/metrics_compare_base.py
Normal file
@ -0,0 +1,350 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build base branch (master) and current tree, then compare code size metrics.
|
||||
|
||||
Creates cmake-metrics/<board>/{base,build} directories for each board.
|
||||
With --combined, also writes cmake-metrics/_combined/metrics_compare.md aggregating
|
||||
all boards into a single comparison.
|
||||
|
||||
Usage:
|
||||
python tools/metrics_compare_base.py -b raspberry_pi_pico
|
||||
python tools/metrics_compare_base.py -b raspberry_pi_pico -b raspberry_pi_pico2
|
||||
python tools/metrics_compare_base.py -b raspberry_pi_pico -f portable/raspberrypi
|
||||
python tools/metrics_compare_base.py -b raspberry_pi_pico -e device/cdc_msc
|
||||
python tools/metrics_compare_base.py -b raspberry_pi_pico -e device/cdc_msc --bloaty
|
||||
python tools/metrics_compare_base.py --ci # first board of each arm-gcc family, combined
|
||||
python tools/metrics_compare_base.py -b pico -b pico2 --combined # aggregate listed boards
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
TINYUSB_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
METRICS_DIR = os.path.join(TINYUSB_ROOT, 'cmake-metrics')
|
||||
|
||||
def tinyusb_src_filter(checkout_dir):
|
||||
"""Return a path-substring filter that uniquely matches TinyUSB stack source files
|
||||
in `checkout_dir`. The substring is the absolute path to the checkout's `src/`
|
||||
dir — collision-free with vendored deps (pico-sdk, lwip, FreeRTOS, etc.) which
|
||||
live at unrelated paths."""
|
||||
return os.path.realpath(os.path.join(checkout_dir, 'src')) + os.sep
|
||||
|
||||
verbose = False
|
||||
|
||||
|
||||
def run(cmd, **kwargs):
|
||||
"""Run a command. cmd must be a list (no shell=True). On `timeout=`-induced
|
||||
TimeoutExpired, return a CompletedProcess with rc=124 instead of letting the
|
||||
exception propagate, so the caller can fall through to error reporting and
|
||||
worktree cleanup rather than crashing with a traceback."""
|
||||
if not isinstance(cmd, list):
|
||||
raise TypeError('run() requires a list, got str — fix the caller')
|
||||
if verbose:
|
||||
print(f' $ {" ".join(shlex.quote(str(c)) for c in cmd)}')
|
||||
try:
|
||||
return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
|
||||
except subprocess.TimeoutExpired as e:
|
||||
msg = f'Command timed out after {e.timeout}s: {" ".join(shlex.quote(str(c)) for c in cmd)}'
|
||||
stderr = (e.stderr or '') + ('\n' if e.stderr else '') + msg
|
||||
return subprocess.CompletedProcess(cmd, 124, stdout=(e.stdout or ''), stderr=stderr)
|
||||
|
||||
|
||||
def symlink_deps(main_root, worktree_dir):
|
||||
"""Symlink dependency directories (fetched by tools/get_deps.py) from the main
|
||||
checkout into the temporary worktree. Without this, the base build fails because
|
||||
the worktree doesn't have the untracked deps."""
|
||||
def link_subdirs(rel_parent):
|
||||
src_parent = os.path.join(main_root, rel_parent)
|
||||
dst_parent = os.path.join(worktree_dir, rel_parent)
|
||||
if not os.path.isdir(src_parent):
|
||||
return
|
||||
os.makedirs(dst_parent, exist_ok=True)
|
||||
for entry in os.listdir(src_parent):
|
||||
src = os.path.join(src_parent, entry)
|
||||
dst = os.path.join(dst_parent, entry)
|
||||
if os.path.isdir(src) and not os.path.exists(dst):
|
||||
os.symlink(src, dst)
|
||||
|
||||
# lib/* and tools/* deps (e.g. lib/lwip, tools/linkermap)
|
||||
link_subdirs('lib')
|
||||
link_subdirs('tools')
|
||||
# hw/mcu/<vendor>/<dep> (e.g. hw/mcu/raspberry_pi/Pico-PIO-USB)
|
||||
hw_mcu = os.path.join(main_root, 'hw', 'mcu')
|
||||
if os.path.isdir(hw_mcu):
|
||||
for vendor in os.listdir(hw_mcu):
|
||||
link_subdirs(os.path.join('hw', 'mcu', vendor))
|
||||
|
||||
|
||||
def ci_first_boards():
|
||||
"""Return the first board (alphabetical) of each arm-gcc CI family."""
|
||||
matrix_py = os.path.join(TINYUSB_ROOT, '.github', 'workflows', 'ci_set_matrix.py')
|
||||
if not os.path.isfile(matrix_py):
|
||||
return []
|
||||
ret = run([sys.executable, matrix_py])
|
||||
if ret.returncode != 0:
|
||||
return []
|
||||
try:
|
||||
data = json.loads(ret.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
families = data.get('arm-gcc', [])
|
||||
boards = []
|
||||
bsp_root = os.path.join(TINYUSB_ROOT, 'hw', 'bsp')
|
||||
for family in families:
|
||||
family_boards = sorted(
|
||||
d for d in os.listdir(os.path.join(bsp_root, family, 'boards'))
|
||||
if os.path.isdir(os.path.join(bsp_root, family, 'boards', d))
|
||||
) if os.path.isdir(os.path.join(bsp_root, family, 'boards')) else []
|
||||
if family_boards:
|
||||
boards.append(family_boards[0])
|
||||
return boards
|
||||
|
||||
|
||||
def build_board(src_dir, build_dir, board, example=None):
|
||||
"""Configure and build examples for a board. Returns True on success.
|
||||
|
||||
When `example` is given, only that target is built (`cmake --build --target NAME`),
|
||||
keeping single-example workflows fast.
|
||||
"""
|
||||
os.makedirs(build_dir, exist_ok=True)
|
||||
ret = run(['cmake', '-B', build_dir, '-G', 'Ninja',
|
||||
f'-DBOARD={board}', '-DCMAKE_BUILD_TYPE=MinSizeRel',
|
||||
os.path.join(src_dir, 'examples')])
|
||||
if ret.returncode != 0:
|
||||
print(f' Error configuring {board}: {ret.stderr}')
|
||||
return False
|
||||
cmd = ['cmake', '--build', build_dir]
|
||||
if example:
|
||||
cmd += ['--target', os.path.basename(example)]
|
||||
ret = run(cmd, timeout=600)
|
||||
if ret.returncode != 0:
|
||||
print(f' Error building {board}: {ret.stderr}')
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def generate_metrics(build_dir, out_basename, filters, example=None):
|
||||
"""Run metrics.py combine on .map.json files. Returns metrics json path or None.
|
||||
|
||||
`filters` is a list of substrings; metrics.py keeps a compile unit if its path
|
||||
contains any of them.
|
||||
"""
|
||||
if example:
|
||||
patterns = glob.glob(f'{build_dir}/{example}/*.map.json')
|
||||
else:
|
||||
patterns = glob.glob(f'{build_dir}/**/*.map.json', recursive=True)
|
||||
if not patterns:
|
||||
print(f' Error: no .map.json files in {build_dir}' + (f' for {example}' if example else ''))
|
||||
return None
|
||||
|
||||
metrics_py = os.path.join(TINYUSB_ROOT, 'tools', 'metrics.py')
|
||||
cmd = [sys.executable, metrics_py, 'combine']
|
||||
for f in filters:
|
||||
cmd += ['-f', f]
|
||||
cmd += ['-j', '-q', '-o', out_basename, *patterns]
|
||||
ret = run(cmd)
|
||||
if ret.returncode != 0:
|
||||
print(f' Error: {ret.stderr}')
|
||||
return None
|
||||
return f'{out_basename}.json'
|
||||
|
||||
|
||||
def main():
|
||||
global verbose
|
||||
|
||||
parser = argparse.ArgumentParser(description='Compare code size metrics with base branch')
|
||||
parser.add_argument('-b', '--board', action='append', default=[],
|
||||
help='Board name (repeatable). Required unless --ci is given.')
|
||||
parser.add_argument('-f', '--filter', action='append', default=None,
|
||||
help='Path-substring filter (repeatable). When given, '
|
||||
'overrides the default and is applied to BOTH base and '
|
||||
'current builds. Default: each side\'s own absolute '
|
||||
'<checkout>/src/ path, which uniquely matches TinyUSB '
|
||||
'stack code without colliding with vendored deps.')
|
||||
parser.add_argument('--base-branch', default='master',
|
||||
help='Base branch to compare against (default: master)')
|
||||
parser.add_argument('-e', '--example', action='append', default=None,
|
||||
help='Compare specific example (repeatable, e.g. -e device/cdc_msc -e host/cdc_msc_hid)')
|
||||
parser.add_argument('--bloaty', action='store_true',
|
||||
help='Use bloaty for detailed section/symbol diff (requires -e)')
|
||||
parser.add_argument('--ci', action='store_true',
|
||||
help='Add the first board of every arm-gcc CI family. Implies --combined.')
|
||||
parser.add_argument('--combined', action='store_true',
|
||||
help='Aggregate map.json files across all boards into one comparison '
|
||||
'(in cmake-metrics/_combined/), instead of (or in addition to) per-board.')
|
||||
parser.add_argument('-v', '--verbose', action='store_true',
|
||||
help='Print build commands')
|
||||
args = parser.parse_args()
|
||||
verbose = args.verbose
|
||||
|
||||
if args.bloaty and not args.example:
|
||||
parser.error('--bloaty requires -e/--example')
|
||||
|
||||
if args.ci:
|
||||
args.combined = True
|
||||
ci_boards = ci_first_boards()
|
||||
if not ci_boards:
|
||||
parser.error('--ci: failed to derive boards from .github/workflows/ci_set_matrix.py')
|
||||
# Append, dedup, preserve order
|
||||
seen = set(args.board)
|
||||
for b in ci_boards:
|
||||
if b not in seen:
|
||||
args.board.append(b)
|
||||
seen.add(b)
|
||||
|
||||
if not args.board:
|
||||
parser.error('at least one -b BOARD is required (or pass --ci)')
|
||||
|
||||
metrics_py = os.path.join(TINYUSB_ROOT, 'tools', 'metrics.py')
|
||||
worktree_dir = os.path.join(METRICS_DIR, '_worktree')
|
||||
|
||||
# Per-side filters: when no override is given, each build uses its own
|
||||
# absolute <checkout>/src/ path so we only match TinyUSB stack code from that
|
||||
# checkout (and never vendored-dep `src/` like pico-sdk/src/...).
|
||||
if args.filter:
|
||||
base_filters = cur_filters = list(args.filter)
|
||||
else:
|
||||
base_filters = [tinyusb_src_filter(worktree_dir)]
|
||||
cur_filters = [tinyusb_src_filter(TINYUSB_ROOT)]
|
||||
|
||||
# Step 1: Create worktree for base branch
|
||||
print(f'[1/5] Setting up {args.base_branch} worktree...')
|
||||
if os.path.isdir(worktree_dir):
|
||||
run(['git', '-C', TINYUSB_ROOT, 'worktree', 'remove', '--force', worktree_dir])
|
||||
# --detach: check out the ref at a detached HEAD instead of trying to claim the
|
||||
# branch. Lets us add a worktree of `master` even if master is already checked
|
||||
# out elsewhere (main repo, another worktree).
|
||||
ret = run(['git', '-C', TINYUSB_ROOT, 'worktree', 'add', '--detach',
|
||||
worktree_dir, args.base_branch])
|
||||
if ret.returncode != 0:
|
||||
print(f'Error creating worktree: {ret.stderr}')
|
||||
sys.exit(1)
|
||||
|
||||
# Symlink dependency dirs (lib/*, hw/mcu/*/*, tools/*) so the worktree builds.
|
||||
symlink_deps(TINYUSB_ROOT, worktree_dir)
|
||||
|
||||
try:
|
||||
examples = args.example or [None]
|
||||
# For --combined: track every (base_build, cur_build) pair so we can aggregate at the end.
|
||||
built_pairs = []
|
||||
|
||||
for board in args.board:
|
||||
print(f'\n=== {board} ===')
|
||||
board_dir = os.path.join(METRICS_DIR, board)
|
||||
base_build = os.path.join(board_dir, 'base')
|
||||
cur_build = os.path.join(board_dir, 'build')
|
||||
|
||||
# Build only the requested examples (or all if -e not given). Single-example
|
||||
# mode used to build everything and filter at metric time — that was wasted work.
|
||||
board_failed = False
|
||||
for example in examples:
|
||||
build_label = f' --target {os.path.basename(example)}' if example else ''
|
||||
print(f'[2/5] Building {args.base_branch} for {board}{build_label}...')
|
||||
if not build_board(worktree_dir, base_build, board, example):
|
||||
board_failed = True
|
||||
break
|
||||
print(f'[3/5] Building current for {board}{build_label}...')
|
||||
if not build_board(TINYUSB_ROOT, cur_build, board, example):
|
||||
board_failed = True
|
||||
break
|
||||
if board_failed:
|
||||
continue
|
||||
|
||||
built_pairs.append((board, base_build, cur_build))
|
||||
|
||||
for example in examples:
|
||||
suffix = f'_{example.replace("/", "_")}' if example else ''
|
||||
label = f' ({example})' if example else ''
|
||||
|
||||
# Step 4: Generate metrics
|
||||
print(f'[4/5] Generating metrics for {board}{label}...')
|
||||
base_json = generate_metrics(base_build, os.path.join(board_dir, f'base_metrics{suffix}'),
|
||||
base_filters, example)
|
||||
cur_json = generate_metrics(cur_build, os.path.join(board_dir, f'build_metrics{suffix}'),
|
||||
cur_filters, example)
|
||||
if not base_json or not cur_json:
|
||||
continue
|
||||
|
||||
# Step 5: Compare
|
||||
out_base = os.path.join(board_dir, f'metrics_compare{suffix}')
|
||||
print(f'[5/5] Comparing {board}{label}...')
|
||||
ret = run([sys.executable, metrics_py, 'compare', '-m', '-o', out_base, base_json, cur_json])
|
||||
print(ret.stdout)
|
||||
|
||||
# Optional: bloaty diff
|
||||
if args.bloaty and example:
|
||||
elf_name = os.path.basename(example)
|
||||
base_elf = os.path.join(base_build, example, f'{elf_name}.elf')
|
||||
cur_elf = os.path.join(cur_build, example, f'{elf_name}.elf')
|
||||
if os.path.exists(base_elf) and os.path.exists(cur_elf):
|
||||
# Bloaty expects one regex; OR-join all filters (current side
|
||||
# for the new ELF, base side for the base ELF).
|
||||
bloaty_regex = '(' + '|'.join(
|
||||
re.escape(f) for f in (cur_filters + base_filters)
|
||||
) + ')'
|
||||
bloaty_common = ['bloaty', '--domain=vm', f'--source-filter={bloaty_regex}']
|
||||
print(f'--- bloaty sections ---')
|
||||
ret = run(bloaty_common + ['-d', 'compileunits,sections', cur_elf, '--', base_elf])
|
||||
print(ret.stdout)
|
||||
print(f'--- bloaty symbols ---')
|
||||
ret = run(bloaty_common + ['-d', 'compileunits,symbols', '-s', 'vm',
|
||||
cur_elf, '--', base_elf])
|
||||
print(ret.stdout)
|
||||
else:
|
||||
print(f' bloaty: ELF not found')
|
||||
|
||||
# Optional combined comparison across all boards.
|
||||
# Aggregates the per-board metrics JSONs (not raw map.json globs) so the argv
|
||||
# stays small even with --ci spanning many boards.
|
||||
if args.combined and built_pairs:
|
||||
combined_dir = os.path.join(METRICS_DIR, '_combined')
|
||||
os.makedirs(combined_dir, exist_ok=True)
|
||||
|
||||
# Use the no-suffix per-board JSONs (whole-board metrics). Combined mode
|
||||
# is meant for board-level sweeps; -e/--example combinations skip combined.
|
||||
base_jsons, cur_jsons = [], []
|
||||
for board, _, _ in built_pairs:
|
||||
bj = os.path.join(METRICS_DIR, board, 'base_metrics.json')
|
||||
cj = os.path.join(METRICS_DIR, board, 'build_metrics.json')
|
||||
if os.path.isfile(bj) and os.path.isfile(cj):
|
||||
base_jsons.append(bj)
|
||||
cur_jsons.append(cj)
|
||||
|
||||
if not base_jsons or not cur_jsons:
|
||||
print(' combined: no per-board metrics found (did you pass -e? skip --combined with -e)')
|
||||
else:
|
||||
print(f'\n=== combined ({len(base_jsons)} boards) ===')
|
||||
base_out = os.path.join(combined_dir, 'base_metrics')
|
||||
cur_out = os.path.join(combined_dir, 'build_metrics')
|
||||
|
||||
# Per-board JSONs are already filtered to TinyUSB-only files; combine
|
||||
# without re-filtering so we don't accidentally drop entries.
|
||||
def _combine(out_basename, inputs):
|
||||
cmd = [sys.executable, metrics_py, 'combine',
|
||||
'-j', '-q', '-o', out_basename, *inputs]
|
||||
return run(cmd)
|
||||
|
||||
ret = _combine(base_out, base_jsons)
|
||||
if ret.returncode != 0:
|
||||
print(f' combined base error: {ret.stderr}')
|
||||
else:
|
||||
ret = _combine(cur_out, cur_jsons)
|
||||
if ret.returncode != 0:
|
||||
print(f' combined current error: {ret.stderr}')
|
||||
else:
|
||||
out_combined = os.path.join(combined_dir, 'metrics_compare')
|
||||
ret = run([sys.executable, metrics_py, 'compare', '-m',
|
||||
'-o', out_combined, f'{base_out}.json', f'{cur_out}.json'])
|
||||
print(ret.stdout)
|
||||
print(f' combined report: {out_combined}.md')
|
||||
finally:
|
||||
print(f'\nCleaning up worktree...')
|
||||
run(['git', '-C', TINYUSB_ROOT, 'worktree', 'remove', '--force', worktree_dir])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user