#!/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 by Maxim Prokhorov # prokhorov.max@outlook.com # # 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\ # # ------------------------------------------------------------------------------- from __future__ import print_function import argparse import os import re import subprocess import sys from collections import OrderedDict from subprocess import getstatusoutput __version__ = (0, 2) # ------------------------------------------------------------------------------- TOTAL_IRAM = 32786 TOTAL_DRAM = 81920 DEFAULT_ENV = "nodemcu-lolin" OBJDUMP_PREFIX = "~/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-" 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 objdump_path(prefix): return "{}objdump".format(os.path.expanduser(prefix)) def file_size(file): try: return os.stat(file).st_size except OSError: return 0 def analyse_memory(elf_file, objdump): proc = subprocess.Popen( [objdump, "-t", elf_file], stdout=subprocess.PIPE, universal_newlines=True ) lines = proc.stdout.readlines() # print("{0: >10}|{1: >30}|{2: >12}|{3: >12}|{4: >8}".format("Section", "Description", "Start (hex)", "End (hex)", "Used space")); # print("------------------------------------------------------------------------------"); ret = {} for (id_, _) in list(SECTIONS.items()): section_start_token = " _{}_start".format(id_) section_end_token = " _{}_end".format(id_) section_start = -1 section_end = -1 for line in lines: line = line.strip() if section_start_token in line: data = line.split(" ") section_start = int(data[0], 16) if section_end_token in line: data = line.split(" ") section_end = int(data[0], 16) if section_start != -1 and section_end != -1: break section_length = section_end - section_start # if i < 3: # usedRAM += section_length # if i == 3: # usedIRAM = TOTAL_IRAM - section_length; ret[id_] = section_length # print("{0: >10}|{1: >30}|{2:12X}|{3:12X}|{4:8}".format(id_, descr, section_start, section_end, section_length)) # i += 1 # print("Total Used RAM : {:d}".format(usedRAM)) # print("Free RAM : {:d}".format(TOTAL_DRAM - usedRAM)) # print("Free IRam : {:d}".format(usedIRAM)) return ret 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=OBJDUMP_PREFIX, ) parser.add_argument( "--platformio-prefix", help="where to find the platformio executable", default="", ) 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 objdump_check(args): status, _ = getstatusoutput(objdump_path(args.toolchain_prefix)) if status not in (2, 512): print("objdump 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}" ELF_FORMAT = ".pio/build/{env}/firmware.elf" BIN_FORMAT = ".pio/build/{env}/firmware.bin" 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._elf_path = self.ELF_FORMAT.format(env=args.environment) self._bin_path = self.BIN_FORMAT.format(env=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): self.print( "-" * 20, "-" * 15, "-" * 15, "-" * 15, "-" * 15, "-" * 15, "-" * 15, "-" * 15, ) 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", ".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) values = analyse_memory(self._elf_path, objdump_path(self._toolchain_prefix)) free = 80 * 1024 - values["data"] - values["rodata"] - values["bss"] free = free + (16 - free % 16) values["free"] = free values["size"] = file_size(self._bin_path) return values _debug = False _platformio_prefix = None _toolchain_prefix = None _environment = None _bin_path = None _elf_path = None modules = None baseline = None def main(args): # Check xtensa-lx106-elf-objdump is in the path objdump_check(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())