From c77ef7f96a187b9ddda51b1971801a1de89200d9 Mon Sep 17 00:00:00 2001 From: iabdalkader Date: Wed, 24 Jul 2024 11:47:05 +0300 Subject: [PATCH] misc/github: Post firmware stats after PRs. --- .github/workflows/firmware.yml | 61 +++++++++++++++- .github/workflows/requirements.txt | 1 + tools/gen_stats.py | 108 +++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 tools/gen_stats.py diff --git a/.github/workflows/firmware.yml b/.github/workflows/firmware.yml index b193f4945..63c882025 100644 --- a/.github/workflows/firmware.yml +++ b/.github/workflows/firmware.yml @@ -65,13 +65,72 @@ jobs: run: source tools/ci.sh && ci_build_target ${{ matrix.target }} - name: '⬆ Upload artifacts' - if: github.event_name != 'pull_request' uses: actions/upload-artifact@v4 with: name: ${{ matrix.target }} path: ${{ matrix.target }} if-no-files-found: error + post-stats: + needs: build-firmware + runs-on: ubuntu-22.04 + if: github.event_name == 'pull_request' + steps: + - name: '⏳ Checkout repository' + uses: actions/checkout@v4 + with: + submodules: false + + - name: '♻ Caching dependencies' + uses: actions/cache@v4.0.2 + id: cache + with: + path: ~/cache/gcc + key: 'arm-gnu-toolchain-13.2.rel1' + + - name: '🐍 Install Python' + uses: actions/setup-python@v5 + with: + cache: 'pip' + python-version: "3.12.4" + + - name: '🛠 Install dependencies' + run: | + pip install tabulate==0.9.0 + + - name: '🤌 Download artifacts' + uses: actions/download-artifact@v4 + with: + path: new_firmware + merge-multiple: false + + - name: '🤌 Download release' + uses: robinraju/release-downloader@v1 + with: + repository: 'openmv/openmv' + tag: 'development' + fileName: '*.zip' + extract: false + out-file-path: 'old_firmware' + + - name: '📝 Generate report' + run: | + TOOLCHAIN_PATH=${HOME}/cache/gcc + export PATH=${TOOLCHAIN_PATH}/bin:${PATH} + python tools/gen_stats.py > ${GITHUB_WORKSPACE}/stats.txt + cat ${GITHUB_WORKSPACE}/stats.txt + if [ -s ./stats.txt ]; then + echo "report=true" >> $GITHUB_ENV + else + echo "report=false" >> $GITHUB_ENV + fi + + - name: '📝 Post report' + if: env.report == 'true' + uses: thollander/actions-comment-pull-request@v2 + with: + filePath: ./stats.txt + stable-release: needs: build-firmware runs-on: ubuntu-22.04 diff --git a/.github/workflows/requirements.txt b/.github/workflows/requirements.txt index ae61b4afc..cc438bd8c 100644 --- a/.github/workflows/requirements.txt +++ b/.github/workflows/requirements.txt @@ -1,3 +1,4 @@ flake8==6.0.0 pytest==7.4.0 ethos-u-vela==3.12.0 +tabulate==0.9.0 diff --git a/tools/gen_stats.py b/tools/gen_stats.py new file mode 100644 index 000000000..3c54a19f4 --- /dev/null +++ b/tools/gen_stats.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# This file is part of the OpenMV project. +# +# Copyright (c) 2024 Ibrahim Abdelkader +# Copyright (c) 2024 Kwabena W. Agyeman +# +# This work is licensed under the MIT license, see the file LICENSE for details. +import os +import sys +import zipfile +import subprocess +from operator import itemgetter +from tabulate import tabulate + + +def extract_zip_files(directory): + for item in os.listdir(directory): + if item.endswith(".zip"): + with zipfile.ZipFile(os.path.join(directory, item), "r") as zip_ref: + name = os.path.splitext(item.strip())[0].split("firmware_")[1] + extract_directory = os.path.join(directory, name) + if not os.path.exists(extract_directory): + os.makedirs(extract_directory) + zip_ref.extractall(extract_directory) + + +def find_elf_files(directory): + elf_files = [] + for root, _, files in os.walk(directory): + for file in files: + if file.endswith(".elf"): + elf_files.append(os.path.join(root, file)) + return elf_files + + +def get_relative_path(full_path, base_dir): + return os.path.relpath(full_path, base_dir) + + +def get_firmware_sizes(path): + try: + output = subprocess.check_output(["arm-none-eabi-size", path], text=True) + except subprocess.CalledProcessError as e: + print(e.stderr, file=sys.stderr) + raise e + return [int(s) for s in output.split() if s.isdigit()][0:3] + + +def main(): + new_firmware_dir = "new_firmware" + old_firmware_dir = "old_firmware" + + # Extract zip files + extract_zip_files(old_firmware_dir) + extract_zip_files(new_firmware_dir) + + # Find ELF files and their relative paths + new_elf_files = find_elf_files(new_firmware_dir) + old_elf_files = find_elf_files(old_firmware_dir) + + new_elf_paths = { get_relative_path(path, new_firmware_dir): path for path in new_elf_files } + old_elf_paths = { get_relative_path(path, old_firmware_dir): path for path in old_elf_files } + + # Find common ELF files based on relative paths + common_paths = set(new_elf_paths.keys()).intersection(old_elf_paths.keys()) + # List of common ELF files with their paths + common_elf_files = [(new_elf_paths[rel_path], old_elf_paths[rel_path]) for rel_path in common_paths] + common_elf_files = sorted(common_elf_files, key=lambda x: x[0].split(os.sep)[1]) + + # Prepare the header + headers = ["Firmware", "Text Diff", "Data Diff", "BSS Diff"] + + # Prepare the data + data = [] + for new_path, old_path in common_elf_files: + new_sizes = get_firmware_sizes(new_path) + old_sizes = get_firmware_sizes(old_path) + + text_diff = new_sizes[0] - old_sizes[0] + data_diff = new_sizes[1] - old_sizes[1] + bss_diff = new_sizes[2] - old_sizes[2] + + if not any([text_diff, data_diff, bss_diff]): + continue + + text_percent = (((new_sizes[0] - old_sizes[0]) / old_sizes[0]) * 100 if old_sizes[0] != 0 else 0) + data_percent = (((new_sizes[1] - old_sizes[1]) / old_sizes[1]) * 100 if old_sizes[1] != 0 else 0) + bss_percent = (((new_sizes[2] - old_sizes[2]) / old_sizes[2]) * 100 if old_sizes[2] != 0 else 0) + + text_emoji = "🔺" if text_percent > 0 else "🔻" if text_percent < 0 else "➖" + data_emoji = "🔺" if data_percent > 0 else "🔻" if data_percent < 0 else "➖" + bss_emoji = "🔺" if bss_percent > 0 else "🔻" if bss_percent < 0 else "➖" + + text_diff_str = f"{text_emoji}{text_percent:.2f}% ({text_diff:+})" + data_diff_str = f"{data_emoji}{data_percent:.2f}% ({data_diff:+})" + bss_diff_str = f"{bss_emoji}{bss_percent:.2f}% ({bss_diff:+})" + + board_name = "_".join(old_path.split(os.sep)[1].split("_")[0:3]) + file_name = os.sep.join([board_name, old_path.split(os.sep)[-1]]) + data.append([file_name, text_diff_str, data_diff_str, bss_diff_str]) + + if data: + # Print the table + print("Code Size Report:") + print(tabulate(data, headers=headers, tablefmt="github", colalign=("left", "left", "left", "left"))) + +if __name__ == "__main__": + main()