1#!/usr/bin/env python3
2# SPDX-License-Identifier: Apache-2.0
3# -----------------------------------------------------------------------------
4# Copyright 2019-2020 Arm Limited
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may not
7# use this file except in compliance with the License. You may obtain a copy
8# of the License at:
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17# -----------------------------------------------------------------------------
18"""
19The ``astc_size_binary`` utility provides a wrapper around the Linux ``size``
20utility to view binary section sizes, and optionally compare the section sizes
21of two binaries. Section sizes are given for code (``.text``), read-only data
22(``.rodata``), and zero initialized data (``.bss``). All other sections are
23ignored.
24
25A typical report comparing the size of a new binary against a reference looks
26like this:
27
28.. code-block::
29
30              Code   RO Data   ZI Data
31     Ref    411298    374560    128576
32     New    560530     89552     31744
33   Abs D    149232   -285008    -96832
34   Rel D    36.28%   -76.09%   -75.31%
35"""
36
37
38import argparse
39import platform
40import shutil
41import subprocess as sp
42import sys
43
44
45def run_size_linux(binary):
46    """
47    Run size on a single binary.
48
49    Args:
50        binary (str): The path of the binary file to process.
51
52    Returns:
53        tuple(int, int, int): A triplet of code size, read-only data size, and
54        zero-init data size, all in bytes.
55
56    Raises:
57        CalledProcessException: The ``size`` subprocess failed for any reason.
58    """
59    args = ["size", "--format=sysv", binary]
60    result = sp.run(args, stdout=sp.PIPE, stderr=sp.PIPE,
61                    check=True, universal_newlines=True)
62
63    data = {}
64    patterns = {"Code": ".text", "RO": ".rodata", "ZI": ".bss"}
65
66    lines = result.stdout.splitlines()
67    for line in lines:
68        for key, value in patterns.items():
69            if line.startswith(value):
70                size = float(line.split()[1])
71                data[key] = size
72
73    return (data["Code"], data["RO"], data["ZI"])
74
75
76def run_size_macos(binary):
77    """
78    Run size on a single binary.
79
80    Args:
81        binary (str): The path of the binary file to process.
82
83    Returns:
84        tuple(int, int, int): A triplet of code size, read-only data size, and
85        zero-init data size, all in bytes.
86
87    Raises:
88        CalledProcessException: The ``size`` subprocess failed for any reason.
89    """
90    args = ["size", "-m", binary]
91    result = sp.run(args, stdout=sp.PIPE, stderr=sp.PIPE,
92                    check=True, universal_newlines=True)
93
94    code = 0
95    dataRO = 0
96    dataZI = 0
97
98    currentSegment = None
99
100    lines = result.stdout.splitlines()
101    for line in lines:
102        line = line.strip()
103
104        if line.startswith("Segment"):
105            parts = line.split()
106            assert len(parts) >= 3, parts
107
108            currentSegment = parts[1]
109            size = int(parts[2])
110
111            if currentSegment == "__TEXT:":
112                code += size
113
114            if currentSegment == "__DATA_CONST:":
115                dataRO += size
116
117            if currentSegment == "__DATA:":
118                dataZI += size
119
120        if line.startswith("Section"):
121            parts = line.split()
122            assert len(parts) >= 3, parts
123
124            section = parts[1]
125            size = int(parts[2])
126
127            if currentSegment == "__TEXT:" and section == "__const:":
128                code -= size
129                dataRO += size
130
131    return (code, dataRO, dataZI)
132
133
134def parse_command_line():
135    """
136    Parse the command line.
137
138    Returns:
139        Namespace: The parsed command line container.
140    """
141    parser = argparse.ArgumentParser()
142
143    parser.add_argument("bin", type=argparse.FileType("r"),
144                        help="The new binary to size")
145
146    parser.add_argument("ref", nargs="?", type=argparse.FileType("r"),
147                        help="The reference binary to compare against")
148
149    return parser.parse_args()
150
151
152def main():
153    """
154    The main function.
155
156    Returns:
157        int: The process return code.
158    """
159    args = parse_command_line()
160
161    # Preflight - check that size exists. Note that size might still fail at
162    # runtime later, e.g. if the binary is not of the correct format
163    path = shutil.which("size")
164    if not path:
165        print("ERROR: The 'size' utility is not installed on the PATH")
166        return 1
167
168    if platform.system() == "Darwin":
169        run_size = run_size_macos
170    else:
171        run_size = run_size_linux
172
173    # Collect the data
174    try:
175        newSize = run_size(args.bin.name)
176        if args.ref:
177            refSize = run_size(args.ref.name)
178    except sp.CalledProcessError as ex:
179        print("ERROR: The 'size' utility failed")
180        print("       %s" % ex.stderr.strip())
181        return 1
182
183    # Print the basic table of absolute values
184    print("%8s  % 8s  % 8s  % 8s" % ("", "Code", "RO Data", "ZI Data"))
185    if args.ref:
186        print("%8s  % 8u  % 8u  % 8u" % ("Ref", *refSize))
187    print("%8s  % 8u  % 8u  % 8u" % ("New", *newSize))
188
189    # Print the difference if we have a reference
190    if args.ref:
191        diffAbs = []
192        diffRel = []
193        for refVal, newVal in zip(refSize, newSize):
194            diff = newVal - refVal
195            diffAbs.append(diff)
196            diffRel.append((diff / refVal) * 100.0)
197
198        dat = ("Abs D", diffAbs[0], diffAbs[1], diffAbs[2])
199        print("%8s  % 8u  % 8u  % 8u" % dat)
200        dat = ("Rel D", diffRel[0], diffRel[1], diffRel[2])
201        print("%8s  % 7.2f%%  % 7.2f%%  % 7.2f%%" % dat)
202
203    return 0
204
205
206if __name__ == "__main__":
207    sys.exit(main())
208