|  | #!/usr/bin/python | 
|  | # | 
|  | # Copyright (c) 2016, Intel Corporation | 
|  | # | 
|  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | # you may not use this file except in compliance with the License. | 
|  | # You may obtain a copy of the License at | 
|  | # | 
|  | #     http://www.apache.org/licenses/LICENSE-2.0 | 
|  | # | 
|  | # Unless required by applicable law or agreed to in writing, software | 
|  | # distributed under the License is distributed on an "AS IS" BASIS, | 
|  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | # See the License for the specific language governing permissions and | 
|  | # limitations under the License. | 
|  |  | 
|  | # Based on a script by: | 
|  | #       Chereau, Fabien <fabien.chereau@intel.com> | 
|  |  | 
|  | import os | 
|  | import re | 
|  | from optparse import OptionParser | 
|  | import sys | 
|  | import argparse | 
|  | import subprocess | 
|  | import json | 
|  | import operator | 
|  |  | 
|  | class bcolors: | 
|  | HEADER = '\033[95m' | 
|  | OKBLUE = '\033[94m' | 
|  | OKGREEN = '\033[92m' | 
|  | WARNING = '\033[93m' | 
|  | FAIL = '\033[91m' | 
|  | ENDC = '\033[0m' | 
|  | BOLD = '\033[1m' | 
|  | UNDERLINE = '\033[4m' | 
|  |  | 
|  |  | 
|  | parser = OptionParser() | 
|  | parser.add_option("-d", "--depth", dest="depth", type="int", | 
|  | help="How deep should we go into the tree", metavar="DEPTH") | 
|  | parser.add_option("-o", "--outdir", dest="outdir", | 
|  | help="read files from directory OUT", metavar="OUT") | 
|  | parser.add_option("-k", "--kernel-name", dest="binary", default="zephyr", | 
|  | help="kernel binary name") | 
|  | parser.add_option("-r", "--ram", | 
|  | action="store_true", dest="ram", default=False, | 
|  | help="print RAM statistics") | 
|  | parser.add_option("-F", "--rom", | 
|  | action="store_true", dest="rom", default=False, | 
|  | help="print ROM statistics") | 
|  |  | 
|  | (options, args) = parser.parse_args() | 
|  |  | 
|  | # Return a dict containing symbol_name: path/to/file/where/it/originates | 
|  | # for all symbols from the .elf file. Optionnaly strips the path according | 
|  | # to the passed sub-path | 
|  | def load_symbols_and_paths(elf_file, path_to_strip = None): | 
|  | symbols_paths = {} | 
|  | nm_out = subprocess.check_output(["nm", elf_file, "-S", "-l", "--size-sort", "--radix=d"]) | 
|  | for line in nm_out.split('\n'): | 
|  | fields = line.replace('\t', ' ').split(' ') | 
|  | # Get rid of trailing empty field | 
|  | if len(fields) == 1 and fields[0] == '': | 
|  | continue | 
|  | assert len(fields)>=4 | 
|  | if len(fields)<5: | 
|  | path = ":/" + fields[3] | 
|  | else: | 
|  | path = fields[4].split(':')[0] | 
|  | if path_to_strip != None: | 
|  | if path_to_strip in path: | 
|  | path = path.replace(path_to_strip, "") + '/' + fields[3] | 
|  | else: | 
|  | path = ":/" + fields[3] | 
|  | symbols_paths[fields[3]] = path | 
|  | return symbols_paths | 
|  |  | 
|  | def get_section_size(f, section_name): | 
|  | decimal_size = 0 | 
|  | re_res = re.search(r"(.*] "+section_name+".*)", f, re.MULTILINE) | 
|  | if re_res != 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(out, kernel_name, source_dir, features_json): | 
|  | features_path_data = None | 
|  | try: | 
|  | features_path_data = json.loads(open(features_json, 'r').read()) | 
|  | except: | 
|  | 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(["objdump", "-hw", elf_file_abs]) | 
|  | 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.split('\n'): | 
|  | 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 = load_symbols_and_paths(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): | 
|  | splitted_path = path.split('/') | 
|  | cur = None | 
|  | for p in splitted_path: | 
|  | if cur == None: | 
|  | cur = p | 
|  | else: | 
|  | cur = cur + '/' + p | 
|  | if cur in tree: | 
|  | tree[cur] += size | 
|  | else: | 
|  | tree[cur] = size | 
|  |  | 
|  | def _parent_for_node(e): | 
|  | parent = "root" if len(e.split('/')) == 1 else e.rsplit('/', 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(["objdump", "-tw", elf_file_abs]) | 
|  | 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.split('\n'): | 
|  | 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('.*\.[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 == None and is_generated == 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 is 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) == 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 len(_childs_for_node(tmp, e)) == 0: | 
|  | 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 | 
|  | print '{:92s} {:10s} {:8s}'.format(bcolors.FAIL + "Path", "Size", "%" + bcolors.ENDC) | 
|  | print '='*110 | 
|  | for i in sorted(data): | 
|  | p = i.split("/") | 
|  | 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 | 
|  |  | 
|  |  | 
|  | binary = os.path.join(options.outdir, options.binary + ".elf") | 
|  |  | 
|  | if options.outdir and os.path.exists(binary): | 
|  | fp =  get_footprint_from_bin_and_statfile("%s/%s.bin" %(options.outdir, options.binary), | 
|  | "%s/%s.stat" %(options.outdir,options.binary), 0, 0 ) | 
|  | base = os.environ['ZEPHYR_BASE'] | 
|  | ram, data = generate_target_memory_section(options.outdir, options.binary, base + '/',  None) | 
|  | if options.rom: | 
|  | print_tree(data, fp['total_flash'], options.depth) | 
|  | if options.ram: | 
|  | print_tree(ram, fp['total_ram'], options.depth) | 
|  |  | 
|  | else: | 
|  | print "%s does not exist." %(binary) |