#!/usr/bin/env python3 # # This file is part of the MicroPython project, http://micropython.org/ # # The MIT License (MIT) # # Copyright (c) 2020 Damien P. George # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ This script is used to compute metrics, like code size, of the various ports. Typical usage is: $ ./tools/metrics.py build | tee size0 $ git switch new-feature-branch $ ./tools/metrics.py build | tee size1 $ ./tools/metrics.py diff size0 size1 Other commands: $ ./tools/metrics.py sizes # print all firmware sizes $ ./tools/metrics.py clean # clean all ports """ import sys, re, subprocess MAKE_FLAGS = ['-j3', 'CFLAGS_EXTRA=-DNDEBUG'] class PortData: def __init__(self, name, dir, output, make_flags=None): self.name = name self.dir = dir self.output = output self.make_flags = make_flags port_data = { 'b': PortData('bare-arm', 'bare-arm', 'build/firmware.elf'), 'm': PortData('minimal x86', 'minimal', 'build/firmware.elf'), 'u': PortData('unix x64', 'unix', 'micropython'), 'n': PortData('unix nanbox', 'unix', 'micropython-nanbox', 'VARIANT=nanbox'), 's': PortData('stm32', 'stm32', 'build-PYBV10/firmware.elf', 'BOARD=PYBV10'), 'c': PortData('cc3200', 'cc3200', 'build/WIPY/release/application.axf', 'BTARGET=application'), '8': PortData('esp8266', 'esp8266', 'build-GENERIC/firmware.elf'), '3': PortData('esp32', 'esp32', 'build-GENERIC/application.elf'), 'r': PortData('nrf', 'nrf', 'build-pca10040/firmware.elf'), 'd': PortData('samd', 'samd', 'build-ADAFRUIT_ITSYBITSY_M4_EXPRESS/firmware.elf'), } def syscmd(*args): sys.stdout.flush() a2 = [] for a in args: if isinstance(a, str): a2.append(a) elif a: a2.extend(a) subprocess.run(a2) def parse_port_list(args): if not args: return list(port_data.values()) else: ports = [] for arg in args: for port_char in arg: try: ports.append(port_data[port_char]) except KeyError: print('unknown port:', port_char) sys.exit(1) return ports def read_build_log(filename): data = dict() lines = [] found_sizes = False with open(filename) as f: for line in f: line = line.strip() if line.strip() == 'COMPUTING SIZES': found_sizes = True elif found_sizes: lines.append(line) is_size_line = False for line in lines: if is_size_line: fields = line.split() data[fields[-1]] = [int(f) for f in fields[:-2]] is_size_line = False else: is_size_line = line.startswith('text\t ') return data def do_diff(args): """Compute the difference between firmware sizes.""" if len(args) != 2: print('usage: %s diff ' % sys.argv[0]) sys.exit(1) data1 = read_build_log(args[0]) data2 = read_build_log(args[1]) for key, value1 in data1.items(): value2 = data2[key] for port in port_data.values(): if key == 'ports/{}/{}'.format(port.dir, port.output): name = port.name break data = [v2 - v1 for v1, v2 in zip(value1, value2)] warn = '' board = re.search(r'/build-([A-Za-z0-9_]+)/', key) if board: board = board.group(1) else: board = '' if name == 'cc3200': delta = data[0] percent = 100 * delta / value1[0] if data[1] != 0: warn += ' %+u(data)' % data[1] else: delta = data[3] percent = 100 * delta / value1[3] if data[1] != 0: warn += ' %+u(data)' % data[1] if data[2] != 0: warn += ' %+u(bss)' % data[2] if warn: warn = '[incl%s]' % warn print('%11s: %+5u %+.3f%% %s%s' % (name, delta, percent, board, warn)) def do_clean(args): """Clean ports.""" ports = parse_port_list(args) print('CLEANING') for port in ports: syscmd('make', '-C', 'ports/{}'.format(port.dir), port.make_flags, 'clean') def do_build(args): """Build ports and print firmware sizes.""" ports = parse_port_list(args) print('BUILDING MPY-CROSS') syscmd('make', '-C', 'mpy-cross', MAKE_FLAGS) print('BUILDING PORTS') for port in ports: syscmd('make', '-C', 'ports/{}'.format(port.dir), MAKE_FLAGS, port.make_flags) do_sizes(args) def do_sizes(args): """Compute and print sizes of firmware.""" ports = parse_port_list(args) print('COMPUTING SIZES') for port in ports: syscmd('size', 'ports/{}/{}'.format(port.dir, port.output)) def main(): # Get command to execute if len(sys.argv) == 1: print('Available commands:') for cmd in globals(): if cmd.startswith('do_'): print(' {:9} {}'.format(cmd[3:], globals()[cmd].__doc__)) sys.exit(1) cmd = sys.argv.pop(1) # Dispatch to desired command try: cmd = globals()['do_{}'.format(cmd)] except KeyError: print("{}: unknown command '{}'".format(sys.argv[0], cmd)) sys.exit(1) cmd(sys.argv[1:]) if __name__ == '__main__': main()