blob: e6bbb0e13dc10ab91ad16b5958e04bccb980a000 [file] [log] [blame]
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Creates an html report that allows you to view binary size by component."""
import codecs
import collections
import json
import logging
import os
import archive
import diff
import models
_SYMBOL_TYPE_VTABLE = 'v'
_SYMBOL_TYPE_GENERATED = '*'
_SYMBOL_TYPE_DEX_METHOD = 'm'
_SYMBOL_TYPE_OTHER = 'o'
_COMPACT_FILE_PATH_KEY = 'p'
_COMPACT_FILE_COMPONENT_INDEX_KEY = 'c'
_COMPACT_FILE_SYMBOLS_KEY = 's'
_COMPACT_SYMBOL_NAME_KEY = 'n'
_COMPACT_SYMBOL_BYTE_SIZE_KEY = 'b'
_COMPACT_SYMBOL_TYPE_KEY = 't'
_COMPACT_SYMBOL_COUNT_KEY = 'u'
_SMALL_SYMBOL_DESCRIPTIONS = {
'b': 'Other small uninitialized data',
'd': 'Other small initialized data',
'r': 'Other small readonly data',
't': 'Other small code',
'v': 'Other small vtable entries',
'*': 'Other small generated symbols',
'x': 'Other small dex non-method entries',
'm': 'Other small dex methods',
'p': 'Other small locale pak entries',
'P': 'Other small non-locale pak entries',
'o': 'Other small entries',
}
_DEFAULT_SYMBOL_COUNT = 250000
def _GetSymbolType(symbol):
symbol_type = symbol.section
if symbol.name.endswith('[vtable]'):
symbol_type = _SYMBOL_TYPE_VTABLE
elif symbol.name.endswith(']'):
symbol_type = _SYMBOL_TYPE_GENERATED
if symbol_type not in _SMALL_SYMBOL_DESCRIPTIONS:
symbol_type = _SYMBOL_TYPE_OTHER
return symbol_type
def _GetOrAddFileNode(symbol, file_nodes, components):
path = symbol.source_path or symbol.object_path
file_node = file_nodes.get(path)
if file_node is None:
component_index = components.GetOrAdd(symbol.component)
file_node = {
_COMPACT_FILE_PATH_KEY: path,
_COMPACT_FILE_COMPONENT_INDEX_KEY: component_index,
_COMPACT_FILE_SYMBOLS_KEY: [],
}
file_nodes[path] = file_node
return file_node
class IndexedSet(object):
"""Set-like object where values are unique and indexed.
Values must be immutable.
"""
def __init__(self):
self._index_dict = {} # Value -> Index dict
self.value_list = [] # List containing all the set items
def GetOrAdd(self, value):
"""Get the index of the value in the list. Append it if not yet present."""
index = self._index_dict.get(value)
if index is None:
self.value_list.append(value)
index = len(self.value_list) - 1
self._index_dict[value] = index
return index
def _MakeTreeViewList(symbols, include_all_symbols):
"""Builds JSON data of the symbols for the tree view HTML report.
As the tree is built on the client-side, this function creates a flat list
of files, where each file object contains symbols that have the same path.
Args:
symbols: A SymbolGroup containing all symbols.
include_all_symbols: If true, include all symbols in the data file.
"""
file_nodes = {}
components = IndexedSet()
# Build a container for symbols smaller than min_symbol_size
small_symbols = collections.defaultdict(dict)
# Dex methods (type "m") are whitelisted for the method_count mode on the
# UI. It's important to see details on all the methods.
dex_symbols = symbols.WhereIsDex()
ordered_symbols = dex_symbols.Inverted().Sorted()
if include_all_symbols:
symbol_count = len(ordered_symbols)
else:
symbol_count = max(_DEFAULT_SYMBOL_COUNT - len(dex_symbols), 0)
main_symbols = dex_symbols + ordered_symbols[:symbol_count]
extra_symbols = ordered_symbols[symbol_count:]
logging.info('Found %d large symbols, %s small symbols',
len(main_symbols), len(extra_symbols))
# Bundle symbols by the file they belong to,
# and add all the file buckets into file_nodes
for symbol in main_symbols:
symbol_type = _GetSymbolType(symbol)
symbol_size = round(symbol.pss, 2)
if symbol_size.is_integer():
symbol_size = int(symbol_size)
symbol_count = 1
if symbol.IsDelta() and symbol.diff_status == models.DIFF_STATUS_REMOVED:
symbol_count = -1
file_node = _GetOrAddFileNode(symbol, file_nodes, components)
is_dex_method = symbol_type == _SYMBOL_TYPE_DEX_METHOD
symbol_entry = {
_COMPACT_SYMBOL_NAME_KEY: symbol.template_name,
_COMPACT_SYMBOL_TYPE_KEY: symbol_type,
_COMPACT_SYMBOL_BYTE_SIZE_KEY: symbol_size,
}
# We use symbol count for the method count mode in the diff mode report.
# Negative values are used to indicate a symbol was removed, so it should
# count as -1 rather than the default, 1.
# We don't care about accurate counts for other symbol types currently,
# so this data is only included for methods.
if is_dex_method and symbol_count != 1:
symbol_entry[_COMPACT_SYMBOL_COUNT_KEY] = symbol_count
file_node[_COMPACT_FILE_SYMBOLS_KEY].append(symbol_entry)
for symbol in extra_symbols:
symbol_type = _GetSymbolType(symbol)
file_node = _GetOrAddFileNode(symbol, file_nodes, components)
path = file_node[_COMPACT_FILE_PATH_KEY]
small_type_symbol = small_symbols[path].get(symbol_type)
if small_type_symbol is None:
small_type_symbol = {
_COMPACT_SYMBOL_NAME_KEY: _SMALL_SYMBOL_DESCRIPTIONS[symbol_type],
_COMPACT_SYMBOL_TYPE_KEY: symbol_type,
_COMPACT_SYMBOL_BYTE_SIZE_KEY: 0,
}
small_symbols[path][symbol_type] = small_type_symbol
file_node[_COMPACT_FILE_SYMBOLS_KEY].append(small_type_symbol)
small_type_symbol[_COMPACT_SYMBOL_BYTE_SIZE_KEY] += symbol.pss
meta = {
'components': components.value_list,
'total': symbols.pss,
}
return meta, file_nodes.values()
def _MakeDirIfDoesNotExist(rel_path):
"""Ensures a directory exists."""
abs_path = os.path.abspath(rel_path)
try:
os.makedirs(abs_path)
except OSError:
if not os.path.isdir(abs_path):
raise
def AddArguments(parser):
parser.add_argument('input_file',
help='Path to input .size file.')
parser.add_argument('--report-file', metavar='PATH', required=True,
help='Write generated data to the specified '
'.ndjson file.')
parser.add_argument('--all-symbols', action='store_true',
help='Include all symbols. Will cause the data file to '
'take longer to load.')
parser.add_argument('--diff-with',
help='Diffs the input_file against an older .size file')
def Run(args, parser):
if not args.input_file.endswith('.size'):
parser.error('Input must end with ".size"')
if args.diff_with and not args.diff_with.endswith('.size'):
parser.error('Diff input must end with ".size"')
if not args.report_file.endswith('.ndjson'):
parser.error('Output must end with ".ndjson"')
logging.info('Reading .size file')
size_info = archive.LoadAndPostProcessSizeInfo(args.input_file)
if args.diff_with:
before_size_info = archive.LoadAndPostProcessSizeInfo(args.diff_with)
after_size_info = size_info
size_info = diff.Diff(before_size_info, after_size_info)
symbols = size_info.raw_symbols
symbols = symbols.WhereDiffStatusIs(models.DIFF_STATUS_UNCHANGED).Inverted()
else:
symbols = size_info.raw_symbols
logging.info('Creating JSON objects')
meta, tree_nodes = _MakeTreeViewList(symbols, args.all_symbols)
meta.update({
'diff_mode': bool(args.diff_with),
'section_sizes': size_info.section_sizes,
})
if args.diff_with:
meta.update({
'before_metadata': size_info.before.metadata,
'after_metadata': size_info.after.metadata,
})
else:
meta['metadata'] = size_info.metadata
logging.info('Serializing JSON')
# Write newline-delimited JSON file
with codecs.open(args.report_file, 'w', encoding='ascii') as out_file:
# Use separators without whitespace to get a smaller file.
json_dump_args = {
'separators': (',', ':'),
'ensure_ascii': True,
'check_circular': False,
}
json.dump(meta, out_file, **json_dump_args)
out_file.write('\n')
for tree_node in tree_nodes:
json.dump(tree_node, out_file, **json_dump_args)
out_file.write('\n')
logging.warning('Report saved to %s', args.report_file)
logging.warning('Open server by running: \n'
'tools/binary_size/supersize start_server %s',
args.report_file)