zephyr/scripts/footprint/size_report

451 lines
15 KiB
Python
Executable File

#!/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 as e:
# 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, "")
#
# depending on if the file is 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 symbol[\t<absolute_path>:line]
#
# The file is optional, nm might not find out where a symbol 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) >= 4
symbol = fields[3]
file_is_missing = len(fields) == 4
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):
features_path_data = None
try:
features_path_data = 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('.*\.[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 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) 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 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
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()