summaryrefslogtreecommitdiff
path: root/tools/metrics.py
diff options
context:
space:
mode:
authorDamien George <damien.p.george@gmail.com>2020-01-13 17:05:46 +1100
committerDamien George <damien.p.george@gmail.com>2020-01-23 12:48:52 +1100
commita11e3062275685dd80e1202a3dfa5f026d91a7a7 (patch)
tree5af032b0cbcb8fd1c954bcae23620b3c34aa3c9d /tools/metrics.py
parentdccace6f3fdded89dbf8173eb3882205c419f2fb (diff)
tools: Add metrics.py script to build and compute port sizes/metrics.
Diffstat (limited to 'tools/metrics.py')
-rwxr-xr-xtools/metrics.py205
1 files changed, 205 insertions, 0 deletions
diff --git a/tools/metrics.py b/tools/metrics.py
new file mode 100755
index 000000000..0cc6db510
--- /dev/null
+++ b/tools/metrics.py
@@ -0,0 +1,205 @@
+#!/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
+ <wait for build to complete>
+ $ git switch new-feature-branch
+ $ ./tools/metrics.py build | tee size1
+ <wait for build to complete>
+ $ ./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 <out1> <out2>' % 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()