| #!/usr/bin/env python3 |
| # SPDX-License-Identifier: Apache-2.0 |
| # ----------------------------------------------------------------------------- |
| # Copyright 2019-2020 Arm Limited |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy |
| # of the License at: |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations |
| # under the License. |
| # ----------------------------------------------------------------------------- |
| """ |
| The ``astc_size_binary`` utility provides a wrapper around the Linux ``size`` |
| utility to view binary section sizes, and optionally compare the section sizes |
| of two binaries. Section sizes are given for code (``.text``), read-only data |
| (``.rodata``), and zero initialized data (``.bss``). All other sections are |
| ignored. |
| |
| A typical report comparing the size of a new binary against a reference looks |
| like this: |
| |
| .. code-block:: |
| |
| Code RO Data ZI Data |
| Ref 411298 374560 128576 |
| New 560530 89552 31744 |
| Abs D 149232 -285008 -96832 |
| Rel D 36.28% -76.09% -75.31% |
| """ |
| |
| |
| import argparse |
| import platform |
| import shutil |
| import subprocess as sp |
| import sys |
| |
| |
| def run_size_linux(binary): |
| """ |
| Run size on a single binary. |
| |
| Args: |
| binary (str): The path of the binary file to process. |
| |
| Returns: |
| tuple(int, int, int): A triplet of code size, read-only data size, and |
| zero-init data size, all in bytes. |
| |
| Raises: |
| CalledProcessException: The ``size`` subprocess failed for any reason. |
| """ |
| args = ["size", "--format=sysv", binary] |
| result = sp.run(args, stdout=sp.PIPE, stderr=sp.PIPE, |
| check=True, universal_newlines=True) |
| |
| data = {} |
| patterns = {"Code": ".text", "RO": ".rodata", "ZI": ".bss"} |
| |
| lines = result.stdout.splitlines() |
| for line in lines: |
| for key, value in patterns.items(): |
| if line.startswith(value): |
| size = float(line.split()[1]) |
| data[key] = size |
| |
| return (data["Code"], data["RO"], data["ZI"]) |
| |
| |
| def run_size_macos(binary): |
| """ |
| Run size on a single binary. |
| |
| Args: |
| binary (str): The path of the binary file to process. |
| |
| Returns: |
| tuple(int, int, int): A triplet of code size, read-only data size, and |
| zero-init data size, all in bytes. |
| |
| Raises: |
| CalledProcessException: The ``size`` subprocess failed for any reason. |
| """ |
| args = ["size", "-m", binary] |
| result = sp.run(args, stdout=sp.PIPE, stderr=sp.PIPE, |
| check=True, universal_newlines=True) |
| |
| code = 0 |
| dataRO = 0 |
| dataZI = 0 |
| |
| currentSegment = None |
| |
| lines = result.stdout.splitlines() |
| for line in lines: |
| line = line.strip() |
| |
| if line.startswith("Segment"): |
| parts = line.split() |
| assert(len(parts) == 3) |
| |
| currentSegment = parts[1] |
| size = int(parts[2]) |
| |
| if currentSegment == "__TEXT:": |
| code += size |
| |
| if currentSegment == "__DATA_CONST:": |
| dataRO += size |
| |
| if currentSegment == "__DATA:": |
| dataZI += size |
| |
| if line.startswith("Section"): |
| parts = line.split() |
| assert(len(parts) == 3) |
| |
| section = parts[1] |
| size = int(parts[2]) |
| |
| if currentSegment == "__TEXT:" and section == "__const:": |
| code -= size |
| dataRO += size |
| |
| return (code, dataRO, dataZI) |
| |
| |
| def parse_command_line(): |
| """ |
| Parse the command line. |
| |
| Returns: |
| Namespace: The parsed command line container. |
| """ |
| parser = argparse.ArgumentParser() |
| |
| parser.add_argument("bin", type=argparse.FileType("r"), |
| help="The new binary to size") |
| |
| parser.add_argument("ref", nargs="?", type=argparse.FileType("r"), |
| help="The reference binary to compare against") |
| |
| return parser.parse_args() |
| |
| |
| def main(): |
| """ |
| The main function. |
| |
| Returns: |
| int: The process return code. |
| """ |
| args = parse_command_line() |
| |
| # Preflight - check that size exists. Note that size might still fail at |
| # runtime later, e.g. if the binary is not of the correct format |
| path = shutil.which("size") |
| if not path: |
| print("ERROR: The 'size' utility is not installed on the PATH") |
| return 1 |
| |
| if platform.system() == "Darwin": |
| run_size = run_size_macos |
| else: |
| run_size = run_size_linux |
| |
| # Collect the data |
| try: |
| newSize = run_size(args.bin.name) |
| if args.ref: |
| refSize = run_size(args.ref.name) |
| except sp.CalledProcessError as ex: |
| print("ERROR: The 'size' utility failed") |
| print(" %s" % ex.stderr.strip()) |
| return 1 |
| |
| # Print the basic table of absolute values |
| print("%8s % 8s % 8s % 8s" % ("", "Code", "RO Data", "ZI Data")) |
| if args.ref: |
| print("%8s % 8u % 8u % 8u" % ("Ref", *refSize)) |
| print("%8s % 8u % 8u % 8u" % ("New", *newSize)) |
| |
| # Print the difference if we have a reference |
| if args.ref: |
| diffAbs = [] |
| diffRel = [] |
| for refVal, newVal in zip(refSize, newSize): |
| diff = newVal - refVal |
| diffAbs.append(diff) |
| diffRel.append((diff / refVal) * 100.0) |
| |
| dat = ("Abs D", diffAbs[0], diffAbs[1], diffAbs[2]) |
| print("%8s % 8u % 8u % 8u" % dat) |
| dat = ("Rel D", diffRel[0], diffRel[1], diffRel[2]) |
| print("%8s % 7.2f%% % 7.2f%% % 7.2f%%" % dat) |
| |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |