| #!/usr/bin/env python3 |
| # |
| # Copyright (c) 2016, Intel Corporation |
| # |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| # Based on a script by: |
| # Chereau, Fabien <fabien.chereau@intel.com> |
| |
| import os |
| import re |
| import argparse |
| import subprocess |
| import json |
| import operator |
| import platform |
| from pathlib import Path |
| |
| |
| # Return a dict containing { |
| # symbol_name: {:,path/to/file}/symbol |
| # } |
| # for all symbols from the .elf file. Optionaly strips the path according |
| # to the passed sub-path |
| def load_symbols_and_paths(bin_nm, elf_file, path_to_strip=""): |
| nm_out = subprocess.check_output( |
| [bin_nm, elf_file, "-S", "-l", "--size-sort", "--radix=d"], |
| universal_newlines=True |
| ) |
| for line in nm_out.splitlines(): |
| if not line: |
| # Get rid of trailing empty field |
| continue |
| |
| symbol, path = parse_symbol_path_pair(line) |
| |
| if path: |
| p_path = Path(path) |
| p_path_to_strip = Path(path_to_strip) |
| try: |
| processed_path = p_path.relative_to(p_path_to_strip) |
| except ValueError: |
| # path is valid, but is not prefixed by path_to_strip |
| processed_path = p_path |
| else: |
| processed_path = Path(":") |
| |
| pathlike_string = processed_path / symbol |
| |
| yield symbol, pathlike_string |
| |
| # Return a pair containing either |
| # |
| # (symbol_name, "path/to/file") |
| # or |
| # (symbol_name, "") |
| # or |
| # ("", "") |
| # |
| # depending on whether the symbol name and the file are found or not |
| # } |
| def parse_symbol_path_pair(line): |
| # Line's output from nm might look like this: |
| # '536871152 00000012 b gpio_e /absolute/path/gpio.c:247' |
| # |
| # We are only trying to extract the symbol and the filename. |
| # |
| # In general lines look something like this: |
| # |
| # 'number number string[\t<symbol>][\t<absolute_path>:line] |
| # |
| # The symbol and file is optional, nm might not find out what a |
| # symbol is named or where it came from. |
| # |
| # NB: <absolute_path> looks different on Windows and Linux |
| |
| # Replace tabs with spaces to easily split up the fields (NB: |
| # Whitespace in paths is not supported) |
| line_without_tabs = line.replace('\t', ' ') |
| |
| fields = line_without_tabs.split() |
| |
| assert len(fields) >= 3 |
| |
| # When a symbol has been stripped, it's symbol name does not show |
| # in the 'nm' output, but it is still listed as something that |
| # takes up space. We use the empty string to denote these stripped |
| # symbols. |
| symbol_is_missing = len(fields) < 4 |
| if symbol_is_missing: |
| symbol = "" |
| else: |
| symbol = fields[3] |
| |
| |
| file_is_missing = len(fields) < 5 |
| if file_is_missing: |
| path = "" |
| else: |
| path_with_line_number = fields[4] |
| |
| # Remove the trailing line number, e.g. 'C:\file.c:237' |
| line_number_index = path_with_line_number.rfind(':') |
| path = path_with_line_number[:line_number_index] |
| |
| return (symbol, path) |
| |
| |
| def get_section_size(f, section_name): |
| decimal_size = 0 |
| re_res = re.search(r"(.*] " + section_name + ".*)", f, re.MULTILINE) |
| if re_res is not None: |
| # Replace multiple spaces with one space |
| # Skip first characters to avoid having 1 extra random space |
| res = ' '.join(re_res.group(1).split())[5:] |
| decimal_size = int(res.split()[4], 16) |
| return decimal_size |
| |
| |
| def get_footprint_from_bin_and_statfile( |
| bin_file, stat_file, total_flash, total_ram): |
| """Compute flash and RAM memory footprint from a .bin and .stat file""" |
| f = open(stat_file).read() |
| |
| # Get kctext + text + ctors + rodata + kcrodata segment size |
| total_used_flash = os.path.getsize(bin_file) |
| |
| # getting used ram on target |
| total_used_ram = (get_section_size(f, "noinit") + |
| get_section_size(f, "bss") + |
| get_section_size(f, "initlevel") + |
| get_section_size(f, "datas") + |
| get_section_size(f, ".data") + |
| get_section_size(f, ".heap") + |
| get_section_size(f, ".stack") + |
| get_section_size(f, ".bss") + |
| get_section_size(f, ".panic_section")) |
| |
| total_percent_ram = 0 |
| total_percent_flash = 0 |
| if total_ram > 0: |
| total_percent_ram = float(total_used_ram) / total_ram * 100 |
| if total_flash > 0: |
| total_percent_flash = float(total_used_flash) / total_flash * 100 |
| |
| res = {"total_flash": total_used_flash, |
| "percent_flash": total_percent_flash, |
| "total_ram": total_used_ram, |
| "percent_ram": total_percent_ram} |
| return res |
| |
| |
| def generate_target_memory_section( |
| bin_objdump, bin_nm, out, kernel_name, source_dir, features_json): |
| try: |
| json.loads(open(features_json, 'r').read()) |
| except BaseException: |
| pass |
| |
| bin_file_abs = os.path.join(out, kernel_name + '.bin') |
| elf_file_abs = os.path.join(out, kernel_name + '.elf') |
| |
| # First deal with size on flash. These are the symbols flagged as LOAD in |
| # objdump output |
| size_out = subprocess.check_output( |
| [bin_objdump, "-hw", elf_file_abs], |
| universal_newlines=True |
| ) |
| loaded_section_total = 0 |
| loaded_section_names = [] |
| loaded_section_names_sizes = {} |
| ram_section_total = 0 |
| ram_section_names = [] |
| ram_section_names_sizes = {} |
| for line in size_out.splitlines(): |
| if "LOAD" in line: |
| loaded_section_total = loaded_section_total + \ |
| int(line.split()[2], 16) |
| loaded_section_names.append(line.split()[1]) |
| loaded_section_names_sizes[line.split()[1]] = int( |
| line.split()[2], 16) |
| if "ALLOC" in line and "READONLY" not in line and "rodata" not in line and "CODE" not in line: |
| ram_section_total = ram_section_total + int(line.split()[2], 16) |
| ram_section_names.append(line.split()[1]) |
| ram_section_names_sizes[line.split()[1]] = int(line.split()[2], 16) |
| |
| # Actual .bin size, which doesn't not always match section sizes |
| bin_size = os.stat(bin_file_abs).st_size |
| |
| # Get the path associated to each symbol |
| symbols_paths = dict(load_symbols_and_paths(bin_nm, elf_file_abs, source_dir)) |
| |
| # A set of helper function for building a simple tree with a path-like |
| # hierarchy. |
| def _insert_one_elem(tree, path, size): |
| cur = None |
| for p in path.parts: |
| if cur is None: |
| cur = p |
| else: |
| cur = cur + os.path.sep + p |
| if cur in tree: |
| tree[cur] += size |
| else: |
| tree[cur] = size |
| |
| def _parent_for_node(e): |
| parent = "root" if len(os.path.sep) == 1 else e.rsplit(os.path.sep, 1)[0] |
| if e == "root": |
| parent = None |
| return parent |
| |
| def _childs_for_node(tree, node): |
| res = [] |
| for e in tree: |
| if _parent_for_node(e) == node: |
| res += [e] |
| return res |
| |
| def _siblings_for_node(tree, node): |
| return _childs_for_node(tree, _parent_for_node(node)) |
| |
| def _max_sibling_size(tree, node): |
| siblings = _siblings_for_node(tree, node) |
| return max([tree[e] for e in siblings]) |
| |
| # Extract the list of symbols a second time but this time using the objdump tool |
| # which provides more info as nm |
| |
| symbols_out = subprocess.check_output( |
| [bin_objdump, "-tw", elf_file_abs], |
| universal_newlines=True |
| ) |
| flash_symbols_total = 0 |
| data_nodes = {} |
| data_nodes['root'] = 0 |
| |
| ram_symbols_total = 0 |
| ram_nodes = {} |
| ram_nodes['root'] = 0 |
| for l in symbols_out.splitlines(): |
| line = l[0:9] + "......." + l[16:] |
| fields = line.replace('\t', ' ').split(' ') |
| # Get rid of trailing empty field |
| if len(fields) != 5: |
| continue |
| size = int(fields[3], 16) |
| if fields[2] in loaded_section_names and size != 0: |
| flash_symbols_total += size |
| _insert_one_elem(data_nodes, symbols_paths[fields[4]], size) |
| if fields[2] in ram_section_names and size != 0: |
| ram_symbols_total += size |
| _insert_one_elem(ram_nodes, symbols_paths[fields[4]], size) |
| |
| def _init_features_list_results(features_list): |
| for feature in features_list: |
| _init_feature_results(feature) |
| |
| def _init_feature_results(feature): |
| feature["size"] = 0 |
| # recursive through children |
| for child in feature["children"]: |
| _init_feature_results(child) |
| |
| def _check_all_symbols(symbols_struct, features_list): |
| out = "" |
| sorted_nodes = sorted(symbols_struct.items(), |
| key=operator.itemgetter(0)) |
| named_symbol_filter = re.compile(r'.*\.[a-zA-Z]+/.*') |
| out_symbols_filter = re.compile('^:/') |
| for symbpath in sorted_nodes: |
| matched = 0 |
| # The files and folders (not matching regex) are discarded |
| # like: folder folder/file.ext |
| is_symbol = named_symbol_filter.match(symbpath[0]) |
| is_generated = out_symbols_filter.match(symbpath[0]) |
| if is_symbol is None and is_generated is None: |
| continue |
| # The symbols inside a file are kept: folder/file.ext/symbol |
| # and unrecognized paths too (":/") |
| for feature in features_list: |
| matched = matched + \ |
| _does_symbol_matches_feature( |
| symbpath[0], symbpath[1], feature) |
| if matched == 0: |
| out += "UNCATEGORIZED: %s %d<br/>" % (symbpath[0], symbpath[1]) |
| return out |
| |
| def _does_symbol_matches_feature(symbol, size, feature): |
| matched = 0 |
| # check each include-filter in feature |
| for inc_path in feature["folders"]: |
| # filter out if the include-filter is not in the symbol string |
| if inc_path not in symbol: |
| continue |
| # if the symbol match the include-filter, check against |
| # exclude-filter |
| is_excluded = 0 |
| for exc_path in feature["excludes"]: |
| if exc_path in symbol: |
| is_excluded = 1 |
| break |
| if is_excluded == 0: |
| matched = 1 |
| feature["size"] = feature["size"] + size |
| # it can only be matched once per feature (add size once) |
| break |
| # check children independently of this feature's result |
| for child in feature["children"]: |
| child_matched = _does_symbol_matches_feature(symbol, size, child) |
| matched = matched + child_matched |
| return matched |
| |
| # Create a simplified tree keeping only the most important contributors |
| # This is used for the pie diagram summary |
| min_parent_size = bin_size / 25 |
| min_sibling_size = bin_size / 35 |
| tmp = {} |
| for e in data_nodes: |
| if _parent_for_node(e) is None: |
| continue |
| if data_nodes[_parent_for_node(e)] < min_parent_size: |
| continue |
| if _max_sibling_size(data_nodes, e) < min_sibling_size: |
| continue |
| tmp[e] = data_nodes[e] |
| |
| # Keep only final nodes |
| tmp2 = {} |
| for e in tmp: |
| if not _childs_for_node(tmp, e): |
| tmp2[e] = tmp[e] |
| |
| # Group nodes too small in an "other" section |
| filtered_data_nodes = {} |
| for e in tmp2: |
| if tmp[e] < min_sibling_size: |
| k = _parent_for_node(e) + "/(other)" |
| if k in filtered_data_nodes: |
| filtered_data_nodes[k] += tmp[e] |
| else: |
| filtered_data_nodes[k] = tmp[e] |
| else: |
| filtered_data_nodes[e] = tmp[e] |
| |
| def _parent_level_3_at_most(node): |
| e = _parent_for_node(node) |
| while e.count('/') > 2: |
| e = _parent_for_node(e) |
| return e |
| |
| return ram_nodes, data_nodes |
| |
| |
| def print_tree(data, total, depth): |
| base = os.environ['ZEPHYR_BASE'] |
| totp = 0 |
| |
| bcolors_ansi = { |
| "HEADER" : '\033[95m', |
| "OKBLUE" : '\033[94m', |
| "OKGREEN" : '\033[92m', |
| "WARNING" : '\033[93m', |
| "FAIL" : '\033[91m', |
| "ENDC" : '\033[0m', |
| "BOLD" : '\033[1m', |
| "UNDERLINE" : '\033[4m' |
| } |
| if platform.system() == "Windows": |
| # Set all color codes to empty string on Windows |
| # |
| # TODO: Use an approach like the pip package 'colorama' to |
| # support colors on Windows |
| bcolors = dict.fromkeys(bcolors_ansi, '') |
| else: |
| bcolors = bcolors_ansi |
| |
| print('{:92s} {:10s} {:8s}'.format( |
| bcolors["FAIL"] + "Path", "Size", "%" + bcolors["ENDC"])) |
| print('=' * 110) |
| for i in sorted(data): |
| p = i.split(os.path.sep) |
| if depth and len(p) > depth: |
| continue |
| |
| percent = 100 * float(data[i]) / float(total) |
| percent_c = percent |
| if len(p) < 2: |
| totp += percent |
| |
| if len(p) > 1: |
| if not os.path.exists(os.path.join(base, i)): |
| s = bcolors["WARNING"] + p[-1] + bcolors["ENDC"] |
| else: |
| s = bcolors["OKBLUE"] + p[-1] + bcolors["ENDC"] |
| print('{:80s} {:20d} {:8.2f}%'.format( |
| " " * (len(p) - 1) + s, data[i], percent_c)) |
| else: |
| print('{:80s} {:20d} {:8.2f}%'.format( |
| bcolors["OKBLUE"] + i + bcolors["ENDC"], data[i], percent_c)) |
| |
| print('=' * 110) |
| print('{:92d}'.format(total)) |
| return totp |
| |
| |
| def main(): |
| |
| parser = argparse.ArgumentParser( |
| description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter) |
| |
| parser.add_argument("-d", "--depth", dest="depth", type=int, |
| help="How deep should we go into the tree", metavar="DEPTH") |
| parser.add_argument("-o", "--outdir", dest="outdir", required=True, |
| help="read files from directory OUT", metavar="OUT") |
| parser.add_argument("-k", "--kernel-name", dest="binary", default="zephyr", |
| help="kernel binary name") |
| parser.add_argument("-r", "--ram", |
| action="store_true", dest="ram", default=False, |
| help="print RAM statistics") |
| parser.add_argument("-F", "--rom", |
| action="store_true", dest="rom", default=False, |
| help="print ROM statistics") |
| parser.add_argument("-s", "--objdump", dest="bin_objdump", required=True, |
| help="Path to the GNU binary utility objdump") |
| parser.add_argument("-c", "--objcopy", dest="bin_objcopy", |
| help="Path to the GNU binary utility objcopy") |
| parser.add_argument("-n", "--nm", dest="bin_nm", required=True, |
| help="Path to the GNU binary utility nm") |
| |
| args = parser.parse_args() |
| |
| bin_file = os.path.join(args.outdir, args.binary + ".bin") |
| stat_file = os.path.join(args.outdir, args.binary + ".stat") |
| elf_file = os.path.join(args.outdir, args.binary + ".elf") |
| |
| if not os.path.exists(elf_file): |
| print("%s does not exist." % (elf_file)) |
| return |
| |
| if not os.path.exists(bin_file): |
| FNULL = open(os.devnull, 'w') |
| subprocess.call([args.bin_objcopy,"-S", "-Obinary", "-R", ".comment", "-R", |
| "COMMON", "-R", ".eh_frame", elf_file, bin_file], |
| stdout=FNULL, stderr=subprocess.STDOUT) |
| |
| fp = get_footprint_from_bin_and_statfile(bin_file, stat_file, 0, 0) |
| base = os.environ['ZEPHYR_BASE'] |
| ram, data = generate_target_memory_section( |
| args.bin_objdump, args.bin_nm, args.outdir, args.binary, |
| base + '/', None) |
| if args.rom: |
| print_tree(data, fp['total_flash'], args.depth) |
| if args.ram: |
| print_tree(ram, fp['total_ram'], args.depth) |
| |
| |
| if __name__ == "__main__": |
| main() |