|
|
- #!/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 <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\
- #
- # -------------------------------------------------------------------------------
-
- 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())
|