#!/usr/bin/env python3 # pylint: disable=C0301,C0114,C0116,W0511 # coding=utf-8 # ------------------------------------------------------------------------------- # ESPurna module memory analyser # xose.perez@gmail.com # # Rewritten for python-3 and changed to use "size" instead of "objdump" # Based on https://github.com/esp8266/Arduino/pull/6525 # by Maxim Prokhorov # # Based on: # https://github.com/letscontrolit/ESPEasy/blob/mega/memanalyzer.py # by psy0rz # https://raw.githubusercontent.com/SmingHub/Sming/develop/tools/memanalyzer.py # by Slavey Karadzhov # https://github.com/Sermus/ESP8266_memory_analyzer # by Andrey Filimonov # # ------------------------------------------------------------------------------- # # When using Windows with non-default installation at the C:\.platformio, # you would need to specify toolchain path manually. For example: # # $ py -3 scripts\memanalyzer.py --toolchain-prefix C:\.platformio\packages\toolchain-xtensa\bin\xtensa-lx106-elf- # # You could also change the path to platformio binary in a similar fashion: # $ py -3 scripts\memanalyzer.py --platformio-prefix C:\Users\Max\platformio-penv\Scripts\ # # ------------------------------------------------------------------------------- import argparse import os import re import subprocess import sys from collections import OrderedDict from subprocess import getstatusoutput __version__ = (0, 3) # ------------------------------------------------------------------------------- TOTAL_IRAM = 32786 TOTAL_DRAM = 81920 DEFAULT_ENV = "nodemcu-lolin" TOOLCHAIN_PREFIX = "~/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-" PLATFORMIO_PREFIX = "" SECTIONS = OrderedDict( [ (".data", "Initialized Data (RAM)"), (".rodata", "ReadOnly Data (RAM)"), (".bss", "Uninitialized Data (RAM)"), (".text", "Cached Code (IRAM)"), (".irom0.text", "Uncached Code (SPI)"), ] ) DESCRIPTION = "ESPurna Memory Analyzer v{}".format( ".".join(str(x) for x in __version__) ) # ------------------------------------------------------------------------------- def size_binary_path(prefix): return "{}size".format(os.path.expanduser(prefix)) def file_size(file): try: return os.stat(file).st_size except OSError: return 0 def analyse_memory(size, elf_file): proc = subprocess.Popen( [size, "-A", elf_file], stdout=subprocess.PIPE, universal_newlines=True ) lines = proc.stdout.readlines() values = {} for line in lines: words = line.split() for name in SECTIONS.keys(): if line.startswith(name): value = values.setdefault(name, 0) value += int(words[1]) values[name] = value break return values def run(prefix, env, modules, debug): flags = " ".join("-D{}_SUPPORT={:d}".format(k, v) for k, v in modules.items()) os_env = os.environ.copy() os_env["PLATFORMIO_SRC_BUILD_FLAGS"] = flags os_env["PLATFORMIO_BUILD_CACHE_DIR"] = "test/pio_cache" os_env["ESPURNA_PIO_SHARED_LIBRARIES"] = "y" command = [os.path.join(prefix, "platformio"), "run"] if not debug: command.append("--silent") command.extend(["--environment", env]) output = None if debug else subprocess.DEVNULL try: subprocess.check_call( command, shell=False, env=os_env, stdout=output, stderr=output ) except subprocess.CalledProcessError: print(" - Command failed: {}".format(command)) print(" - Selected flags: {}".format(flags)) sys.exit(1) def get_available_modules(): modules = [] for line in open("espurna/config/arduino.h"): match = re.search(r"(\w*)_SUPPORT", line) if match: modules.append((match.group(1), 0)) modules.sort(key=lambda item: item[0]) return OrderedDict(modules) def parse_commandline_args(): parser = argparse.ArgumentParser( description=DESCRIPTION, formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument( "-e", "--environment", help="platformio envrionment to use", default=DEFAULT_ENV ) parser.add_argument( "--toolchain-prefix", help="where to find the xtensa toolchain binaries", default=TOOLCHAIN_PREFIX, ) parser.add_argument( "--platformio-prefix", help="where to find the platformio executable", default=PLATFORMIO_PREFIX, ) parser.add_argument( "-c", "--core", help="use core as base configuration instead of default", action="store_true", default=False, ) parser.add_argument( "-l", "--list", help="list available modules", action="store_true", default=False, ) parser.add_argument("-d", "--debug", action="store_true", default=False) parser.add_argument( "modules", nargs="*", help="Modules to test (use ALL to test them all)" ) return parser.parse_args() def size_binary_exists(args): status, _ = getstatusoutput(size_binary_path(args.toolchain_prefix)) if status != 1: print("size not found, please check that the --toolchain-prefix is correct") sys.exit(1) def get_modules(args): # Load list of all modules available_modules = get_available_modules() if args.list: print("List of available modules:\n") for module in available_modules: print("* " + module) print() sys.exit(0) modules = [] if args.modules: if "ALL" in args.modules: modules.extend(available_modules.keys()) else: modules.extend(args.modules) modules.sort() # Check test modules exist for module in modules: if module not in available_modules: print("Module {} not found".format(module)) sys.exit(2) # Either use all of modules or specified subset if args.core: modules = available_modules else: modules = OrderedDict((x, 0) for x in modules) configuration = "CORE" if args.core else "DEFAULT" return configuration, modules # ------------------------------------------------------------------------------- class Analyser: """Run platformio and print info about the resulting binary.""" OUTPUT_FORMAT = "{:<20}|{:<15}|{:<15}|{:<15}|{:<15}|{:<15}|{:<15}|{:<15}" DELIMETERS = OUTPUT_FORMAT.format( "-" * 20, "-" * 15, "-" * 15, "-" * 15, "-" * 15, "-" * 15, "-" * 15, "-" * 15 ) FIRMWARE_FORMAT = ".pio/build/{env}/firmware.{suffix}" class _Enable: def __init__(self, analyser, module=None): self.analyser = analyser self.module = module def __enter__(self): if not self.module: for name in self.analyser.modules: self.analyser.modules[name] = 1 else: self.analyser.modules[self.module] = 1 return self.analyser def __exit__(self, *args, **kwargs): if not self.module: for name in self.analyser.modules: self.analyser.modules[name] = 0 else: self.analyser.modules[self.module] = 0 analyser = None module = None def __init__(self, args, modules): self._debug = args.debug self._platformio_prefix = args.platformio_prefix self._toolchain_prefix = args.toolchain_prefix self._environment = args.environment self.modules = modules self.baseline = None def enable(self, module=None): return self._Enable(self, module) def print(self, *args): print(self.OUTPUT_FORMAT.format(*args)) def print_delimiters(self): print(self.DELIMETERS) def begin(self, name): self.print( "Module", "Cache IRAM", "Init RAM", "R.O. RAM", "Uninit RAM", "Available RAM", "Flash ROM", "Binary size", ) self.print( "", ".text + .text1", ".data", ".rodata", ".bss", "heap + stack", ".irom0.text", "", ) self.print_delimiters() self.baseline = self.run() self.print_values(name, self.baseline) def print_values(self, header, values): self.print( header, values[".text"], values[".data"], values[".rodata"], values[".bss"], values["free"], values[".irom0.text"], values["size"], ) # TODO: sensor modules need to be print_compared with SENSOR as baseline # TODO: some modules need to be print_compared with WEB as baseline def print_compare(self, header, values): self.print( header, values[".text"] - self.baseline[".text"], values[".data"] - self.baseline[".data"], values[".rodata"] - self.baseline[".rodata"], values[".bss"] - self.baseline[".bss"], values["free"] - self.baseline["free"], values[".irom0.text"] - self.baseline[".irom0.text"], values["size"] - self.baseline["size"], ) def run(self): run(self._platformio_prefix, self._environment, self.modules, self._debug) elf_path = self.FIRMWARE_FORMAT.format(env=self._environment, suffix="elf") bin_path = self.FIRMWARE_FORMAT.format(env=self._environment, suffix="bin") values = analyse_memory( size_binary_path(self._toolchain_prefix), elf_path ) free = 80 * 1024 - values[".data"] - values[".rodata"] - values[".bss"] free = free + (16 - free % 16) values["free"] = free values["size"] = file_size(bin_path) return values def main(args): # Check xtensa-lx106-elf-size is in the path size_binary_exists(args) # Which modules to test? configuration, modules = get_modules(args) # print_values init message print('Selected environment "{}"'.format(args.environment), end="") if modules: print(" with modules: {}".format(" ".join(modules.keys()))) else: print() print() print("Analyzing {} configuration".format(configuration)) print() # Build the core without any modules to get base memory usage analyser = Analyser(args, modules) analyser.begin(configuration) # Test each module separately results = {} for module in analyser.modules: with analyser.enable(module): results[module] = analyser.run() analyser.print_compare(module, results[module]) # Test all modules if analyser.modules: with analyser.enable(): total = analyser.run() analyser.print_delimiters() if len(analyser.modules) > 1: analyser.print_compare("ALL MODULES", total) analyser.print_values("TOTAL", total) if __name__ == "__main__": main(parse_commandline_args())