1# 2# Copyright (c) 2016-2025, Arm Limited. All rights reserved. 3# 4# SPDX-License-Identifier: Apache-2.0 5# 6 7import json 8import os 9import re 10from collections import defaultdict 11from copy import deepcopy 12from os.path import ( 13 abspath, 14 basename, 15 commonprefix, 16 dirname, 17 join, 18 relpath, 19 splitext, 20) 21from pathlib import Path 22from sys import stdout 23from typing import IO, Any, Dict, List, Optional, Pattern, Tuple, Union 24 25from jinja2 import FileSystemLoader, StrictUndefined 26from jinja2.environment import Environment 27from prettytable import HEADER, PrettyTable 28 29ModuleStats = Dict[str, int] 30Modules = Dict[str, ModuleStats] 31 32SECTIONS: Tuple[str, ...] = (".text", ".data", ".bss", ".heap", ".stack") 33MISC_FLASH_SECTIONS: Tuple[str, ...] = (".interrupts", ".flash_config") 34OTHER_SECTIONS: Tuple[str, ...] = ( 35 ".interrupts_ram", 36 ".init", 37 ".ARM.extab", 38 ".ARM.exidx", 39 ".ARM.attributes", 40 ".eh_frame", 41 ".init_array", 42 ".fini_array", 43 ".jcr", 44 ".stab", 45 ".stabstr", 46 ".ARM.exidx", 47 ".ARM", 48) 49ALL_SECTIONS: Tuple[str, ...] = ( 50 SECTIONS + OTHER_SECTIONS + MISC_FLASH_SECTIONS + ("unknown", "OUTPUT") 51) 52 53 54class Parser: 55 """Internal interface for parsing""" 56 57 _RE_OBJECT_FILE: Pattern[str] = re.compile(r"^(.+\/.+\.o(bj)?)$") 58 _RE_LIBRARY_OBJECT: Pattern[str] = re.compile( 59 r"((^.+" + r"" + r"lib.+\.a)\((.+\.o(bj)?)\))$" 60 ) 61 _RE_STD_SECTION: Pattern[str] = re.compile(r"^\s+.*0x(\w{8,16})\s+0x(\w+)\s(.+)$") 62 _RE_FILL_SECTION: Pattern[str] = re.compile( 63 r"^\s*\*fill\*\s+0x(\w{8,16})\s+0x(\w+).*$" 64 ) 65 _RE_TRANS_FILE: Pattern[str] = re.compile(r"^(.+\/|.+\.ltrans.o(bj)?)$") 66 _OBJECT_EXTENSIONS: Tuple[str, ...] = (".o", ".obj") 67 68 _modules: Modules 69 _fill: bool 70 71 def __init__(self, fill: bool = True): 72 self._modules: Modules = {} 73 self._fill = fill 74 75 def module_add(self, object_name: str, size: int, section: str): 76 """Adds a module or section to the list 77 78 Positional arguments: 79 object_name - name of the entry to add 80 size - the size of the module being added 81 section - the section the module contributes to 82 """ 83 if ( 84 not object_name 85 or not size 86 or not section 87 or (not self._fill and object_name == "[fill]") 88 ): 89 return 90 91 if object_name in self._modules: 92 self._modules[object_name].setdefault(section, 0) 93 self._modules[object_name][section] += size 94 return 95 96 obj_split = os.sep + basename(object_name) 97 for module_path, contents in self._modules.items(): 98 if module_path.endswith(obj_split) or module_path == object_name: 99 contents.setdefault(section, 0) 100 contents[section] += size 101 return 102 103 new_module: ModuleStats = defaultdict(int) 104 new_module[section] = size 105 self._modules[object_name] = new_module 106 107 def module_replace(self, old_object: str, new_object: str): 108 """Replaces an object name with a new one""" 109 if old_object in self._modules: 110 self._modules[new_object] = self._modules.pop(old_object) 111 112 def check_new_section(self, line: str) -> Optional[str]: 113 """Check whether a new section in a map file has been detected 114 115 Positional arguments: 116 line - the line to check for a new section 117 118 return value - A section name, if a new section was found, None 119 otherwise 120 """ 121 line_s = line.strip() 122 for i in ALL_SECTIONS: 123 if line_s.startswith(i): 124 return i 125 if line.startswith("."): 126 return "unknown" 127 else: 128 return None 129 130 def parse_object_name(self, line: str) -> str: 131 """Parse a path to object file 132 133 Positional arguments: 134 line - the path to parse the object and module name from 135 136 return value - an object file name 137 """ 138 if re.match(self._RE_TRANS_FILE, line): 139 return "[misc]" 140 141 test_re_file_name = re.match(self._RE_OBJECT_FILE, line) 142 143 if test_re_file_name: 144 object_name = test_re_file_name.group(1) 145 146 return object_name 147 else: 148 test_re_obj_name = re.match(self._RE_LIBRARY_OBJECT, line) 149 150 if test_re_obj_name: 151 return join(test_re_obj_name.group(2), test_re_obj_name.group(3)) 152 else: 153 if not line.startswith("LONG") and not line.startswith("linker stubs"): 154 print("Unknown object name found in GCC map file: %s" % line) 155 return "[misc]" 156 157 def parse_section(self, line: str) -> Tuple[str, int]: 158 """Parse data from a section of gcc map file 159 160 examples: 161 0x00004308 0x7c ./BUILD/K64F/GCC_ARM/spi_api.o 162 .text 0x00000608 0x198 ./BUILD/K64F/HAL_CM4.o 163 164 Positional arguments: 165 line - the line to parse a section from 166 """ 167 is_fill = re.match(self._RE_FILL_SECTION, line) 168 if is_fill: 169 o_name: str = "[fill]" 170 o_size: int = int(is_fill.group(2), 16) 171 return o_name, o_size 172 173 is_section = re.match(self._RE_STD_SECTION, line) 174 if is_section: 175 o_size: int = int(is_section.group(2), 16) 176 if o_size: 177 o_name: str = self.parse_object_name(is_section.group(3)) 178 return o_name, o_size 179 180 return "", 0 181 182 def parse_mapfile(self, file_desc: IO[str]) -> Modules: 183 """Main logic to decode gcc map files 184 185 Positional arguments: 186 file_desc - a stream object to parse as a gcc map file 187 """ 188 current_section: str = "unknown" 189 190 with file_desc as infile: 191 for line in infile: 192 if line.startswith("Linker script and memory map"): 193 current_section = "unknown" 194 break 195 196 for line in infile: 197 next_section = self.check_new_section(line) 198 199 if next_section == "OUTPUT": 200 break 201 elif next_section: 202 current_section = next_section 203 204 object_name, object_size = self.parse_section(line) 205 self.module_add(object_name, object_size, current_section) 206 207 def is_obj(name: str) -> bool: 208 return not name.startswith("[") or not name.endswith("]") 209 210 common_prefix: str = dirname( 211 commonprefix([o for o in self._modules.keys() if is_obj(o)]) 212 ) 213 new_modules: Modules = {} 214 for name, stats in self._modules.items(): 215 if is_obj(name): 216 new_modules[relpath(name, common_prefix)] = stats 217 else: 218 new_modules[name] = stats 219 return new_modules 220 221 222class MapParser(object): 223 """An object that represents parsed results, parses the memory map files, 224 and writes out different file types of memory results 225 """ 226 227 print_sections: Tuple[str, ...] = (".text", ".data", ".bss") 228 delta_sections: Tuple[str, ...] = (".text-delta", ".data-delta", ".bss-delta") 229 230 # sections to print info (generic for all toolchains) 231 sections: Tuple[str, ...] = SECTIONS 232 misc_flash_sections: Tuple[str, ...] = MISC_FLASH_SECTIONS 233 other_sections: Tuple[str, ...] = OTHER_SECTIONS 234 235 modules: Modules 236 old_modules: Modules 237 short_modules: Modules 238 mem_report: List[Dict[str, Union[str, ModuleStats]]] 239 mem_summary: Dict[str, int] 240 subtotal: Dict[str, int] 241 tc_name: Optional[str] 242 243 RAM_FORMAT_STR: str = "Total Static RAM memory (data + bss): {}({:+}) bytes\n" 244 ROM_FORMAT_STR: str = "Total Flash memory (text + data): {}({:+}) bytes\n" 245 246 def __init__(self): 247 # list of all modules and their sections 248 # full list - doesn't change with depth 249 self.modules: Modules = {} 250 self.old_modules = {} 251 # short version with specific depth 252 self.short_modules: Modules = {} 253 254 # Memory report (sections + summary) 255 self.mem_report: List[Dict[str, Union[str, ModuleStats]]] = [] 256 257 # Memory summary 258 self.mem_summary: Dict[str, int] = {} 259 260 # Totals of ".text", ".data" and ".bss" 261 self.subtotal: Dict[str, int] = {} 262 263 # Name of the toolchain, for better headings 264 self.tc_name = None 265 266 def reduce_depth(self, depth: Optional[int]): 267 """ 268 populates the short_modules attribute with a truncated module list 269 270 (1) depth = 1: 271 main.o 272 mbed-os 273 274 (2) depth = 2: 275 main.o 276 mbed-os/test.o 277 mbed-os/drivers 278 279 """ 280 if depth == 0 or depth is None: 281 self.short_modules = deepcopy(self.modules) 282 else: 283 self.short_modules = dict() 284 for module_name, v in self.modules.items(): 285 split_name = module_name.split(os.sep) 286 if split_name[0] == "": 287 split_name = split_name[1:] 288 new_name = join(*split_name[:depth]) 289 self.short_modules.setdefault(new_name, defaultdict(int)) 290 for section_idx, value in v.items(): 291 self.short_modules[new_name][section_idx] += value 292 delta_name = section_idx + "-delta" 293 self.short_modules[new_name][delta_name] += value 294 295 for module_name, v in self.old_modules.items(): 296 split_name = module_name.split(os.sep) 297 if split_name[0] == "": 298 split_name = split_name[1:] 299 new_name = join(*split_name[:depth]) 300 self.short_modules.setdefault(new_name, defaultdict(int)) 301 for section_idx, value in v.items(): 302 delta_name = section_idx + "-delta" 303 self.short_modules[new_name][delta_name] -= value 304 305 export_formats: List[str] = ["json", "html", "table"] 306 307 def generate_output( 308 self, 309 export_format: str, 310 depth: Optional[int], 311 file_output: Optional[str] = None, 312 ) -> Optional[bool]: 313 """Generates summary of memory map data 314 315 Positional arguments: 316 export_format - the format to dump 317 318 Keyword arguments: 319 file_desc - descriptor (either stdout or file) 320 depth - directory depth on report 321 322 Returns: generated string for the 'table' format, otherwise Nonef 323 """ 324 if depth is None or depth > 0: 325 self.reduce_depth(depth) 326 self.compute_report() 327 try: 328 if file_output: 329 file_desc = open(file_output, "w") 330 else: 331 file_desc = stdout 332 except IOError as error: 333 print("I/O error({0}): {1}".format(error.errno, error.strerror)) 334 return False 335 336 to_call = { 337 "json": self.generate_json, 338 "html": self.generate_html, 339 "table": self.generate_table, 340 }[export_format] 341 to_call(file_desc) 342 343 if file_desc is not stdout: 344 file_desc.close() 345 346 @staticmethod 347 def _move_up_tree(tree: Dict[str, Any], next_module: str) -> Dict[str, Any]: 348 tree.setdefault("children", []) 349 for child in tree["children"]: 350 if child["name"] == next_module: 351 return child 352 353 new_module = {"name": next_module, "value": 0, "delta": 0} 354 tree["children"].append(new_module) 355 356 return new_module 357 358 def generate_html(self, file_desc: IO[str]): 359 """Generate a json file from a memory map for D3 360 361 Positional arguments: 362 file_desc - the file to write out the final report to 363 """ 364 365 tree_text = {"name": ".text", "value": 0, "delta": 0} 366 tree_bss = {"name": ".bss", "value": 0, "delta": 0} 367 tree_data = {"name": ".data", "value": 0, "delta": 0} 368 369 def accumulate(tree_root: Dict[str, Any], size_key: str, stats: ModuleStats): 370 parts = module_name.split(os.sep) 371 372 val = stats.get(size_key, 0) 373 tree_root["value"] += val 374 tree_root["delta"] += val 375 376 cur = tree_root 377 for part in parts: 378 cur = self._move_up_tree(cur, part) 379 cur["value"] += val 380 cur["delta"] += val 381 382 def subtract(tree_root: Dict[str, Any], size_key: str, stats: ModuleStats): 383 parts = module_name.split(os.sep) 384 385 cur = tree_root 386 cur["delta"] -= stats.get(size_key, 0) 387 388 for part in parts: 389 children = {c["name"]: c for c in cur.get("children", [])} 390 if part not in children: 391 return 392 393 cur = children[part] 394 cur["delta"] -= stats.get(size_key, 0) 395 396 for module_name, dct in self.modules.items(): 397 accumulate(tree_text, ".text", dct) 398 accumulate(tree_data, ".data", dct) 399 accumulate(tree_bss, ".bss", dct) 400 401 for module_name, dct in self.old_modules.items(): 402 subtract(tree_text, ".text", dct) 403 subtract(tree_data, ".data", dct) 404 subtract(tree_bss, ".bss", dct) 405 406 jinja_loader = FileSystemLoader(dirname(abspath(__file__))) 407 jinja_environment = Environment(loader=jinja_loader, undefined=StrictUndefined) 408 template = jinja_environment.get_template("templates/summary-flamegraph.html") 409 410 name, _ = splitext(basename(file_desc.name)) 411 412 if name.endswith("_map"): 413 name = name[:-4] 414 if self.tc_name: 415 name = f"{name} {self.tc_name}" 416 417 file_desc.write( 418 template.render( 419 { 420 "name": name, 421 "rom": json.dumps( 422 { 423 "name": "ROM", 424 "value": tree_text["value"] + tree_data["value"], 425 "delta": tree_text["delta"] + tree_data["delta"], 426 "children": [tree_text, tree_data], 427 } 428 ), 429 "ram": json.dumps( 430 { 431 "name": "RAM", 432 "value": tree_bss["value"] + tree_data["value"], 433 "delta": tree_bss["delta"] + tree_data["delta"], 434 "children": [tree_bss, tree_data], 435 } 436 ), 437 } 438 ) 439 ) 440 441 def generate_json(self, file_desc: IO[str]): 442 """Generate a json file from a memory map 443 444 Positional arguments: 445 file_desc - the file to write out the final report to 446 """ 447 file_desc.write(json.dumps(self.mem_report, indent=4)) 448 file_desc.write("\n") 449 450 def generate_table(self, file_desc: IO[str]): 451 """Generate a table from a memory map 452 453 Returns: string of the generated table 454 """ 455 # Create table 456 columns = ["Module"] 457 columns.extend(self.print_sections) 458 459 table = PrettyTable(columns, junction_char="|", hrules=HEADER) 460 table.align["Module"] = "l" 461 462 for col in self.print_sections: 463 table.align[col] = "r" 464 465 for i in sorted(self.short_modules): 466 row = [i] 467 468 for k in self.print_sections: 469 row.append( 470 "{}({:+})".format( 471 self.short_modules[i][k], self.short_modules[i][k + "-delta"] 472 ) 473 ) 474 475 table.add_row(row) 476 477 subtotal_row = ["Subtotals"] 478 for k in self.print_sections: 479 subtotal_row.append( 480 "{}({:+})".format(self.subtotal[k], self.subtotal[k + "-delta"]) 481 ) 482 483 table.add_row(subtotal_row) 484 485 output = table.get_string() 486 output += "\n" 487 488 output += self.RAM_FORMAT_STR.format( 489 self.mem_summary["static_ram"], self.mem_summary["static_ram_delta"] 490 ) 491 output += self.ROM_FORMAT_STR.format( 492 self.mem_summary["total_flash"], self.mem_summary["total_flash_delta"] 493 ) 494 file_desc.write(output) 495 496 def compute_report(self): 497 """Generates summary of memory usage for main areas""" 498 self.subtotal = defaultdict(int) 499 500 for mod in self.modules.values(): 501 for k in self.sections: 502 self.subtotal[k] += mod[k] 503 self.subtotal[k + "-delta"] += mod[k] 504 505 for mod in self.old_modules.values(): 506 for k in self.sections: 507 self.subtotal[k + "-delta"] -= mod[k] 508 509 self.mem_summary = { 510 "static_ram": self.subtotal[".data"] + self.subtotal[".bss"], 511 "static_ram_delta": self.subtotal[".data-delta"] 512 + self.subtotal[".bss-delta"], 513 "total_flash": (self.subtotal[".text"] + self.subtotal[".data"]), 514 "total_flash_delta": self.subtotal[".text-delta"] 515 + self.subtotal[".data-delta"], 516 } 517 518 self.mem_report = [] 519 if self.short_modules: 520 for name, sizes in sorted(self.short_modules.items()): 521 self.mem_report.append( 522 { 523 "module": name, 524 "size": { 525 k: sizes.get(k, 0) 526 for k in (self.print_sections + self.delta_sections) 527 }, 528 } 529 ) 530 531 self.mem_report.append({"summary": self.mem_summary}) 532 533 def parse( 534 self, mapfile: Path, oldfile: Optional[Path] = None, no_fill: bool = False 535 ) -> bool: 536 """Parse and decode map file depending on the toolchain 537 538 Positional arguments: 539 mapfile - the file name of the memory map file 540 toolchain - the toolchain used to create the file 541 """ 542 try: 543 with open(mapfile, "r") as file_input: 544 self.modules = Parser(not no_fill).parse_mapfile(file_input) 545 try: 546 if oldfile is not None: 547 with open(oldfile, "r") as old_input: 548 self.old_modules = Parser(not no_fill).parse_mapfile(old_input) 549 else: 550 self.old_modules = self.modules 551 except IOError: 552 self.old_modules = {} 553 return True 554 555 except IOError as error: 556 print("I/O error({0}): {1}".format(error.errno, error.strerror)) 557 return False 558