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.

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