idf_size.py 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226
  1. #!/usr/bin/env python
  2. #
  3. # esp-idf alternative to "size" to print ELF file sizes, also analyzes
  4. # the linker map file to dump higher resolution details.
  5. #
  6. # Includes information which is not shown in "xtensa-esp32-elf-size",
  7. # or easy to parse from "xtensa-esp32-elf-objdump" or raw map files.
  8. #
  9. # SPDX-FileCopyrightText: 2017-2022 Espressif Systems (Shanghai) CO LTD
  10. # SPDX-License-Identifier: Apache-2.0
  11. #
  12. import argparse
  13. import collections
  14. import json
  15. import os.path
  16. import re
  17. import sys
  18. from typing import Any, Callable, Collection, Dict, Iterable, List, Optional, TextIO, Tuple, Union
  19. import yaml
  20. class Section(Dict):
  21. # define for python type hints
  22. size: int
  23. address: int
  24. name: str
  25. sources: List[Dict]
  26. SectionDict = Dict[str, Section]
  27. try:
  28. basestring
  29. except NameError:
  30. basestring = str
  31. GLOBAL_JSON_INDENT = 4
  32. GLOBAL_JSON_SEPARATORS = (',', ': ')
  33. class MemRegions(object):
  34. """
  35. Regions determined by the chip target.
  36. """
  37. # DIRAM is not added here. The DIRAM is indicated by the `secondary_addr` of each MemRegDef
  38. (DRAM_ID, IRAM_ID, CACHE_D_ID, CACHE_I_ID, RTC_FAST_D_ID, RTC_FAST_I_ID, RTC_SLOW_D_ID) = range(7)
  39. # The order of variables in the tuple is the same as in the soc_memory_layout.c files
  40. MemRegDef = collections.namedtuple('MemRegDef', ['primary_addr', 'length', 'type', 'secondary_addr'])
  41. class Region(object):
  42. # Helper class to store region information
  43. def __init__(self, start: int, length: int, region: 'MemRegions.MemRegDef', section: Optional[str]=None) -> None:
  44. self.start = start
  45. self.len = length
  46. self.region = region
  47. self.section = section
  48. @staticmethod
  49. def get_mem_regions(target: str) -> List:
  50. """
  51. Get memory regions for specific target
  52. """
  53. # The target specific memory structure is deduced from soc_memory_types defined in
  54. # $IDF_PATH/components/soc/**/soc_memory_layout.c files.
  55. MemRegDef = MemRegions.MemRegDef
  56. def change_to_proper_format(length: Union[str, bytes]) -> Any:
  57. '''
  58. Change `length` if it is string like `'0x8000 + 6 * 0x10000'` to resolve of this math equation
  59. or if `length` is number function return it without changing.
  60. '''
  61. try:
  62. return eval(length)
  63. except TypeError:
  64. return length
  65. def get_mem_reg_def(chip_info: Dict, memory_reg: str) -> Tuple:
  66. chip_info[memory_reg]['secondary_address'] = chip_info[memory_reg].get('secondary_address') or 0
  67. return MemRegDef(chip_info[memory_reg]['primary_address'], change_to_proper_format(chip_info[memory_reg]['length']),
  68. getattr(MemRegions, memory_reg.strip('_12') + '_ID'), chip_info[memory_reg]['secondary_address'])
  69. try:
  70. with open(os.path.join(os.path.dirname(__file__), 'idf_size_yaml', target + '_data_info.yaml'), 'r') as stream:
  71. chip_info = (yaml.safe_load(stream))
  72. except FileNotFoundError:
  73. raise RuntimeError('Target not detected.')
  74. return sorted([get_mem_reg_def(chip_info, item) for item in chip_info])
  75. def __init__(self, target: str) -> None:
  76. self.chip_mem_regions = self.get_mem_regions(target)
  77. if not self.chip_mem_regions:
  78. raise RuntimeError('Target {} is not implemented in idf_size'.format(target))
  79. def _get_first_region(self, start: int, length: int) -> Tuple[Union['MemRegions.MemRegDef', None], int]:
  80. for region in self.chip_mem_regions: # type: ignore
  81. if region.primary_addr <= start < region.primary_addr + region.length:
  82. return (region, length)
  83. if region.secondary_addr and region.secondary_addr <= start < region.secondary_addr + region.length:
  84. return (region, length)
  85. print('WARNING: Given section not found in any memory region.')
  86. print('Check whether the LD file is compatible with the definitions in get_mem_regions in idf_size.py')
  87. return (None, length)
  88. def _get_regions(self, start: int, length: int, name: Optional[str]=None) -> List:
  89. ret = []
  90. while length > 0:
  91. (region, cur_len) = self._get_first_region(start, length)
  92. if region is None:
  93. # skip regions that not in given section
  94. length -= cur_len
  95. start += cur_len
  96. continue
  97. ret.append(MemRegions.Region(start, cur_len, region, name))
  98. length -= cur_len
  99. start += cur_len
  100. return ret
  101. def fit_segments_into_regions(self, segments: Dict) -> List:
  102. region_list = []
  103. for segment in segments.values():
  104. sorted_segments = self._get_regions(segment['origin'], segment['length'])
  105. region_list.extend(sorted_segments)
  106. return region_list
  107. def fit_sections_into_regions(self, sections: Dict) -> List:
  108. region_list = []
  109. for section in sections.values():
  110. sorted_sections = self._get_regions(section['address'], section['size'], section['name'])
  111. region_list.extend(sorted_sections)
  112. return region_list
  113. class LinkingSections(object):
  114. _section_type_dict = {key: re.compile(value) for key, value in {
  115. 'text': r'.*\.text',
  116. 'data': r'.*\.data',
  117. 'bss': r'.*\.bss',
  118. 'rodata': r'.*\.rodata',
  119. 'noinit': r'.*noinit',
  120. 'vectors': r'.*\.vectors',
  121. 'flash': r'.*flash.*',
  122. }.items()}
  123. @staticmethod
  124. def in_section(section: str, section_name_or_list: Union[str, Iterable]) -> bool:
  125. """
  126. Check if section in section_name_or_list
  127. """
  128. if isinstance(section_name_or_list, basestring):
  129. section_name_or_list = [section_name_or_list]
  130. for section_name in section_name_or_list:
  131. if LinkingSections._section_type_dict[section_name].match(section):
  132. return True
  133. return False
  134. @staticmethod
  135. def filter_sections(sections: Dict) -> Dict:
  136. return {key: v for key, v in sections.items()
  137. if LinkingSections.in_section(key, LinkingSections._section_type_dict.keys())}
  138. @staticmethod
  139. def get_display_name_order(section_name_list: List[str]) -> Tuple[List[str], List[str]]:
  140. '''
  141. Return two lists, in the suggested display order.
  142. First list is the reordered section_name_list, second list is the suggested display name, corresponding to the first list
  143. '''
  144. def get_memory_name(split_name: List) -> Tuple[str, str]:
  145. memory_name = '.{}'.format(split_name[1])
  146. display_name = section
  147. for seg_name in ['iram','dram','flash']:
  148. if seg_name in split_name[1]:
  149. memory_name = '.{}'.format(seg_name)
  150. seg_name = seg_name.upper() if seg_name != 'flash' else seg_name.capitalize()
  151. display_name = ''.join([seg_name,
  152. split_name[1].replace('iram', '') if seg_name == 'IRAM' else '',
  153. ' .{}'.format(split_name[2]) if len(split_name) > 2 else ''])
  154. return memory_name, display_name
  155. ordered_name_list = sorted(section_name_list)
  156. display_name_list = ordered_name_list.copy()
  157. memory_name = ''
  158. ordered_name_list = sort_dict(ordered_name_list)
  159. for i, section in enumerate(ordered_name_list):
  160. if memory_name and section.startswith(memory_name):
  161. # If the section has same memory type with the previous one, use shorter name
  162. display_name_list[i] = section.replace(memory_name, '& ')
  163. continue
  164. memory_name = ''
  165. split_name = section.split('.')
  166. if len(split_name) > 1:
  167. # If the section has a memory type, update the type and try to display the type properly
  168. assert split_name[0] == '', 'Unexpected section name "{}"'.format(section)
  169. memory_name, display_name_list[i] = get_memory_name(split_name)
  170. continue
  171. # Otherwise use its original name
  172. display_name_list[i] = section
  173. return ordered_name_list, display_name_list
  174. def scan_to_header(file: Iterable, header_line: str) -> None:
  175. """ Scan forward in a file until you reach 'header_line', then return """
  176. for line in file:
  177. if line.strip() == header_line:
  178. return
  179. raise RuntimeError("Didn't find line '%s' in file" % header_line)
  180. def format_json(json_object: Dict) -> str:
  181. return json.dumps(json_object,
  182. allow_nan=True,
  183. indent=GLOBAL_JSON_INDENT,
  184. separators=GLOBAL_JSON_SEPARATORS) + os.linesep
  185. def load_map_data(map_file: TextIO) -> Tuple[str, Dict, Dict]:
  186. segments = load_segments(map_file)
  187. detected_chip = detect_target_chip(map_file)
  188. sections = load_sections(map_file)
  189. # Exclude the dummy and .text_end section, which usually means shared region among I/D buses
  190. for key in list(sections.keys()):
  191. if key.endswith(('dummy', '.text_end')):
  192. sections.pop(key)
  193. return detected_chip, segments, sections
  194. def load_segments(map_file: TextIO) -> Dict:
  195. """ Memory Configuration section is the total size of each segment """
  196. result = {} # type: Dict[Any, Dict]
  197. scan_to_header(map_file, 'Memory Configuration')
  198. RE_MEMORY_SECTION = re.compile(r'(?P<name>[^ ]+) +0x(?P<origin>[\da-f]+) +0x(?P<length>[\da-f]+)')
  199. for line in map_file:
  200. match_section = RE_MEMORY_SECTION.match(line)
  201. if match_section is None:
  202. if len(result) == 0:
  203. continue # whitespace or a header, before the content we want
  204. else:
  205. return result # we're at the end of the Memory Configuration
  206. segment = {
  207. 'name': match_section.group('name'),
  208. 'origin': int(match_section.group('origin'), 16),
  209. 'length': int(match_section.group('length'), 16),
  210. }
  211. if segment['name'] != '*default*':
  212. result[segment['name']] = segment
  213. raise RuntimeError('End of file while scanning memory configuration?')
  214. def detect_target_chip(map_file: Iterable) -> str:
  215. ''' Detect target chip based on the target archive name in the linker script part of the MAP file '''
  216. scan_to_header(map_file, 'Linker script and memory map')
  217. RE_TARGET = re.compile(r'IDF_TARGET_(\S*) =')
  218. # For back-compatible with cmake in idf version before 5.0
  219. RE_TARGET_CMAKEv4x = re.compile(r'project_elf_src_(\S*)\.c.obj')
  220. # For back-compatible with make
  221. RE_TARGET_MAKE = re.compile(r'^LOAD .*?/xtensa-([^-]+)-elf/')
  222. for line in map_file:
  223. match_target = RE_TARGET.search(line)
  224. if match_target:
  225. return match_target.group(1).lower()
  226. match_target = RE_TARGET_CMAKEv4x.search(line)
  227. if match_target:
  228. return match_target.group(1)
  229. match_target = RE_TARGET_MAKE.search(line)
  230. if match_target:
  231. return match_target.group(1)
  232. line = line.strip()
  233. # There could be empty line(s) between the "Linker script and memory map" header and "LOAD lines". Therefore,
  234. # line stripping and length is checked as well. The "LOAD lines" are between START GROUP and END GROUP for
  235. # older MAP files.
  236. if not line.startswith(('LOAD', 'START GROUP', 'END GROUP')) and len(line) > 0:
  237. # This break is a failsafe to not process anything load_sections() might want to analyze.
  238. break
  239. raise RuntimeError('Target not detected')
  240. def load_sections(map_file: TextIO) -> Dict:
  241. """ Load section size information from the MAP file.
  242. Returns a dict of 'sections', where each key is a section name and the value
  243. is a dict with details about this section, including a "sources" key which holds a list of source file line
  244. information for each symbol linked into the section.
  245. There are two kinds of lines:
  246. - symbol_only: [optional space]<sym_name>
  247. - full line: [optional space][optional sym_name] <address> <size> [optional file_info]
  248. If <sym_name> doesn't exist, ues the symbol name from the symbol_only line above
  249. If the line is the starting of a section, the <file> should be empty, otherwise if the line is for a source
  250. line, the <file> must exist, or the <sym_name> should be is no *fill*. This rule is used to tell sections from
  251. source lines.
  252. """
  253. # Check for lines which only contain the sym name (and rest is on following lines)
  254. RE_SYMBOL_ONLY_LINE = re.compile(r'^\s*(?P<sym_name>\S*)$')
  255. # Fast check to see if line is a potential source line before running the slower full regex against it
  256. RE_PRE_FILTER = re.compile(r'.*0x[\da-f]+\s*0x[\da-f]+.*')
  257. # source file line, ie
  258. # 0x0000000040080400 0xa4 /home/gus/esp/32/idf/examples/get-started/hello_world/build/esp32/libesp32.a(cpu_start.o)
  259. # cmake build system links some object files directly, not part of any archive, so make that part optional
  260. # .xtensa.info 0x0000000000000000 0x38 CMakeFiles/hello_world.elf.dir/project_elf_src.c.obj
  261. # *fill* 0x00000000400e2967 0x1
  262. RE_FULL_LINE = re.compile(r'\s*(?P<sym_name>\S*) +0x(?P<address>[\da-f]+) +0x(?P<size>[\da-f]+)\s*(?P<file>.*)$')
  263. # Extract archive and object_file from the file_info field
  264. # The object file extention (.obj or .o) is optional including the dot. This is necessary for some third-party
  265. # libraries. Since the dot is optional and the search gready the parsing of the object name must stop at ). Hence
  266. # the [^ )] part of the regex.
  267. RE_FILE = re.compile(r'((?P<archive>[^ ]+\.a)?\(?(?P<object_file>[^ )]+(\.(o|obj))?)\)?)')
  268. def dump_src_line(src: Dict) -> str:
  269. return '%s(%s) addr: 0x%08x, size: 0x%x+%d' % (src['sym_name'], src['file'], src['address'], src['size'], src['fill'])
  270. sections = {} # type: Dict[Any, Dict]
  271. section = {} # type: Dict[str, Any]
  272. sym_backup = ''
  273. for line in map_file:
  274. if line.strip() == 'Cross Reference Table':
  275. # Stop processing lines because we are at the next section in the map file
  276. break
  277. match_line = RE_SYMBOL_ONLY_LINE.match(line)
  278. if match_line:
  279. # In some cases the section name appears on the previous line, back it up in here
  280. sym_backup = match_line.group('sym_name')
  281. continue
  282. if not RE_PRE_FILTER.match(line):
  283. # Line does not match our quick check, so skip to next line
  284. continue
  285. match_line = RE_FULL_LINE.match(line)
  286. if not match_line:
  287. assert not sym_backup, 'Symbol only line must be followed by a line with address and size'
  288. continue
  289. name = match_line.group('sym_name') if match_line.group('sym_name') else sym_backup
  290. sym_backup = ''
  291. is_section = not match_line.group('file') and name != '*fill*'
  292. if is_section:
  293. # section
  294. section = {
  295. 'name': name,
  296. 'address': int(match_line.group('address'), 16),
  297. 'size': int(match_line.group('size'), 16),
  298. 'sources': [],
  299. }
  300. sections[name] = section
  301. else:
  302. # symbol
  303. if not section:
  304. continue
  305. # There are some source lines in rodata section doesn't actually take any space, but have size
  306. # Make size of those sections zero
  307. srcs = section['sources'] # type: List[Dict]
  308. if srcs:
  309. last_src = srcs[-1]
  310. if last_src['size'] > 0 and last_src['address'] == int(match_line.group('address'), 16):
  311. if section['name'] not in ['.comment', '.debug_str', '.debug_line_str'] and 'rodata' not in last_src['sym_name']:
  312. raise RuntimeError('Due to overlap with following lines, size of the line set to 0:\n %s' % dump_src_line(last_src))
  313. last_src['size'] = 0
  314. # Count the padding size into the last valid (size > 0) source in the section
  315. if name == '*fill*':
  316. for src in reversed(srcs):
  317. if src['size'] > 0:
  318. src['fill'] += int(match_line.group('size'), 16)
  319. break
  320. continue
  321. # Extract archive and file information
  322. match_arch_and_file = RE_FILE.match(match_line.group('file'))
  323. assert match_arch_and_file, 'Archive and file information not found for "{}"'.format(match_line.group('file'))
  324. archive = match_arch_and_file.group('archive')
  325. if archive is None:
  326. # optional named group "archive" was not matched, so assign a value to it
  327. archive = '(exe)'
  328. file = match_arch_and_file.group('object_file')
  329. assert name
  330. source = {
  331. 'size': int(match_line.group('size'), 16),
  332. 'address': int(match_line.group('address'), 16),
  333. 'archive': os.path.basename(archive),
  334. 'object_file': os.path.basename(file),
  335. 'sym_name': name,
  336. 'fill': 0, # padding size ofter the source
  337. }
  338. source['file'] = '%s:%s' % (source['archive'], source['object_file'])
  339. section['sources'].append(source) # type: ignore
  340. # Validate the map file
  341. for section in sections.values():
  342. src_curr = {} # type: Dict[str, Any]
  343. for src in section['sources']:
  344. if src['size'] == 0:
  345. continue
  346. expected_addr = src_curr['address'] + src_curr['size'] + src_curr['fill'] if src_curr else section['sources'][0]['address']
  347. if src['address'] != expected_addr:
  348. print('Warning: source line overlap:')
  349. print(' ' + dump_src_line(src_curr))
  350. print(' ' + dump_src_line(src))
  351. src_curr = src
  352. return sections
  353. def check_target(target: str, map_file: TextIO) -> None:
  354. if target is None:
  355. raise RuntimeError('The target chip cannot be detected for {}. '
  356. 'Please report the issue.'.format(map_file.name))
  357. def main() -> None:
  358. parser = argparse.ArgumentParser(description='idf_size - a tool to print size information from an IDF MAP file')
  359. parser.add_argument(
  360. 'map_file', help='MAP file produced by linker',
  361. type=argparse.FileType('r'))
  362. format_group = parser.add_mutually_exclusive_group()
  363. format_group.add_argument(
  364. '--format',
  365. help='Specify output format: text, csv or json',
  366. choices=['text','csv', 'json'],
  367. default='text')
  368. format_group.add_argument(
  369. '--json', help=argparse.SUPPRESS, action='store_true')
  370. parser.add_argument(
  371. '--archives', help='Print per-archive sizes', action='store_true')
  372. parser.add_argument(
  373. '--archive_details', help='Print detailed symbols per archive')
  374. parser.add_argument(
  375. '--files', help='Print per-file sizes', action='store_true')
  376. parser.add_argument(
  377. '--target', help='Set target chip', default=None)
  378. parser.add_argument(
  379. '--diff', help='Show the differences in comparison with another MAP file',
  380. metavar='ANOTHER_MAP_FILE',
  381. default=None,
  382. dest='another_map_file')
  383. parser.add_argument(
  384. '-o',
  385. '--output-file',
  386. type=argparse.FileType('w'),
  387. default=sys.stdout,
  388. help='Print output to the specified file instead of stdout')
  389. args = parser.parse_args()
  390. if args.json:
  391. print('WARNING: --json argument is deprecated in favour of the --format argument')
  392. detected_target, segments, sections = load_map_data(args.map_file)
  393. args.map_file.close()
  394. check_target(detected_target, args.map_file)
  395. if args.another_map_file:
  396. with open(args.another_map_file, 'r') as f:
  397. detected_target_diff, segments_diff, sections_diff = load_map_data(f)
  398. check_target(detected_target_diff, f)
  399. if detected_target_diff != detected_target:
  400. print('WARNING: The target of the reference and other MAP files is {} and {}, respectively.'
  401. ''.format(detected_target, detected_target_diff))
  402. else:
  403. segments_diff, sections_diff, detected_target_diff = {}, {}, ''
  404. if args.target is not None:
  405. if args.target != detected_target or (detected_target_diff and args.target != detected_target_diff):
  406. print('WARNING: The detected chip target overwritten to {} by command line argument!'.format(args.target))
  407. detected_target = args.target
  408. detected_target_diff = args.target
  409. output = ''
  410. if not args.format == 'json' or not (args.archives or args.files or args.archive_details):
  411. output += get_summary(args.map_file.name, segments, sections, detected_target,
  412. args.format, args.another_map_file, segments_diff,
  413. sections_diff, detected_target_diff, not (args.archives or args.files))
  414. if args.archives:
  415. output += get_detailed_sizes(sections, 'archive', 'Archive File', args.format, sections_diff)
  416. if args.files:
  417. output += get_detailed_sizes(sections, 'file', 'Object File', args.format, sections_diff)
  418. if args.archive_details:
  419. output += get_archive_symbols(sections, args.archive_details, args.format, sections_diff)
  420. args.output_file.write(output)
  421. args.output_file.close()
  422. class StructureForSummary(object):
  423. dram_data, dram_bss, dram_rodata, dram_other, used_dram, dram_total, dram_remain = (0, ) * 7
  424. used_dram_ratio = 0.
  425. iram_vectors, iram_text, iram_other, used_iram, iram_total, iram_remain = (0, ) * 6
  426. used_iram_ratio = 0.
  427. diram_data, diram_bss, diram_text, diram_vectors, diram_rodata, diram_other, diram_total, used_diram, diram_remain = (0, ) * 9
  428. used_diram_ratio = 0.
  429. flash_code, flash_rodata, flash_other, used_flash_non_ram, total_size = (0, ) * 5
  430. def __sub__(self, rhs: 'StructureForSummary') -> 'StructureForSummary':
  431. assert isinstance(rhs, StructureForSummary)
  432. ret = self
  433. for key in StructureForSummary.get_required_items():
  434. setattr(ret, key, getattr(self, key) - getattr(rhs, key))
  435. return ret
  436. def get_dram_overflowed(self) -> bool:
  437. return self.used_dram_ratio > 1.0
  438. def get_iram_overflowed(self) -> bool:
  439. return self.used_iram_ratio > 1.0
  440. def get_diram_overflowed(self) -> bool:
  441. return self.used_diram_ratio > 1.0
  442. @classmethod
  443. def get_required_items(cls: Any) -> List:
  444. whole_list = list(filter(lambda x: not (x.startswith('__') or x.endswith('__') or callable(getattr(cls, x))), dir(cls)))
  445. return whole_list
  446. @staticmethod
  447. def get(segments: List, sections: List) -> 'StructureForSummary':
  448. def get_size(sections: Iterable) -> int:
  449. return sum([x.len for x in sections])
  450. def in_diram(x: MemRegions.Region) -> bool:
  451. return x.region.type in (MemRegions.DRAM_ID, MemRegions.IRAM_ID) and x.region.secondary_addr > 0
  452. def in_dram(x: MemRegions.Region) -> bool:
  453. return x.region.type == MemRegions.DRAM_ID and x.region.secondary_addr == 0 # type: ignore
  454. def in_iram(x: MemRegions.Region) -> bool:
  455. return x.region.type == MemRegions.IRAM_ID and x.region.secondary_addr == 0 # type: ignore
  456. r = StructureForSummary()
  457. diram_filter = filter(in_diram, segments)
  458. r.diram_total = get_size(diram_filter)
  459. dram_filter = filter(in_dram, segments)
  460. r.dram_total = get_size(dram_filter)
  461. iram_filter = filter(in_iram, segments)
  462. r.iram_total = get_size(iram_filter)
  463. # This fixes counting the diram twice if the cache fills the iram entirely
  464. if r.iram_total == 0:
  465. r.diram_total //= 2
  466. def filter_in_section(sections: Iterable[MemRegions.Region], section_to_check: str) -> List[MemRegions.Region]:
  467. return list(filter(lambda x: LinkingSections.in_section(x.section, section_to_check), sections)) # type: ignore
  468. dram_sections = list(filter(in_dram, sections))
  469. iram_sections = list(filter(in_iram, sections))
  470. diram_sections = list(filter(in_diram, sections))
  471. flash_sections = filter_in_section(sections, 'flash')
  472. dram_data_list = filter_in_section(dram_sections, 'data')
  473. dram_bss_list = filter_in_section(dram_sections, 'bss')
  474. dram_rodata_list = filter_in_section(dram_sections, 'rodata')
  475. dram_other_list = [x for x in dram_sections if x not in dram_data_list + dram_bss_list + dram_rodata_list]
  476. iram_vectors_list = filter_in_section(iram_sections, 'vectors')
  477. iram_text_list = filter_in_section(iram_sections, 'text')
  478. iram_other_list = [x for x in iram_sections if x not in iram_vectors_list + iram_text_list]
  479. diram_vectors_list = filter_in_section(diram_sections, 'vectors')
  480. diram_data_list = filter_in_section(diram_sections, 'data')
  481. diram_bss_list = filter_in_section(diram_sections, 'bss')
  482. diram_text_list = filter_in_section(diram_sections, 'text')
  483. diram_rodata_list = filter_in_section(diram_sections, 'rodata')
  484. diram_other_list = [x for x in diram_sections if x not in diram_data_list + diram_bss_list + diram_text_list + diram_vectors_list + diram_rodata_list]
  485. flash_text_list = filter_in_section(flash_sections, 'text')
  486. flash_rodata_list = filter_in_section(flash_sections, 'rodata')
  487. flash_other_list = [x for x in flash_sections if x not in flash_text_list + flash_rodata_list]
  488. r.dram_data = get_size(dram_data_list)
  489. r.dram_bss = get_size(dram_bss_list)
  490. r.dram_rodata = get_size(dram_rodata_list)
  491. r.dram_other = get_size(dram_other_list)
  492. r.used_dram = r.dram_data + r.dram_bss + r.dram_other + r.dram_rodata
  493. try:
  494. r.used_dram_ratio = r.used_dram / r.dram_total
  495. except ZeroDivisionError:
  496. r.used_dram_ratio = float('nan') if r.used_dram != 0 else 0
  497. r.dram_remain = r.dram_total - r.used_dram
  498. r.iram_vectors = get_size((iram_vectors_list))
  499. r.iram_text = get_size((iram_text_list))
  500. r.iram_other = get_size((iram_other_list))
  501. r.used_iram = r.iram_vectors + r.iram_text + r.iram_other
  502. try:
  503. r.used_iram_ratio = r.used_iram / r.iram_total
  504. except ZeroDivisionError:
  505. r.used_iram_ratio = float('nan') if r.used_iram != 0 else 0
  506. r.iram_remain = r.iram_total - r.used_iram
  507. r.diram_data = get_size(diram_data_list)
  508. r.diram_bss = get_size(diram_bss_list)
  509. r.diram_text = get_size(diram_text_list)
  510. r.diram_vectors = get_size(diram_vectors_list)
  511. r.diram_rodata = get_size(diram_rodata_list)
  512. r.diram_other = get_size(diram_other_list)
  513. r.used_diram = r.diram_data + r.diram_bss + r.diram_text + r.diram_vectors + r.diram_other + r.diram_rodata
  514. try:
  515. r.used_diram_ratio = r.used_diram / r.diram_total
  516. except ZeroDivisionError:
  517. r.used_diram_ratio = float('nan') if r.used_diram != 0 else 0
  518. r.diram_remain = r.diram_total - r.used_diram
  519. r.flash_code = get_size(flash_text_list)
  520. r.flash_rodata = get_size(flash_rodata_list)
  521. r.flash_other = get_size(flash_other_list)
  522. r.used_flash_non_ram = r.flash_code + r.flash_rodata + r.flash_other
  523. # The used DRAM BSS is counted into the "Used static DRAM" but not into the "Total image size"
  524. r.total_size = r.used_dram - r.dram_bss + r.used_iram + r.used_diram - r.diram_bss + r.used_flash_non_ram
  525. return r
  526. def get_dict(self) -> collections.OrderedDict:
  527. ret = collections.OrderedDict([
  528. ('dram_data', self.dram_data),
  529. ('dram_bss', self.dram_bss),
  530. ('dram_rodata', self.dram_rodata),
  531. ('dram_other', self.dram_other),
  532. ('used_dram', self.used_dram),
  533. ('dram_total', self.dram_total),
  534. ('used_dram_ratio', self.used_dram_ratio if self.used_dram_ratio is not float('nan') else 0),
  535. ('dram_remain', self.dram_remain),
  536. ('iram_vectors', self.iram_vectors),
  537. ('iram_text', self.iram_text),
  538. ('iram_other', self.iram_other),
  539. ('used_iram', self.used_iram),
  540. ('iram_total', self.iram_total),
  541. ('used_iram_ratio', self.used_iram_ratio),
  542. ('iram_remain', self.iram_remain),
  543. ('diram_data', self.diram_data),
  544. ('diram_bss', self.diram_bss),
  545. ('diram_text', self.diram_text),
  546. ('diram_vectors', self.diram_vectors),
  547. ('diram_rodata', self.diram_rodata),
  548. ('diram_other', self.diram_other),
  549. ('diram_total', self.diram_total),
  550. ('used_diram', self.used_diram),
  551. ('used_diram_ratio', self.used_diram_ratio),
  552. ('diram_remain', self.diram_remain),
  553. ('flash_code', self.flash_code),
  554. ('flash_rodata', self.flash_rodata),
  555. ('flash_other', self.flash_other),
  556. ('used_flash_non_ram', self.used_flash_non_ram), # text/data in D/I RAM not included
  557. ('total_size', self.total_size) # bss not included
  558. ])
  559. assert len(ret) == len(StructureForSummary.get_required_items())
  560. return ret
  561. def get_structure_for_target(segments: Dict, sections: Dict, target: str) -> StructureForSummary:
  562. """
  563. Return StructureForSummary for specific target
  564. """
  565. mem_regions = MemRegions(target)
  566. segment_layout = mem_regions.fit_segments_into_regions(segments)
  567. section_layout = mem_regions.fit_sections_into_regions(LinkingSections.filter_sections(sections))
  568. current = StructureForSummary.get(segment_layout, section_layout)
  569. return current
  570. def get_summary(path: str, segments: Dict, sections: Dict, target: str, output_format: str='',
  571. path_diff: str='', segments_diff: Optional[Dict]=None, sections_diff: Optional[Dict]=None,
  572. target_diff: str='', print_suggestions: bool=True) -> str:
  573. segments_diff = segments_diff or {}
  574. sections_diff = sections_diff or {}
  575. current = get_structure_for_target(segments, sections, target)
  576. if path_diff:
  577. diff_en = True
  578. mem_regions_diff = MemRegions(target_diff)
  579. segment_layout_diff = mem_regions_diff.fit_segments_into_regions(segments_diff)
  580. section_layout_diff = mem_regions_diff.fit_sections_into_regions(LinkingSections.filter_sections(sections_diff))
  581. reference = StructureForSummary.get(segment_layout_diff, section_layout_diff)
  582. else:
  583. diff_en = False
  584. reference = StructureForSummary()
  585. if output_format == 'json':
  586. current_json_dic = current.get_dict()
  587. if diff_en:
  588. reference_json_dic = reference.get_dict()
  589. diff_json_dic = collections.OrderedDict([
  590. (k, v - reference_json_dic[k]) for k, v in current_json_dic.items()])
  591. output = format_json(collections.OrderedDict([('current', current_json_dic),
  592. ('reference', reference_json_dic),
  593. ('diff', diff_json_dic),
  594. ]))
  595. else:
  596. output = format_json(current_json_dic)
  597. else:
  598. class LineDef(object):
  599. def __init__(self, title: str, name: str) -> None:
  600. self.title = title
  601. self.name = name
  602. def format_line(self) -> Tuple[str, str, str, str]:
  603. return ('%16s: {%s:>7} bytes' % (self.title, self.name),
  604. '{%s:>7}' % self.name,
  605. '{%s:+}' % self.name,
  606. '')
  607. def format_line_csv(self) -> Tuple[str, str, str, str]:
  608. return ('%s,{%s} bytes' % (self.title, self.name),
  609. '{%s}' % self.name,
  610. '{%s:+}' % self.name,
  611. '')
  612. class HeadLineDef(LineDef):
  613. def __init__(self, title: str, name: str, remain: str, ratio: str, total: str, warning_message: str) -> None:
  614. super(HeadLineDef, self).__init__(title, name)
  615. self.remain = remain
  616. self.ratio = ratio
  617. self.total = total
  618. self.warning_message = warning_message
  619. def format_line(self) -> Tuple[str, str, str, str]:
  620. return ('%-1s: {%s:>7} bytes ({%s:>7} remain, {%s:.1%%} used)%s' % (self.title, self.name, self.remain, self.ratio, self.warning_message),
  621. '{%s:>7}' % self.name,
  622. '{%s:+}' % self.name,
  623. '({%s:>+7} remain, {%s:>+7} total)' % (self.remain, self.total))
  624. def format_line_csv(self) -> Tuple[str, str, str, str]:
  625. return ('%s,{%s} bytes ({%s} remain {%s:.1%%} used)%s' % (self.title, self.name, self.remain, self.ratio, self.warning_message),
  626. '{%s}' % self.name,
  627. '{%s:+}' % self.name,
  628. '{%s} remain,{%s} total' % (self.remain, self.total))
  629. class TotalLineDef(LineDef):
  630. def format_line(self) -> Tuple[str, str, str, str]:
  631. return ('%16s: {%s:>7} bytes (.bin may be padded larger)' % (self.title, self.name),
  632. '{%s:>7}' % self.name,
  633. '{%s:+}' % self.name,
  634. '')
  635. def format_line_csv(self) -> Tuple[str, str, str, str]:
  636. return ('%s,{%s} bytes (.bin may be padded larger)' % (self.title, self.name),
  637. '{%s}' % self.name,
  638. '{%s:+}' % self.name,
  639. '')
  640. warning_message = ' Overflow detected!' + (' You can run idf.py size-files for more information.' if print_suggestions else '')
  641. format_list = [
  642. HeadLineDef('Used static DRAM', 'used_dram', remain='dram_remain', ratio='used_dram_ratio', total='dram_total',
  643. warning_message=warning_message if current.get_dram_overflowed() else ''),
  644. LineDef('.data size', 'dram_data'),
  645. LineDef('.bss size', 'dram_bss'),
  646. LineDef('.rodata size', 'dram_rodata'),
  647. LineDef('DRAM other size', 'dram_other'),
  648. HeadLineDef('Used static IRAM', 'used_iram', remain='iram_remain', ratio='used_iram_ratio', total='iram_total',
  649. warning_message=warning_message if current.get_iram_overflowed() else ''),
  650. LineDef('.text size', 'iram_text'),
  651. LineDef('.vectors size', 'iram_vectors'),
  652. HeadLineDef('Used stat D/IRAM', 'used_diram', remain='diram_remain', ratio='used_diram_ratio', total='diram_total',
  653. warning_message=warning_message if current.get_diram_overflowed() else ''),
  654. LineDef('.data size', 'diram_data'),
  655. LineDef('.bss size', 'diram_bss'),
  656. LineDef('.text size', 'diram_text'),
  657. LineDef('.vectors size', 'diram_vectors'),
  658. LineDef('.rodata size', 'diram_rodata'),
  659. LineDef('other', 'diram_other'),
  660. LineDef('Used Flash size ', 'used_flash_non_ram'),
  661. LineDef('.text', 'flash_code'),
  662. LineDef('.rodata', 'flash_rodata'),
  663. TotalLineDef('Total image size', 'total_size')
  664. ]
  665. current_dict = current.get_dict()
  666. if diff_en:
  667. reference_dict = reference.get_dict()
  668. diff_dict = (current - reference).get_dict()
  669. if output_format == 'csv':
  670. line_format = '{},{},{},{}'
  671. else:
  672. line_format = '{:60}{:>15}{:>15} {}' # Width for a, b, c, d columns
  673. def print_in_columns(a: str, b: Optional[str]='', c: Optional[str]='', d: Optional[str]='') -> str:
  674. return line_format.format(a, b, c, d).rstrip() + os.linesep
  675. output = ''
  676. if diff_en:
  677. if output_format == 'csv':
  678. output += print_in_columns('','<CURRENT>:', '<REFERENCE>', 'Difference')
  679. output += print_in_columns('File', path, path_diff, '<CURRENT> - <REFERENCE>')
  680. else:
  681. output += print_in_columns('<CURRENT> MAP file: ' + path)
  682. output += print_in_columns('<REFERENCE> MAP file: ' + path_diff)
  683. output += print_in_columns('Difference is counted as <CURRENT> - <REFERENCE>, ',
  684. 'i.e. a positive number means that <CURRENT> is larger.')
  685. output += print_in_columns('Total sizes of <CURRENT>:', '<REFERENCE>', 'Difference')
  686. for line in format_list:
  687. if getattr(current, line.name) > 0 or getattr(reference, line.name) > 0 or line.name == 'total_size':
  688. if output_format == 'csv':
  689. main_string_format, reference_format, sign_format, main_diff_format = line.format_line_csv()
  690. else:
  691. main_string_format, reference_format, sign_format, main_diff_format = line.format_line()
  692. output += print_in_columns(
  693. main_string_format.format(**current_dict),
  694. reference_format.format(**reference_dict),
  695. sign_format.format(**diff_dict) if not sign_format.format(**diff_dict).startswith('+0') else '',
  696. main_diff_format.format(**diff_dict))
  697. else:
  698. output += print_in_columns('Total sizes:')
  699. for line in format_list:
  700. if getattr(current, line.name) > 0 or line.name == 'total_size':
  701. if output_format == 'csv':
  702. main_string_format, _, _, _ = line.format_line_csv()
  703. else:
  704. main_string_format, _, _, _ = line.format_line()
  705. output += print_in_columns(main_string_format.format(**current_dict))
  706. return output
  707. def sort_dict(non_sort_list: List) -> List:
  708. '''
  709. sort with keeping the order data, bss, other, iram, diram, ram_st_total, flash_text, flash_rodata, flash_total
  710. '''
  711. start_of_other = 0
  712. props_sort = [] # type: List
  713. props_elem = ['.data', '.bss', 'other', 'iram', 'diram', 'ram_st_total', 'flash.text', 'flash.rodata', 'flash', 'flash_total']
  714. for i in props_elem:
  715. for j in non_sort_list:
  716. if i == 'other':
  717. # remembering where 'other' will start
  718. start_of_other = len(props_sort)
  719. elif i in j and j not in props_sort:
  720. props_sort.append(j)
  721. for j in non_sort_list:
  722. if j not in props_sort:
  723. # add all item that fit in other in dict
  724. props_sort.insert(start_of_other, j)
  725. return props_sort
  726. class StructureForDetailedSizes(object):
  727. @staticmethod
  728. def sizes_by_key(sections: SectionDict, key: str, include_padding: Optional[bool]=False) -> Dict[str, Dict[str, int]]:
  729. """ Takes a dict of sections (from load_sections) and returns
  730. a dict keyed by 'key' with aggregate output size information.
  731. Key can be either "archive" (for per-archive data) or "file" (for per-file data) in the result.
  732. """
  733. result = {} # type: Dict[str, Dict[str, int]]
  734. for _, section in sections.items():
  735. for s in section['sources']:
  736. if not s[key] in result:
  737. result[s[key]] = {}
  738. archive = result[s[key]]
  739. if not section['name'] in archive:
  740. archive[section['name']] = 0
  741. archive[section['name']] += s['size']
  742. if include_padding:
  743. archive[section['name']] += s['fill']
  744. return result
  745. @staticmethod
  746. def get(sections: SectionDict, by_key: str) -> collections.OrderedDict:
  747. """
  748. Get the detailed structure before using the filter to remove undesired sections,
  749. to show entries without desired sections
  750. """
  751. sizes = StructureForDetailedSizes.sizes_by_key(sections, by_key)
  752. for key_name in sizes:
  753. sizes[key_name] = LinkingSections.filter_sections(sizes[key_name])
  754. s = []
  755. for key, section_dict in sizes.items():
  756. ram_st_total = sum([x[1] for x in section_dict.items() if not LinkingSections.in_section(x[0], 'flash')])
  757. flash_total = sum([x[1] for x in section_dict.items() if not LinkingSections.in_section(x[0], 'bss')]) # type: int
  758. section_dict['ram_st_total'] = ram_st_total
  759. section_dict['flash_total'] = flash_total
  760. sorted_dict = sorted(section_dict.items(), key=lambda elem: elem[0])
  761. s.append((key, collections.OrderedDict(sorted_dict)))
  762. s = sorted(s, key=lambda elem: elem[0])
  763. # do a secondary sort in order to have consistent order (for diff-ing the output)
  764. s = sorted(s, key=lambda elem: elem[1]['flash_total'], reverse=True)
  765. return collections.OrderedDict(s)
  766. def get_detailed_sizes(sections: Dict, key: str, header: str, output_format: str, sections_diff: Dict=None) -> str:
  767. key_name_set = set()
  768. current = StructureForDetailedSizes.get(sections, key)
  769. for section_dict in current.values():
  770. key_name_set.update(section_dict.keys())
  771. if sections_diff:
  772. reference = StructureForDetailedSizes.get(sections_diff, key)
  773. for section_dict in reference.values():
  774. key_name_set.update(section_dict.keys())
  775. diff_en = True
  776. else:
  777. diff_en = False
  778. key_name_list = list(key_name_set)
  779. ordered_key_list, display_name_list = LinkingSections.get_display_name_order(key_name_list)
  780. if output_format == 'json':
  781. if diff_en:
  782. diff_json_dic = collections.OrderedDict()
  783. for name in sorted(list(frozenset(current.keys()) | frozenset(reference.keys()))):
  784. cur_name_dic = current.get(name, {})
  785. ref_name_dic = reference.get(name, {})
  786. all_keys = sorted(list(frozenset(cur_name_dic.keys()) | frozenset(ref_name_dic.keys())))
  787. diff_json_dic[name] = collections.OrderedDict([(k,
  788. cur_name_dic.get(k, 0) -
  789. ref_name_dic.get(k, 0)) for k in all_keys])
  790. output = format_json(collections.OrderedDict([('current', current),
  791. ('reference', reference),
  792. ('diff', diff_json_dic),
  793. ]))
  794. else:
  795. output = format_json(current)
  796. else:
  797. def _get_header_format(disp_list: List=display_name_list, output_format: str='') -> str:
  798. if output_format == 'csv':
  799. return '{},' * (len(disp_list) - 1) + '{}' + os.linesep
  800. len_list = [len(x) for x in disp_list]
  801. len_list.insert(0, 24)
  802. return ' '.join(['{:>%d}' % x for x in len_list]) + os.linesep
  803. def _get_output(data: Dict[str, Dict[str, int]], selection: Collection, output_format: str,
  804. key_list: List=ordered_key_list, disp_list: List=display_name_list) -> str:
  805. header_format = _get_header_format(disp_list, output_format)
  806. output = header_format.format(header, *disp_list)
  807. for key, data_info in data.items():
  808. if key not in selection:
  809. continue
  810. try:
  811. _, key = key.split(':', 1)
  812. # print subheadings for key of format archive:file
  813. except ValueError:
  814. # k remains the same
  815. pass
  816. def get_section_size(section_dict: Dict) -> Callable[[str], int]:
  817. return lambda x: section_dict.get(x, 0)
  818. section_size_list = map(get_section_size(section_dict=data_info), key_list)
  819. output += header_format.format(key[:24], *(section_size_list))
  820. return output
  821. def _get_header_format_diff(disp_list: List=display_name_list, columns: bool=False, output_format: str='') -> str:
  822. if output_format == 'csv':
  823. return '{},' * len(disp_list) + '{}' + os.linesep
  824. if columns:
  825. len_list = (24, ) + (7, ) * 3 * len(disp_list)
  826. return '|'.join(['{:>%d}' % x for x in len_list]) + os.linesep
  827. len_list = (24, ) + (23, ) * len(disp_list)
  828. return ' '.join(['{:>%d}' % x for x in len_list]) + os.linesep
  829. def _get_output_diff(curr: Dict, ref: Dict, output_format: str, key_list: List=ordered_key_list, disp_list: List=display_name_list) -> str:
  830. # First header without Current/Ref/Diff columns
  831. header_format = _get_header_format_diff(columns=False, output_format=output_format)
  832. output = header_format.format(header, *disp_list)
  833. f_print = ('-' * 23, '') * len(key_list)
  834. f_print = f_print[0:len(key_list)]
  835. header_line = header_format.format('', *f_print)
  836. header_format = _get_header_format_diff(columns=True, output_format=output_format)
  837. f_print = ('<C>', '<R>', '<C>-<R>') * len(key_list)
  838. output += header_format.format('', *f_print)
  839. if output_format != 'csv':
  840. output += header_line
  841. for key, data_info in curr.items():
  842. try:
  843. v2 = ref[key]
  844. except KeyError:
  845. continue
  846. try:
  847. _, key = key.split(':', 1)
  848. # print subheadings for key of format archive:file
  849. except ValueError:
  850. # k remains the same
  851. pass
  852. def _get_items(name: str, section_dict: Dict=data_info, section_dict_ref: Dict=v2) -> Tuple[str, str, str]:
  853. a = section_dict.get(name, 0)
  854. b = section_dict_ref.get(name, 0)
  855. diff = a - b
  856. # the sign is added here and not in header_format in order to be able to print empty strings
  857. return (a or '', b or '', '' if diff == 0 else '{:+}'.format(diff))
  858. x = [] # type: List[str]
  859. for section in key_list:
  860. x.extend(_get_items(section))
  861. output += header_format.format(key[:24], *(x))
  862. return output
  863. output = 'Per-{} contributions to ELF file:{}'.format(key, os.linesep)
  864. if diff_en:
  865. output += _get_output_diff(current, reference, output_format)
  866. in_current = frozenset(current.keys())
  867. in_reference = frozenset(reference.keys())
  868. only_in_current = in_current - in_reference
  869. only_in_reference = in_reference - in_current
  870. if len(only_in_current) > 0:
  871. output += 'The following entries are present in <CURRENT> only:{}'.format(os.linesep)
  872. output += _get_output(current, only_in_current, output_format)
  873. if len(only_in_reference) > 0:
  874. output += 'The following entries are present in <REFERENCE> only:{}'.format(os.linesep)
  875. output += _get_output(reference, only_in_reference, output_format)
  876. else:
  877. output += _get_output(current, current, output_format)
  878. return output
  879. class StructureForArchiveSymbols(object):
  880. @staticmethod
  881. def get(archive: str, sections: Dict) -> Dict:
  882. interested_sections = LinkingSections.filter_sections(sections)
  883. result = dict([(t, {}) for t in interested_sections]) # type: Dict[str, Dict[str, int]]
  884. for _, section in sections.items():
  885. section_name = section['name']
  886. if section_name not in interested_sections:
  887. continue
  888. for s in section['sources']:
  889. if archive != s['archive']:
  890. continue
  891. s['sym_name'] = re.sub('(.text.|.literal.|.data.|.bss.|.rodata.)', '', s['sym_name'])
  892. result[section_name][s['sym_name']] = result[section_name].get(s['sym_name'], 0) + s['size']
  893. # build a new ordered dict of each section, where each entry is an ordereddict of symbols to sizes
  894. section_symbols = collections.OrderedDict()
  895. for t in sorted(list(interested_sections)):
  896. s = sorted(result[t].items(), key=lambda k_v: str(k_v[0]))
  897. # do a secondary sort in order to have consistent order (for diff-ing the output)
  898. s = sorted(s, key=lambda k_v: int(k_v[1]), reverse=True)
  899. section_symbols[t] = collections.OrderedDict(s)
  900. return section_symbols
  901. def get_archive_symbols(sections: Dict, archive: str, output_format: str, sections_diff: Dict=None) -> str:
  902. diff_en = bool(sections_diff)
  903. current = StructureForArchiveSymbols.get(archive, sections)
  904. reference = StructureForArchiveSymbols.get(archive, sections_diff) if sections_diff else {}
  905. if output_format == 'json':
  906. if diff_en:
  907. diff_json_dic = collections.OrderedDict()
  908. for name in sorted(list(frozenset(current.keys()) | frozenset(reference.keys()))):
  909. cur_name_dic = current.get(name, {})
  910. ref_name_dic = reference.get(name, {})
  911. all_keys = sorted(list(frozenset(cur_name_dic.keys()) | frozenset(ref_name_dic.keys())))
  912. diff_json_dic[name] = collections.OrderedDict([(key,
  913. cur_name_dic.get(key, 0) -
  914. ref_name_dic.get(key, 0)) for key in all_keys])
  915. output = format_json(collections.OrderedDict([('current', current),
  916. ('reference', reference),
  917. ('diff', diff_json_dic),
  918. ]))
  919. else:
  920. output = format_json(current)
  921. else:
  922. def _get_item_pairs(name: str, section: collections.OrderedDict) -> collections.OrderedDict:
  923. return collections.OrderedDict([(key.replace(name + '.', ''), val) for key, val in section.items()])
  924. def _get_max_len(symbols_dict: Dict) -> Tuple[int, int]:
  925. # the lists have 0 in them because max() doesn't work with empty lists
  926. names_max_len = 0
  927. numbers_max_len = 0
  928. for t, s in symbols_dict.items():
  929. numbers_max_len = max([numbers_max_len, *[len(str(x)) for _, x in s.items()]])
  930. names_max_len = max([names_max_len, *[len(x) for x in _get_item_pairs(t, s)]])
  931. return names_max_len, numbers_max_len
  932. def _get_output(section_symbols: Dict) -> str:
  933. output = ''
  934. names_max_len, numbers_max_len = _get_max_len(section_symbols)
  935. if output_format == 'csv':
  936. line_format = '{},{}' + os.linesep
  937. else:
  938. line_format = ' {:<%d} : {:>%d}' % (names_max_len,numbers_max_len) + os.linesep
  939. for t, s in section_symbols.items():
  940. output += '{}Symbols from section: {}{}'.format(os.linesep, t, os.linesep)
  941. item_pairs = _get_item_pairs(t, s)
  942. for key, val in item_pairs.items():
  943. output += line_format.format(key, val)
  944. section_total = sum([val for _, val in item_pairs.items()])
  945. output += 'Section total: {}{}'.format(section_total, os.linesep)
  946. return output
  947. output = '{}Symbols within the archive: {} (Not all symbols may be reported){}'.format(os.linesep, archive, os.linesep)
  948. if diff_en:
  949. def _generate_line_tuple(curr: collections.OrderedDict, ref: collections.OrderedDict, name: str, indent: str) -> Tuple[str, int, int, str]:
  950. cur_val = curr.get(name, 0)
  951. ref_val = ref.get(name, 0)
  952. diff_val = cur_val - ref_val
  953. # string slicing is used just to make sure it will fit into the first column of line_format
  954. return ((indent + name)[:40], cur_val, ref_val, '' if diff_val == 0 else '{:+}'.format(diff_val))
  955. if output_format == 'csv':
  956. line_format = '{},{},{},{}'
  957. else:
  958. line_format = '{:40} {:>12} {:>12} {:>25}'
  959. all_section_names = sorted(list(frozenset(current.keys()) | frozenset(reference.keys())))
  960. for section_name in all_section_names:
  961. current_item_pairs = _get_item_pairs(section_name, current.get(section_name, {}))
  962. reference_item_pairs = _get_item_pairs(section_name, reference.get(section_name, {}))
  963. output += os.linesep + line_format.format(section_name[:40],
  964. '<CURRENT>',
  965. '<REFERENCE>',
  966. '<CURRENT> - <REFERENCE>') + os.linesep
  967. current_section_total = sum([val for _, val in current_item_pairs.items()])
  968. reference_section_total = sum([val for _, val in reference_item_pairs.items()])
  969. diff_section_total = current_section_total - reference_section_total
  970. all_item_names = sorted(list(frozenset(current_item_pairs.keys()) |
  971. frozenset(reference_item_pairs.keys())))
  972. indent = 4 * ' ' if output_format != 'csv' else ''
  973. output += os.linesep.join(line_format.format(*_generate_line_tuple(current_item_pairs,
  974. reference_item_pairs,
  975. n,
  976. indent)
  977. ).rstrip() for n in all_item_names)
  978. output += os.linesep if current_section_total > 0 or reference_section_total > 0 else ''
  979. output += line_format.format('Section total:',
  980. current_section_total,
  981. reference_section_total,
  982. '' if diff_section_total == 0 else '{:+}'.format(diff_section_total)
  983. ).rstrip() + os.linesep
  984. else:
  985. output += _get_output(current)
  986. return output
  987. if __name__ == '__main__':
  988. main()