Mirror of espurna firmware for wireless switches and more
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.

382 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. command = [os.path.join(prefix, "platformio"), "run"]
  86. if not debug:
  87. command.append("--silent")
  88. command.extend(["--environment", env])
  89. output = None if debug else subprocess.DEVNULL
  90. try:
  91. subprocess.check_call(
  92. command, shell=False, env=os_env, stdout=output, stderr=output
  93. )
  94. except subprocess.CalledProcessError:
  95. print(" - Command failed: {}".format(command))
  96. print(" - Selected flags: {}".format(flags))
  97. sys.exit(1)
  98. def get_available_modules():
  99. modules = []
  100. for line in open("espurna/config/arduino.h"):
  101. match = re.search(r"(\w*)_SUPPORT", line)
  102. if match:
  103. modules.append((match.group(1), 0))
  104. modules.sort(key=lambda item: item[0])
  105. return OrderedDict(modules)
  106. def parse_commandline_args():
  107. parser = argparse.ArgumentParser(
  108. description=DESCRIPTION, formatter_class=argparse.ArgumentDefaultsHelpFormatter
  109. )
  110. parser.add_argument(
  111. "-e", "--environment", help="platformio envrionment to use", default=DEFAULT_ENV
  112. )
  113. parser.add_argument(
  114. "--toolchain-prefix",
  115. help="where to find the xtensa toolchain binaries",
  116. default=TOOLCHAIN_PREFIX,
  117. )
  118. parser.add_argument(
  119. "--platformio-prefix",
  120. help="where to find the platformio executable",
  121. default=PLATFORMIO_PREFIX,
  122. )
  123. parser.add_argument(
  124. "-c",
  125. "--core",
  126. help="use core as base configuration instead of default",
  127. action="store_true",
  128. default=False,
  129. )
  130. parser.add_argument(
  131. "-l",
  132. "--list",
  133. help="list available modules",
  134. action="store_true",
  135. default=False,
  136. )
  137. parser.add_argument("-d", "--debug", action="store_true", default=False)
  138. parser.add_argument(
  139. "modules", nargs="*", help="Modules to test (use ALL to test them all)"
  140. )
  141. return parser.parse_args()
  142. def size_binary_exists(args):
  143. status, _ = getstatusoutput(size_binary_path(args.toolchain_prefix))
  144. if status != 1:
  145. print("size not found, please check that the --toolchain-prefix is correct")
  146. sys.exit(1)
  147. def get_modules(args):
  148. # Load list of all modules
  149. available_modules = get_available_modules()
  150. if args.list:
  151. print("List of available modules:\n")
  152. for module in available_modules:
  153. print("* " + module)
  154. print()
  155. sys.exit(0)
  156. modules = []
  157. if args.modules:
  158. if "ALL" in args.modules:
  159. modules.extend(available_modules.keys())
  160. else:
  161. modules.extend(args.modules)
  162. modules.sort()
  163. # Check test modules exist
  164. for module in modules:
  165. if module not in available_modules:
  166. print("Module {} not found".format(module))
  167. sys.exit(2)
  168. # Either use all of modules or specified subset
  169. if args.core:
  170. modules = available_modules
  171. else:
  172. modules = OrderedDict((x, 0) for x in modules)
  173. configuration = "CORE" if args.core else "DEFAULT"
  174. return configuration, modules
  175. # -------------------------------------------------------------------------------
  176. class Analyser:
  177. """Run platformio and print info about the resulting binary."""
  178. OUTPUT_FORMAT = "{:<20}|{:<15}|{:<15}|{:<15}|{:<15}|{:<15}|{:<15}|{:<15}"
  179. DELIMETERS = OUTPUT_FORMAT.format(
  180. "-" * 20, "-" * 15, "-" * 15, "-" * 15, "-" * 15, "-" * 15, "-" * 15, "-" * 15
  181. )
  182. FIRMWARE_FORMAT = ".pio/build/{env}/firmware.{suffix}"
  183. class _Enable:
  184. def __init__(self, analyser, module=None):
  185. self.analyser = analyser
  186. self.module = module
  187. def __enter__(self):
  188. if not self.module:
  189. for name in self.analyser.modules:
  190. self.analyser.modules[name] = 1
  191. else:
  192. self.analyser.modules[self.module] = 1
  193. return self.analyser
  194. def __exit__(self, *args, **kwargs):
  195. if not self.module:
  196. for name in self.analyser.modules:
  197. self.analyser.modules[name] = 0
  198. else:
  199. self.analyser.modules[self.module] = 0
  200. analyser = None
  201. module = None
  202. def __init__(self, args, modules):
  203. self._debug = args.debug
  204. self._platformio_prefix = args.platformio_prefix
  205. self._toolchain_prefix = args.toolchain_prefix
  206. self._environment = args.environment
  207. self.modules = modules
  208. self.baseline = None
  209. def enable(self, module=None):
  210. return self._Enable(self, module)
  211. def print(self, *args):
  212. print(self.OUTPUT_FORMAT.format(*args))
  213. def print_delimiters(self):
  214. print(self.DELIMETERS)
  215. def begin(self, name):
  216. self.print(
  217. "Module",
  218. "Cache IRAM",
  219. "Init RAM",
  220. "R.O. RAM",
  221. "Uninit RAM",
  222. "Available RAM",
  223. "Flash ROM",
  224. "Binary size",
  225. )
  226. self.print(
  227. "",
  228. ".text + .text1",
  229. ".data",
  230. ".rodata",
  231. ".bss",
  232. "heap + stack",
  233. ".irom0.text",
  234. "",
  235. )
  236. self.print_delimiters()
  237. self.baseline = self.run()
  238. self.print_values(name, self.baseline)
  239. def print_values(self, header, values):
  240. self.print(
  241. header,
  242. values[".text"],
  243. values[".data"],
  244. values[".rodata"],
  245. values[".bss"],
  246. values["free"],
  247. values[".irom0.text"],
  248. values["size"],
  249. )
  250. # TODO: sensor modules need to be print_compared with SENSOR as baseline
  251. # TODO: some modules need to be print_compared with WEB as baseline
  252. def print_compare(self, header, values):
  253. self.print(
  254. header,
  255. values[".text"] - self.baseline[".text"],
  256. values[".data"] - self.baseline[".data"],
  257. values[".rodata"] - self.baseline[".rodata"],
  258. values[".bss"] - self.baseline[".bss"],
  259. values["free"] - self.baseline["free"],
  260. values[".irom0.text"] - self.baseline[".irom0.text"],
  261. values["size"] - self.baseline["size"],
  262. )
  263. def run(self):
  264. run(self._platformio_prefix, self._environment, self.modules, self._debug)
  265. elf_path = self.FIRMWARE_FORMAT.format(env=self._environment, suffix="elf")
  266. bin_path = self.FIRMWARE_FORMAT.format(env=self._environment, suffix="bin")
  267. values = analyse_memory(
  268. size_binary_path(self._toolchain_prefix), elf_path
  269. )
  270. free = 80 * 1024 - values[".data"] - values[".rodata"] - values[".bss"]
  271. free = free + (16 - free % 16)
  272. values["free"] = free
  273. values["size"] = file_size(bin_path)
  274. return values
  275. def main(args):
  276. # Check xtensa-lx106-elf-size is in the path
  277. size_binary_exists(args)
  278. # Which modules to test?
  279. configuration, modules = get_modules(args)
  280. # print_values init message
  281. print('Selected environment "{}"'.format(args.environment), end="")
  282. if modules:
  283. print(" with modules: {}".format(" ".join(modules.keys())))
  284. else:
  285. print()
  286. print()
  287. print("Analyzing {} configuration".format(configuration))
  288. print()
  289. # Build the core without any modules to get base memory usage
  290. analyser = Analyser(args, modules)
  291. analyser.begin(configuration)
  292. # Test each module separately
  293. results = {}
  294. for module in analyser.modules:
  295. with analyser.enable(module):
  296. results[module] = analyser.run()
  297. analyser.print_compare(module, results[module])
  298. # Test all modules
  299. if analyser.modules:
  300. with analyser.enable():
  301. total = analyser.run()
  302. analyser.print_delimiters()
  303. if len(analyser.modules) > 1:
  304. analyser.print_compare("ALL MODULES", total)
  305. analyser.print_values("TOTAL", total)
  306. if __name__ == "__main__":
  307. main(parse_commandline_args())