1#!/usr/bin/env python3 2# 3# This file is part of the MicroPython project, http://micropython.org/ 4# 5# The MIT License (MIT) 6# 7# Copyright (c) 2020 Damien P. George 8# 9# Permission is hereby granted, free of charge, to any person obtaining a copy 10# of this software and associated documentation files (the "Software"), to deal 11# in the Software without restriction, including without limitation the rights 12# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13# copies of the Software, and to permit persons to whom the Software is 14# furnished to do so, subject to the following conditions: 15# 16# The above copyright notice and this permission notice shall be included in 17# all copies or substantial portions of the Software. 18# 19# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25# THE SOFTWARE. 26 27""" 28This script is used to compute metrics, like code size, of the various ports. 29 30Typical usage is: 31 32 $ ./tools/metrics.py build | tee size0 33 <wait for build to complete> 34 $ git switch new-feature-branch 35 $ ./tools/metrics.py build | tee size1 36 <wait for build to complete> 37 $ ./tools/metrics.py diff size0 size1 38 39Other commands: 40 41 $ ./tools/metrics.py sizes # print all firmware sizes 42 $ ./tools/metrics.py clean # clean all ports 43 44""" 45 46import collections, sys, re, subprocess 47 48MAKE_FLAGS = ["-j3", "CFLAGS_EXTRA=-DNDEBUG"] 49 50 51class PortData: 52 def __init__(self, name, dir, output, make_flags=None): 53 self.name = name 54 self.dir = dir 55 self.output = output 56 self.make_flags = make_flags 57 self.needs_mpy_cross = dir not in ("bare-arm", "minimal") 58 59 60port_data = { 61 "b": PortData("bare-arm", "bare-arm", "build/firmware.elf"), 62 "m": PortData("minimal x86", "minimal", "build/firmware.elf"), 63 "u": PortData("unix x64", "unix", "micropython"), 64 "n": PortData("unix nanbox", "unix", "micropython-nanbox", "VARIANT=nanbox"), 65 "s": PortData("stm32", "stm32", "build-PYBV10/firmware.elf", "BOARD=PYBV10"), 66 "c": PortData("cc3200", "cc3200", "build/WIPY/release/application.axf", "BTARGET=application"), 67 "8": PortData("esp8266", "esp8266", "build-GENERIC/firmware.elf"), 68 "3": PortData("esp32", "esp32", "build-GENERIC/micropython.elf"), 69 "r": PortData("nrf", "nrf", "build-pca10040/firmware.elf"), 70 "p": PortData("rp2", "rp2", "build-PICO/firmware.elf"), 71 "d": PortData("samd", "samd", "build-ADAFRUIT_ITSYBITSY_M4_EXPRESS/firmware.elf"), 72} 73 74 75def syscmd(*args): 76 sys.stdout.flush() 77 a2 = [] 78 for a in args: 79 if isinstance(a, str): 80 a2.append(a) 81 elif a: 82 a2.extend(a) 83 subprocess.check_call(a2) 84 85 86def parse_port_list(args): 87 if not args: 88 return list(port_data.values()) 89 else: 90 ports = [] 91 for arg in args: 92 for port_char in arg: 93 try: 94 ports.append(port_data[port_char]) 95 except KeyError: 96 print("unknown port:", port_char) 97 sys.exit(1) 98 return ports 99 100 101def read_build_log(filename): 102 data = collections.OrderedDict() 103 lines = [] 104 found_sizes = False 105 with open(filename) as f: 106 for line in f: 107 line = line.strip() 108 if line.strip() == "COMPUTING SIZES": 109 found_sizes = True 110 elif found_sizes: 111 lines.append(line) 112 is_size_line = False 113 for line in lines: 114 if is_size_line: 115 fields = line.split() 116 data[fields[-1]] = [int(f) for f in fields[:-2]] 117 is_size_line = False 118 else: 119 is_size_line = line.startswith("text\t ") 120 return data 121 122 123def do_diff(args): 124 """Compute the difference between firmware sizes.""" 125 126 # Parse arguments. 127 error_threshold = None 128 if len(args) >= 2 and args[0] == "--error-threshold": 129 args.pop(0) 130 error_threshold = int(args.pop(0)) 131 132 if len(args) != 2: 133 print("usage: %s diff [--error-threshold <x>] <out1> <out2>" % sys.argv[0]) 134 sys.exit(1) 135 136 data1 = read_build_log(args[0]) 137 data2 = read_build_log(args[1]) 138 139 max_delta = None 140 for key, value1 in data1.items(): 141 value2 = data2[key] 142 for port in port_data.values(): 143 if key == "ports/{}/{}".format(port.dir, port.output): 144 name = port.name 145 break 146 data = [v2 - v1 for v1, v2 in zip(value1, value2)] 147 warn = "" 148 board = re.search(r"/build-([A-Za-z0-9_]+)/", key) 149 if board: 150 board = board.group(1) 151 else: 152 board = "" 153 if name == "cc3200": 154 delta = data[0] 155 percent = 100 * delta / value1[0] 156 if data[1] != 0: 157 warn += " %+u(data)" % data[1] 158 else: 159 delta = data[3] 160 percent = 100 * delta / value1[3] 161 if data[1] != 0: 162 warn += " %+u(data)" % data[1] 163 if data[2] != 0: 164 warn += " %+u(bss)" % data[2] 165 if warn: 166 warn = "[incl%s]" % warn 167 print("%11s: %+5u %+.3f%% %s%s" % (name, delta, percent, board, warn)) 168 max_delta = delta if max_delta is None else max(max_delta, delta) 169 170 if error_threshold is not None and max_delta is not None: 171 if max_delta > error_threshold: 172 sys.exit(1) 173 174 175def do_clean(args): 176 """Clean ports.""" 177 178 ports = parse_port_list(args) 179 180 print("CLEANING") 181 for port in ports: 182 syscmd("make", "-C", "ports/{}".format(port.dir), port.make_flags, "clean") 183 184 185def do_build(args): 186 """Build ports and print firmware sizes.""" 187 188 ports = parse_port_list(args) 189 190 if any(port.needs_mpy_cross for port in ports): 191 print("BUILDING MPY-CROSS") 192 syscmd("make", "-C", "mpy-cross", MAKE_FLAGS) 193 194 print("BUILDING PORTS") 195 for port in ports: 196 syscmd("make", "-C", "ports/{}".format(port.dir), MAKE_FLAGS, port.make_flags) 197 198 do_sizes(args) 199 200 201def do_sizes(args): 202 """Compute and print sizes of firmware.""" 203 204 ports = parse_port_list(args) 205 206 print("COMPUTING SIZES") 207 for port in ports: 208 syscmd("size", "ports/{}/{}".format(port.dir, port.output)) 209 210 211def main(): 212 # Get command to execute 213 if len(sys.argv) == 1: 214 print("Available commands:") 215 for cmd in globals(): 216 if cmd.startswith("do_"): 217 print(" {:9} {}".format(cmd[3:], globals()[cmd].__doc__)) 218 sys.exit(1) 219 cmd = sys.argv.pop(1) 220 221 # Dispatch to desired command 222 try: 223 cmd = globals()["do_{}".format(cmd)] 224 except KeyError: 225 print("{}: unknown command '{}'".format(sys.argv[0], cmd)) 226 sys.exit(1) 227 cmd(sys.argv[1:]) 228 229 230if __name__ == "__main__": 231 main() 232