|
|
@ -0,0 +1,414 @@ |
|
|
|
#!/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()) |