- #!/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 <prokhorov.max@outlook.com>
- #
- # Based on:
- # https://github.com/letscontrolit/ESPEasy/blob/mega/memanalyzer.py
- # by psy0rz <edwin@datux.nl>
- # https://raw.githubusercontent.com/SmingHub/Sming/develop/tools/memanalyzer.py
- # by Slavey Karadzhov <slav@attachix.com>
- # 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- <args>
- #
- # 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())
|