You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
351 lines
13 KiB
351 lines
13 KiB
#!/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)
|
|
|