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