blob: f21b908e80f8213200afd77074393c8f76646eed [file] [log] [blame]
#!/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())