diff --git a/.github/scripts/run_nm.py b/.github/scripts/run_nm.py new file mode 100644 index 00000000000..324e7666b87 --- /dev/null +++ b/.github/scripts/run_nm.py @@ -0,0 +1,171 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +import re +import subprocess +import sys +from dataclasses import dataclass +from typing import Dict, List, Optional, Union + + +@dataclass +class Symbol: + name: str + addr: int + size: int + symbol_type: str + + +class Parser: + def __init__(self, elf: str, toolchain_prefix: str = "", filter=None): + self.elf = elf + self.toolchain_prefix = toolchain_prefix + self.symbols: Dict[str, Symbol] = self._get_nm_output() + self.filter = filter + + @staticmethod + def run_nm( + elf_file_path: str, args: Optional[List[str]] = None, nm: str = "nm" + ) -> str: + """ + Run the nm command on the specified ELF file. + """ + args = [] if args is None else args + cmd = [nm] + args + [elf_file_path] + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + return result.stdout + except FileNotFoundError: + print(f"Error: 'nm' command not found. Please ensure it's installed.") + sys.exit(1) + except subprocess.CalledProcessError as e: + print(f"Error running nm on {elf_file_path}: {e}") + print(f"stderr: {e.stderr}") + sys.exit(1) + + def _get_nm_output(self) -> Dict[str, Symbol]: + args = [ + "--print-size", + "--size-sort", + "--reverse-sort", + "--demangle", + "--format=bsd", + ] + output = Parser.run_nm( + self.elf, + args, + nm=self.toolchain_prefix + "nm" if self.toolchain_prefix else "nm", + ) + lines = output.splitlines() + symbols = [] + symbol_pattern = re.compile( + r"(?P[0-9a-fA-F]+)\s+(?P[0-9a-fA-F]+)\s+(?P\w)\s+(?P.+)" + ) + + def parse_line(line: str) -> Optional[Symbol]: + + match = symbol_pattern.match(line) + if match: + addr = int(match.group("addr"), 16) + size = int(match.group("size"), 16) + type_ = match.group("type").strip().strip("\n") + name = match.group("name").strip().strip("\n") + return Symbol(name=name, addr=addr, size=size, symbol_type=type_) + return None + + for line in lines: + symbol = parse_line(line) + if symbol: + symbols.append(symbol) + + assert len(symbols) > 0, "No symbols found in nm output" + if len(symbols) != len(lines): + print( + "** Warning: Not all lines were parsed, check the output of nm. Parsed {len(symbols)} lines, given {len(lines)}" + ) + if any(symbol.size == 0 for symbol in symbols): + print("** Warning: Some symbols have zero size, check the output of nm.") + + # TODO: Populate the section and module fields from the linker map if available (-Wl,-Map=linker.map) + return {symbol.name: symbol for symbol in symbols} + + def print(self): + print(f"Elf: {self.elf}") + + def print_table(filter=None, filter_name=None): + print("\nAddress\t\tSize\tType\tName") + # Apply filter and sort symbols + symbols_to_print = { + name: sym + for name, sym in self.symbols.items() + if not filter or filter(sym) + } + sorted_symbols = sorted( + symbols_to_print.items(), key=lambda x: x[1].size, reverse=True + ) + + # Print symbols and calculate total size + size_total = 0 + for name, sym in sorted_symbols: + print(f"{hex(sym.addr)}\t\t{sym.size}\t{sym.symbol_type}\t{sym.name}") + size_total += sym.size + + # Print summary + symbol_percent = len(symbols_to_print) / len(self.symbols) * 100 + print("-----") + print(f"> Total bytes: {size_total}") + print( + f"Counted: {len(symbols_to_print)}/{len(self.symbols)}, {symbol_percent:0.2f}% (filter: '{filter_name}')" + ) + print("=====\n") + + # Print tables with different filters + def is_executorch_symbol(s): + return "executorch" in s.name or s.name.startswith("et") + + FILTER_NAME_TO_FILTER_AND_LABEL = { + "all": (None, "All"), + "executorch": (is_executorch_symbol, "ExecuTorch"), + "executorch_text": ( + lambda s: is_executorch_symbol(s) and s.symbol_type.lower() == "t", + "ExecuTorch .text", + ), + } + + filter_func, label = FILTER_NAME_TO_FILTER_AND_LABEL.get( + self.filter, FILTER_NAME_TO_FILTER_AND_LABEL["all"] + ) + print_table(filter_func, label) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Process ELF file and linker map file." + ) + parser.add_argument( + "-e", "--elf-file-path", required=True, help="Path to the ELF file" + ) + parser.add_argument( + "-f", + "--filter", + required=False, + default="all", + help="Filter symbols by pre-defined filters", + choices=["all", "executorch", "executorch_text"], + ) + parser.add_argument( + "-p", + "--toolchain-prefix", + required=False, + default="", + help="Optional toolchain prefix for nm", + ) + + args = parser.parse_args() + p = Parser(args.elf_file_path, args.toolchain_prefix, filter=args.filter) + p.print() diff --git a/.github/workflows/trunk.yml b/.github/workflows/trunk.yml index 6c4d7f8a58e..137bf3a4d3f 100644 --- a/.github/workflows/trunk.yml +++ b/.github/workflows/trunk.yml @@ -231,6 +231,60 @@ jobs: # Run arm unit tests using the simulator backends/arm/test/test_arm_baremetal.sh test_pytest_ethosu_fvp + test-arm-cortex-m-size-test: + name: test-arm-cortex-m-size-test + uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main + permissions: + id-token: write + contents: read + with: + runner: linux.2xlarge + docker-image: executorch-ubuntu-22.04-arm-sdk + submodules: 'true' + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + timeout: 90 + script: | + # The generic Linux job chooses to use base env, not the one setup by the image + CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]") + conda activate "${CONDA_ENV}" + + source .ci/scripts/utils.sh + install_executorch "--use-pt-pinned-commit" + .ci/scripts/setup-arm-baremetal-tools.sh + source examples/arm/ethos-u-scratch/setup_path.sh + + # User baremetal toolchain + arm-none-eabi-c++ --version + toolchain_cmake=examples/arm/ethos-u-setup/arm-none-eabi-gcc.cmake + toolchain_cmake=$(realpath ${toolchain_cmake}) + + # Build and test size test + bash test/build_size_test.sh "-DCMAKE_TOOLCHAIN_FILE=${toolchain_cmake} -DEXECUTORCH_BUILD_ARM_BAREMETAL=ON" + elf="cmake-out/test/size_test" + + # Dump basic info + ls -al ${elf} + arm-none-eabi-size ${elf} + + # Dump symbols + python .github/scripts/run_nm.py -e ${elf} + python .github/scripts/run_nm.py -e ${elf} -f "executorch" -p "arm-none-eabi-" + python .github/scripts/run_nm.py -e ${elf} -f "executorch_text" -p "arm-none-eabi-" + + # Add basic guard - TODO: refine this! + arm-none-eabi-strip ${elf} + output=$(ls -la ${elf}) + arr=($output) + size=${arr[4]} + threshold="102400" # 100KiB + echo "size: $size, threshold: $threshold" + if [[ "$size" -le "$threshold" ]]; then + echo "Success $size <= $threshold" + else + echo "Fail $size > $threshold" + exit 1 + fi + test-coreml-delegate: name: test-coreml-delegate uses: pytorch/test-infra/.github/workflows/macos_job.yml@main diff --git a/examples/arm/ethos-u-setup/arm-none-eabi-gcc.cmake b/examples/arm/ethos-u-setup/arm-none-eabi-gcc.cmake index 41fd50481d7..68fbf8985e9 100644 --- a/examples/arm/ethos-u-setup/arm-none-eabi-gcc.cmake +++ b/examples/arm/ethos-u-setup/arm-none-eabi-gcc.cmake @@ -97,5 +97,7 @@ add_compile_options( # -Wall -Wextra -Wcast-align -Wdouble-promotion -Wformat # -Wmissing-field-initializers -Wnull-dereference -Wredundant-decls -Wshadow # -Wswitch -Wswitch-default -Wunused -Wno-redundant-decls + -Wno-error=deprecated-declarations + -Wno-error=shift-count-overflow -Wno-psabi ) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3932f1097e1..845ed78ffda 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -26,7 +26,7 @@ set(EXECUTORCH_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..) include(${EXECUTORCH_ROOT}/tools/cmake/Utils.cmake) # Find prebuilt executorch library -find_package(executorch CONFIG REQUIRED) +find_package(executorch CONFIG REQUIRED HINTS ${CMAKE_INSTALL_PREFIX}) # Let files say "include ". set(_common_include_directories ${EXECUTORCH_ROOT}/..) diff --git a/test/build_size_test.sh b/test/build_size_test.sh index f7f9a0152d2..cae5a015280 100644 --- a/test/build_size_test.sh +++ b/test/build_size_test.sh @@ -11,13 +11,18 @@ set -e # shellcheck source=/dev/null source "$(dirname "${BASH_SOURCE[0]}")/../.ci/scripts/utils.sh" +EXTRA_BUILD_ARGS="${@:-}" # TODO(#8357): Remove -Wno-int-in-bool-context -COMMON_CXXFLAGS="-fno-exceptions -fno-rtti -Wall -Werror -Wno-int-in-bool-context" +# TODO: Replace -ET_HAVE_PREAD=0 with a CMake option. +# FileDataLoader used in the size_test breaks baremetal builds with pread when missing. +COMMON_CXXFLAGS="-fno-exceptions -fno-rtti -Wall -Werror -Wno-int-in-bool-context -DET_HAVE_PREAD=0" cmake_install_executorch_lib() { echo "Installing libexecutorch.a" clean_executorch_install_folders update_tokenizers_git_submodule + local EXTRA_BUILD_ARGS="${@}" + CXXFLAGS="$COMMON_CXXFLAGS" retry cmake -DBUCK2="$BUCK2" \ -DCMAKE_CXX_STANDARD_REQUIRED=ON \ -DCMAKE_INSTALL_PREFIX=cmake-out \ @@ -25,12 +30,16 @@ cmake_install_executorch_lib() { -DEXECUTORCH_BUILD_EXECUTOR_RUNNER=OFF \ -DOPTIMIZE_SIZE=ON \ -DPYTHON_EXECUTABLE="$PYTHON_EXECUTABLE" \ + ${EXTRA_BUILD_ARGS} \ -Bcmake-out . cmake --build cmake-out -j9 --target install --config Release } test_cmake_size_test() { - CXXFLAGS="$COMMON_CXXFLAGS" retry cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=cmake-out -Bcmake-out/test test + CXXFLAGS="$COMMON_CXXFLAGS" retry cmake -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=cmake-out \ + ${EXTRA_BUILD_ARGS} \ + -Bcmake-out/test test echo "Build size test" cmake --build cmake-out/test -j9 --config Release @@ -46,5 +55,5 @@ if [[ -z $PYTHON_EXECUTABLE ]]; then PYTHON_EXECUTABLE=python3 fi -cmake_install_executorch_lib -test_cmake_size_test +cmake_install_executorch_lib ${EXTRA_BUILD_ARGS} +test_cmake_size_test ${EXTRA_BUILD_ARGS}