Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

383 lines
11 KiB

  1. #!/usr/bin/env python3
  2. # pylint: disable=C0301,C0114,C0116,W0511
  3. # coding=utf-8
  4. # -------------------------------------------------------------------------------
  5. # ESPurna module memory analyser
  6. # xose.perez@gmail.com
  7. #
  8. # Rewritten for python-3 and changed to use "size" instead of "objdump"
  9. # Based on https://github.com/esp8266/Arduino/pull/6525
  10. # by Maxim Prokhorov <prokhorov.max@outlook.com>
  11. #
  12. # Based on:
  13. # https://github.com/letscontrolit/ESPEasy/blob/mega/memanalyzer.py
  14. # by psy0rz <edwin@datux.nl>
  15. # https://raw.githubusercontent.com/SmingHub/Sming/develop/tools/memanalyzer.py
  16. # by Slavey Karadzhov <slav@attachix.com>
  17. # https://github.com/Sermus/ESP8266_memory_analyzer
  18. # by Andrey Filimonov
  19. #
  20. # -------------------------------------------------------------------------------
  21. #
  22. # When using Windows with non-default installation at the C:\.platformio,
  23. # you would need to specify toolchain path manually. For example:
  24. #
  25. # $ py -3 scripts\memanalyzer.py --toolchain-prefix C:\.platformio\packages\toolchain-xtensa\bin\xtensa-lx106-elf- <args>
  26. #
  27. # You could also change the path to platformio binary in a similar fashion:
  28. # $ py -3 scripts\memanalyzer.py --platformio-prefix C:\Users\Max\platformio-penv\Scripts\
  29. #
  30. # -------------------------------------------------------------------------------
  31. import argparse
  32. import os
  33. import re
  34. import subprocess
  35. import sys
  36. from collections import OrderedDict
  37. from subprocess import getstatusoutput
  38. __version__ = (0, 3)
  39. # -------------------------------------------------------------------------------
  40. TOTAL_IRAM = 32786
  41. TOTAL_DRAM = 81920
  42. DEFAULT_ENV = "nodemcu-lolin"
  43. TOOLCHAIN_PREFIX = "~/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-"
  44. PLATFORMIO_PREFIX = ""
  45. SECTIONS = OrderedDict(
  46. [
  47. (".data", "Initialized Data (RAM)"),
  48. (".rodata", "ReadOnly Data (RAM)"),
  49. (".bss", "Uninitialized Data (RAM)"),
  50. (".text", "Cached Code (IRAM)"),
  51. (".irom0.text", "Uncached Code (SPI)"),
  52. ]
  53. )
  54. DESCRIPTION = "ESPurna Memory Analyzer v{}".format(
  55. ".".join(str(x) for x in __version__)
  56. )
  57. # -------------------------------------------------------------------------------
  58. def size_binary_path(prefix):
  59. return "{}size".format(os.path.expanduser(prefix))
  60. def file_size(file):
  61. try:
  62. return os.stat(file).st_size
  63. except OSError:
  64. return 0
  65. def analyse_memory(size, elf_file):
  66. proc = subprocess.Popen(
  67. [size, "-A", elf_file], stdout=subprocess.PIPE, universal_newlines=True
  68. )
  69. lines = proc.stdout.readlines()
  70. values = {}
  71. for line in lines:
  72. words = line.split()
  73. for name in SECTIONS.keys():
  74. if line.startswith(name):
  75. value = values.setdefault(name, 0)
  76. value += int(words[1])
  77. values[name] = value
  78. break
  79. return values
  80. def run(prefix, env, modules, debug):
  81. flags = " ".join("-D{}_SUPPORT={:d}".format(k, v) for k, v in modules.items())
  82. os_env = os.environ.copy()
  83. os_env["PLATFORMIO_SRC_BUILD_FLAGS"] = flags
  84. os_env["PLATFORMIO_BUILD_CACHE_DIR"] = "test/pio_cache"
  85. os_env["ESPURNA_PIO_SHARED_LIBRARIES"] = "y"
  86. command = [os.path.join(prefix, "platformio"), "run"]
  87. if not debug:
  88. command.append("--silent")
  89. command.extend(["--environment", env])
  90. output = None if debug else subprocess.DEVNULL
  91. try:
  92. subprocess.check_call(
  93. command, shell=False, env=os_env, stdout=output, stderr=output
  94. )
  95. except subprocess.CalledProcessError:
  96. print(" - Command failed: {}".format(command))
  97. print(" - Selected flags: {}".format(flags))
  98. sys.exit(1)
  99. def get_available_modules():
  100. modules = []
  101. for line in open("espurna/config/arduino.h"):
  102. match = re.search(r"(\w*)_SUPPORT", line)
  103. if match:
  104. modules.append((match.group(1), 0))
  105. modules.sort(key=lambda item: item[0])
  106. return OrderedDict(modules)
  107. def parse_commandline_args():
  108. parser = argparse.ArgumentParser(
  109. description=DESCRIPTION, formatter_class=argparse.ArgumentDefaultsHelpFormatter
  110. )
  111. parser.add_argument(
  112. "-e", "--environment", help="platformio envrionment to use", default=DEFAULT_ENV
  113. )
  114. parser.add_argument(
  115. "--toolchain-prefix",
  116. help="where to find the xtensa toolchain binaries",
  117. default=TOOLCHAIN_PREFIX,
  118. )
  119. parser.add_argument(
  120. "--platformio-prefix",
  121. help="where to find the platformio executable",
  122. default=PLATFORMIO_PREFIX,
  123. )
  124. parser.add_argument(
  125. "-c",
  126. "--core",
  127. help="use core as base configuration instead of default",
  128. action="store_true",
  129. default=False,
  130. )
  131. parser.add_argument(
  132. "-l",
  133. "--list",
  134. help="list available modules",
  135. action="store_true",
  136. default=False,
  137. )
  138. parser.add_argument("-d", "--debug", action="store_true", default=False)
  139. parser.add_argument(
  140. "modules", nargs="*", help="Modules to test (use ALL to test them all)"
  141. )
  142. return parser.parse_args()
  143. def size_binary_exists(args):
  144. status, _ = getstatusoutput(size_binary_path(args.toolchain_prefix))
  145. if status != 1:
  146. print("size not found, please check that the --toolchain-prefix is correct")
  147. sys.exit(1)
  148. def get_modules(args):
  149. # Load list of all modules
  150. available_modules = get_available_modules()
  151. if args.list:
  152. print("List of available modules:\n")
  153. for module in available_modules:
  154. print("* " + module)
  155. print()
  156. sys.exit(0)
  157. modules = []
  158. if args.modules:
  159. if "ALL" in args.modules:
  160. modules.extend(available_modules.keys())
  161. else:
  162. modules.extend(args.modules)
  163. modules.sort()
  164. # Check test modules exist
  165. for module in modules:
  166. if module not in available_modules:
  167. print("Module {} not found".format(module))
  168. sys.exit(2)
  169. # Either use all of modules or specified subset
  170. if args.core:
  171. modules = available_modules
  172. else:
  173. modules = OrderedDict((x, 0) for x in modules)
  174. configuration = "CORE" if args.core else "DEFAULT"
  175. return configuration, modules
  176. # -------------------------------------------------------------------------------
  177. class Analyser:
  178. """Run platformio and print info about the resulting binary."""
  179. OUTPUT_FORMAT = "{:<20}|{:<15}|{:<15}|{:<15}|{:<15}|{:<15}|{:<15}|{:<15}"
  180. DELIMETERS = OUTPUT_FORMAT.format(
  181. "-" * 20, "-" * 15, "-" * 15, "-" * 15, "-" * 15, "-" * 15, "-" * 15, "-" * 15
  182. )
  183. FIRMWARE_FORMAT = ".pio/build/{env}/firmware.{suffix}"
  184. class _Enable:
  185. def __init__(self, analyser, module=None):
  186. self.analyser = analyser
  187. self.module = module
  188. def __enter__(self):
  189. if not self.module:
  190. for name in self.analyser.modules:
  191. self.analyser.modules[name] = 1
  192. else:
  193. self.analyser.modules[self.module] = 1
  194. return self.analyser
  195. def __exit__(self, *args, **kwargs):
  196. if not self.module:
  197. for name in self.analyser.modules:
  198. self.analyser.modules[name] = 0
  199. else:
  200. self.analyser.modules[self.module] = 0
  201. analyser = None
  202. module = None
  203. def __init__(self, args, modules):
  204. self._debug = args.debug
  205. self._platformio_prefix = args.platformio_prefix
  206. self._toolchain_prefix = args.toolchain_prefix
  207. self._environment = args.environment
  208. self.modules = modules
  209. self.baseline = None
  210. def enable(self, module=None):
  211. return self._Enable(self, module)
  212. def print(self, *args):
  213. print(self.OUTPUT_FORMAT.format(*args))
  214. def print_delimiters(self):
  215. print(self.DELIMETERS)
  216. def begin(self, name):
  217. self.print(
  218. "Module",
  219. "Cache IRAM",
  220. "Init RAM",
  221. "R.O. RAM",
  222. "Uninit RAM",
  223. "Available RAM",
  224. "Flash ROM",
  225. "Binary size",
  226. )
  227. self.print(
  228. "",
  229. ".text + .text1",
  230. ".data",
  231. ".rodata",
  232. ".bss",
  233. "heap + stack",
  234. ".irom0.text",
  235. "",
  236. )
  237. self.print_delimiters()
  238. self.baseline = self.run()
  239. self.print_values(name, self.baseline)
  240. def print_values(self, header, values):
  241. self.print(
  242. header,
  243. values[".text"],
  244. values[".data"],
  245. values[".rodata"],
  246. values[".bss"],
  247. values["free"],
  248. values[".irom0.text"],
  249. values["size"],
  250. )
  251. # TODO: sensor modules need to be print_compared with SENSOR as baseline
  252. # TODO: some modules need to be print_compared with WEB as baseline
  253. def print_compare(self, header, values):
  254. self.print(
  255. header,
  256. values[".text"] - self.baseline[".text"],
  257. values[".data"] - self.baseline[".data"],
  258. values[".rodata"] - self.baseline[".rodata"],
  259. values[".bss"] - self.baseline[".bss"],
  260. values["free"] - self.baseline["free"],
  261. values[".irom0.text"] - self.baseline[".irom0.text"],
  262. values["size"] - self.baseline["size"],
  263. )
  264. def run(self):
  265. run(self._platformio_prefix, self._environment, self.modules, self._debug)
  266. elf_path = self.FIRMWARE_FORMAT.format(env=self._environment, suffix="elf")
  267. bin_path = self.FIRMWARE_FORMAT.format(env=self._environment, suffix="bin")
  268. values = analyse_memory(
  269. size_binary_path(self._toolchain_prefix), elf_path
  270. )
  271. free = 80 * 1024 - values[".data"] - values[".rodata"] - values[".bss"]
  272. free = free + (16 - free % 16)
  273. values["free"] = free
  274. values["size"] = file_size(bin_path)
  275. return values
  276. def main(args):
  277. # Check xtensa-lx106-elf-size is in the path
  278. size_binary_exists(args)
  279. # Which modules to test?
  280. configuration, modules = get_modules(args)
  281. # print_values init message
  282. print('Selected environment "{}"'.format(args.environment), end="")
  283. if modules:
  284. print(" with modules: {}".format(" ".join(modules.keys())))
  285. else:
  286. print()
  287. print()
  288. print("Analyzing {} configuration".format(configuration))
  289. print()
  290. # Build the core without any modules to get base memory usage
  291. analyser = Analyser(args, modules)
  292. analyser.begin(configuration)
  293. # Test each module separately
  294. results = {}
  295. for module in analyser.modules:
  296. with analyser.enable(module):
  297. results[module] = analyser.run()
  298. analyser.print_compare(module, results[module])
  299. # Test all modules
  300. if analyser.modules:
  301. with analyser.enable():
  302. total = analyser.run()
  303. analyser.print_delimiters()
  304. if len(analyser.modules) > 1:
  305. analyser.print_compare("ALL MODULES", total)
  306. analyser.print_values("TOTAL", total)
  307. if __name__ == "__main__":
  308. main(parse_commandline_args())