| #!/usr/bin/env python3 |
| # 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. |
| |
| # pyre-strict |
| |
| """ |
| Compare binary file sizes. Used by the Skycastle workflow to ensure no change adds excessive size to executorch. |
| |
| Usage: file_size_compare.py [-h] --compare-file FILE [--base-file FILE] [-s, --max-size SIZE] [-e, --error-size SIZE] [-w, --warning-size SIZE] |
| |
| Exit Codes: |
| 0 - OK |
| 1 - Comparison yielded a warning |
| 2 - Comparison yielded an error |
| 3 - Script errored while executing |
| """ |
| |
| import argparse |
| import os |
| import sys |
| from pathlib import Path |
| |
| # Exit codes. |
| EXIT_OK = 0 |
| EXIT_WARNING = 1 |
| EXIT_ERROR = 2 |
| EXIT_SCRIPT_ERROR = 3 |
| |
| # TTY ANSI color codes. |
| TTY_GREEN = "\033[0;32m" |
| TTY_RED = "\033[0;31m" |
| TTY_RESET = "\033[0m" |
| |
| # Error message printed if size is exceeded. |
| SIZE_ERROR_MESSAGE = """This diff is increasing the binary size of ExecuTorch (the PyTorch Edge model executor) by a large amount. |
| ExecuTorch has strict size requirements due to its embedded use case. Please follow these steps: |
| 1. Check the output of the two steps (Build ... with the base commit/diff version) and compare their executable section sizes. |
| 2. Contact a member of #pytorch_edge_portability so we can better help you. |
| """ |
| |
| |
| def create_file_path(file_name: str) -> Path: |
| """Create Path object from file name string.""" |
| file_path = Path(file_name) |
| if not file_path.is_file(): |
| print(f"{file_path} is not a valid file path") |
| sys.exit(EXIT_SCRIPT_ERROR) |
| return file_path |
| |
| |
| def get_file_size(file_path: Path) -> int: |
| """Get the size of a file on disk.""" |
| return os.path.getsize(file_path) |
| |
| |
| def print_ansi(ansi_code: str) -> None: |
| """Print an ANSI escape code.""" |
| if sys.stdout.isatty(): |
| print(ansi_code, end="") |
| |
| |
| def print_size_diff(compare_file: str, base_file: str, delta: int) -> None: |
| """Print the size difference.""" |
| if delta > 0: |
| print(f"{compare_file} is {delta} bytes bigger than {base_file}.") |
| else: |
| print_ansi(TTY_GREEN) |
| print(f"{compare_file} is {abs(delta)} bytes SMALLER than {base_file}. Great!") |
| print_ansi(TTY_RESET) |
| |
| |
| def print_size_error() -> None: |
| """Print an error message for excessive size.""" |
| print_ansi(TTY_RED) |
| print(SIZE_ERROR_MESSAGE) |
| print_ansi(TTY_RESET) |
| |
| |
| def compare_against_base( |
| base_file: str, compare_file: str, warning_size: int, error_size: int |
| ) -> int: |
| """Compare test binary file size against base revision binary file size.""" |
| base_file = create_file_path(base_file) |
| compare_file = create_file_path(compare_file) |
| |
| diff = get_file_size(compare_file) - get_file_size(base_file) |
| print_size_diff(compare_file.name, base_file.name, diff) |
| |
| if diff >= error_size: |
| print_size_error() |
| return EXIT_ERROR |
| elif diff >= warning_size: |
| return EXIT_WARNING |
| else: |
| return EXIT_OK |
| |
| |
| def compare_against_max(compare_file: str, max_size: int) -> int: |
| """Compare test binary file size against maximum value.""" |
| compare_file = create_file_path(compare_file) |
| |
| diff = get_file_size(compare_file) - max_size |
| print_size_diff(compare_file.name, "specified max size", diff) |
| |
| if diff > 0: |
| print_size_error() |
| return EXIT_ERROR |
| else: |
| return EXIT_OK |
| |
| |
| def main() -> int: |
| # Parse arguments. |
| parser = argparse.ArgumentParser(description="Compare binary file size") |
| parser.add_argument( |
| "--compare-file", |
| metavar="FILE", |
| type=str, |
| required=True, |
| help="Binary to compare against size args or base revision binary", |
| ) |
| parser.add_argument( |
| "--base-file", |
| metavar="FILE", |
| type=str, |
| help="Base revision binary", |
| dest="base_file", |
| ) |
| parser.add_argument( |
| "-s, --max-size", |
| metavar="SIZE", |
| type=int, |
| help="Max size of the binary, in bytes", |
| dest="max_size", |
| ) |
| parser.add_argument( |
| "-e, --error-size", |
| metavar="SIZE", |
| type=int, |
| help="Size difference between binaries constituting an error, in bytes", |
| dest="error_size", |
| ) |
| parser.add_argument( |
| "-w, --warning-size", |
| metavar="SIZE", |
| type=int, |
| help="Size difference between binaries constituting a warning, in bytes", |
| dest="warning_size", |
| ) |
| |
| args = parser.parse_args() |
| |
| if args.base_file is not None: |
| if args.max_size is not None: |
| print("Cannot specify both base file and maximum size arguments.") |
| sys.exit(EXIT_SCRIPT_ERROR) |
| |
| if args.error_size is None or args.warning_size is None: |
| print( |
| "When comparing against base revision, error and warning sizes must be specified." |
| ) |
| sys.exit(EXIT_SCRIPT_ERROR) |
| |
| return compare_against_base( |
| args.base_file, args.compare_file, args.warning_size, args.error_size |
| ) |
| elif args.max_size is not None: |
| return compare_against_max(args.compare_file, args.max_size) |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |