blob: 5efe68f67861b7dac396d219d888bdf977c41f9d [file]
# Copyright 2017 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.
"""Deals with loading & saving .size and .sizediff files.
The .size file is written in the following format. There are no section
delimeters, instead the end of a section is usually determined by a row count
on the first line of a section, followed by that amount of rows. In other
cases, the sections have a known size.
Header
------
4 lines long.
Line 0 of the file is a header comment.
Line 1 is the serialization version of the file.
Line 2 is the number of characters in the metadata string.
Line 3 is the metadata string, a stringified JSON object.
Path list
---------
A list of paths. The first line is the size of the list,
and the next N lines that follow are items in the list. Each item is a tuple
of (object_path, source_path) where the two parts are tab separated.
Component list
--------------
A list of components. The first line is the size of the list,
and the next N lines that follow are items in the list. Each item is a unique
COMPONENT which is referenced later.
This section is only present if 'has_components' is True in the metadata.
Symbol counts
-------------
2 lines long.
The first line is a tab separated list of section names.
The second line is a tab separated list of symbol group lengths, in the same
order as the previous line.
Numeric values
--------------
In each section, the number of rows is the same as the number of section names
in Symbol counts. The values on a row are space separated, in the order of the
symbols in each group.
Addresses
~~~~~~~~~~
Symbol start addresses which are delta-encoded.
Sizes
~~~~~
The number of bytes this symbol takes up.
Padding
~~~~~~~
The number of padding bytes this symbol has.
This section is only present if 'has_padding' is True in the metadata.
Path indices
~~~~~~~~~~~~~
Indices that reference paths in the prior Path list section. Delta-encoded.
Component indices
~~~~~~~~~~~~~~~~~~
Indices that reference components in the prior Component list section.
Delta-encoded.
This section is only present if 'has_components' is True in the metadata.
Symbols
-------
The final section contains details info on each symbol. Each line represents
a single symbol. Values are tab separated and follow this format:
symbol.full_name, symbol.num_aliases, symbol.flags
|num_aliases| will be omitted if the aliases of the symbol are the same as the
previous line. |flags| will be omitted if there are no flags.
The .sizediff file stores a sparse representation of a difference between .size
files. Each .sizediff file stores two sparse .size files, before and after,
containing only symbols that differed between "before" and "after". They can
be rendered via the Tiger viewer. .sizediff files use the following format:
Header
------
3 lines long.
Line 0 of the file is a header comment.
Line 1 is the number of characters in the metadata string.
Line 2 is the metadata string, a stringified JSON object. This currently
contains two fields, 'before_length' (the length in bytes of the 'before'
section) and 'version', which is always 1.
Before
------
The next |metadata.before_length| bytes are a valid gzipped sparse .size file
containing the "before" snapshot.
After
-----
All remaining bytes are a valid gzipped sparse .size file containing the
"after" snapshot.
"""
from __future__ import division
import cStringIO
import contextlib
import gzip
import itertools
import json
import logging
import os
import shutil
import models
import parallel
# File format version for .size files.
_SERIALIZATION_VERSION = 'Size File Format v1'
# Header for .sizediff files
_SIZEDIFF_HEADER = '# Created by //tools/binary_size\nDIFF\n'
class _Writer:
"""Helper to format and write data to a file object."""
def __init__(self, file_obj):
self.file_obj_ = file_obj
def WriteBytes(self, b):
# Direct write of raw bytes.
self.file_obj_.write(b)
def WriteString(self, s):
# TODO(huangs): Python 3 will require |s.encode('ascii')|.
self.file_obj_.write(s)
def WriteLine(self, s):
# TODO(huangs): Python 3 will require |s.encode('ascii')|.
self.file_obj_.write(s)
self.file_obj_.write(b'\n')
def WriteNumberList(self, gen):
"""Writes numbers from |gen| separated by space, in one line."""
sep = b''
for num in gen:
self.WriteBytes(sep)
self.WriteString(str(num))
sep = b' '
self.WriteBytes(b'\n')
def LogSize(self, desc):
self.file_obj_.flush()
size = self.file_obj_.tell()
logging.debug('File size with %s: %d' % (desc, size))
def SortSymbols(raw_symbols):
logging.debug('Sorting %d symbols', len(raw_symbols))
# TODO(agrieve): Either change this sort so that it's only sorting by section
# (and not using .sort()), or have it specify a total ordering (which must
# also include putting padding-only symbols before others of the same
# address). Note: The sort as-is takes ~1.5 seconds.
raw_symbols.sort(
key=lambda s: (s.IsPak(), s.IsBss(), s.section_name, s.address))
logging.info('Processed %d symbols', len(raw_symbols))
def CalculatePadding(raw_symbols):
"""Populates the |padding| field based on symbol addresses. """
logging.info('Calculating padding')
# Padding not really required, but it is useful to check for large padding and
# log a warning.
seen_sections = set()
for i, symbol in enumerate(raw_symbols[1:]):
prev_symbol = raw_symbols[i]
if symbol.IsOverhead():
# Overhead symbols are not actionable so should be padding-only.
symbol.padding = symbol.size
if prev_symbol.section_name != symbol.section_name:
assert symbol.section_name not in seen_sections, (
'Input symbols must be sorted by section, then address.')
seen_sections.add(symbol.section_name)
continue
if (symbol.address <= 0 or prev_symbol.address <= 0
or not symbol.IsNative() or not prev_symbol.IsNative()):
continue
if symbol.address == prev_symbol.address:
if symbol.aliases and symbol.aliases is prev_symbol.aliases:
symbol.padding = prev_symbol.padding
symbol.size = prev_symbol.size
continue
# Padding-only symbols happen for ** symbol gaps.
assert prev_symbol.size_without_padding == 0, (
'Found duplicate symbols:\n%r\n%r' % (prev_symbol, symbol))
padding = symbol.address - prev_symbol.end_address
symbol.padding = padding
symbol.size += padding
assert symbol.size >= 0, (
'Symbol has negative size (likely not sorted propertly): '
'%r\nprev symbol: %r' % (symbol, prev_symbol))
def _ExpandSparseSymbols(sparse_symbols):
"""Expands a symbol list with all aliases of all symbols in the list.
Args:
sparse_symbols: A list or SymbolGroup to expand.
"""
representative_symbols = set()
raw_symbols = set()
logging.debug('Expanding sparse_symbols with aliases of included symbols')
for sym in sparse_symbols:
if sym.aliases:
representative_symbols.add(sym.aliases[0])
else:
raw_symbols.add(sym)
for sym in representative_symbols:
raw_symbols.update(set(sym.aliases))
raw_symbols = list(raw_symbols)
SortSymbols(raw_symbols)
logging.debug('Done expanding sparse_symbols')
return models.SymbolGroup(raw_symbols)
def _SaveSizeInfoToFile(size_info,
file_obj,
include_padding=False,
sparse_symbols=None):
"""Saves size info to a .size file.
Args:
size_info: Data to write to the file
file_obj: File opened for writing
sparse_symbols: If present, only save these symbols to the file
"""
if sparse_symbols is not None:
# Any aliases of sparse symbols must also be included, or else file parsing
# will attribute symbols that happen to follow an incomplete alias group to
# that alias group.
raw_symbols = _ExpandSparseSymbols(sparse_symbols)
else:
raw_symbols = size_info.raw_symbols
w = _Writer(file_obj)
# Created by supersize header
w.WriteLine('# Created by //tools/binary_size')
w.WriteLine(_SERIALIZATION_VERSION)
# JSON metadata
headers = {
'metadata': size_info.metadata,
'section_sizes': size_info.section_sizes,
'has_components': True,
'has_padding': include_padding,
}
metadata_str = json.dumps(headers, file_obj, indent=2, sort_keys=True)
# TODO(huangs): Remove .replace() after transitioning to Python 3.
# Strip space at end of each line, injected by Python 2 json.dumps().
metadata_str = metadata_str.replace(' \n', '\n')
w.WriteLine(str(len(metadata_str)))
w.WriteLine(metadata_str)
w.LogSize('header') # For libchrome: 570 bytes.
# Store a single copy of all paths and have them referenced by index.
unique_path_tuples = sorted(
set((s.object_path, s.source_path) for s in raw_symbols))
path_tuples = {tup: i for i, tup in enumerate(unique_path_tuples)}
w.WriteLine(str(len(unique_path_tuples)))
for pair in unique_path_tuples:
w.WriteLine('%s\t%s' % pair)
w.LogSize('paths') # For libchrome, adds 200kb.
# Store a single copy of all components and have them referenced by index.
unique_components = sorted(set(s.component for s in raw_symbols))
components = {comp: i for i, comp in enumerate(unique_components)}
w.WriteLine(str(len(unique_components)))
for comp in unique_components:
w.WriteLine(comp)
w.LogSize('components')
# Symbol counts by section.
symbol_group_by_section = raw_symbols.GroupedBySectionName()
w.WriteLine('\t'.join(g.name for g in symbol_group_by_section))
w.WriteLine('\t'.join(str(len(g)) for g in symbol_group_by_section))
def gen_delta(gen, prev_value=0):
"""Adapts a generator of numbers to deltas."""
for value in gen:
yield value - prev_value
prev_value = value
def write_groups(func, delta=False):
"""Write func(symbol) for each symbol in each symbol group.
Each line written represents one symbol group in |symbol_group_by_section|.
The values in each line are space separated and are the result of calling
|func| with the Nth symbol in the group.
If |delta| is True, the differences in values are written instead."""
for group in symbol_group_by_section:
gen = itertools.imap(func, group)
w.WriteNumberList(gen_delta(gen) if delta else gen)
write_groups(lambda s: s.address, delta=True)
w.LogSize('addresses') # For libchrome, adds 300kb.
write_groups(lambda s: s.size if s.IsOverhead() else s.size_without_padding)
w.LogSize('sizes') # For libchrome, adds 300kb
# Padding for non-padding-only symbols is recalculated from addresses on
# load, so we only need to write it if we're writing a subset of symbols.
if include_padding:
write_groups(lambda s: s.padding)
w.LogSize('paddings') # For libchrome, adds 300kb
write_groups(
lambda s: path_tuples[(s.object_path, s.source_path)], delta=True)
w.LogSize('path indices') # For libchrome: adds 125kb.
write_groups(lambda s: components[s.component], delta=True)
w.LogSize('component indices')
prev_aliases = None
for group in symbol_group_by_section:
for symbol in group:
w.WriteString(symbol.full_name)
if symbol.aliases and symbol.aliases is not prev_aliases:
w.WriteString('\t0%x' % symbol.num_aliases)
prev_aliases = symbol.aliases
if symbol.flags:
w.WriteString('\t%x' % symbol.flags)
w.WriteBytes(b'\n')
w.LogSize('names (final)') # For libchrome: adds 3.5mb.
def _ReadLine(file_iter):
"""Read a line from a file object iterator and remove the newline character.
Args:
file_iter: File object iterator
Returns:
String
"""
# str[:-1] removes the last character from a string, specifically the newline
return next(file_iter)[:-1]
def _ReadValuesFromLine(file_iter, split):
"""Read a list of values from a line in a file object iterator.
Args:
file_iter: File object iterator
split: Splits the line with the given string
Returns:
List of string values
"""
return _ReadLine(file_iter).split(split)
def _LoadSizeInfoFromFile(file_obj, size_path):
"""Loads a size_info from the given file.
See _SaveSizeInfoToFile for details on the .size file format.
Args:
file_obj: File to read, should be a GzipFile
"""
lines = iter(file_obj)
_ReadLine(lines) # Line 0: Created by supersize header
actual_version = _ReadLine(lines)
assert actual_version == _SERIALIZATION_VERSION, (
'Version mismatch. Need to write some upgrade code.')
# JSON metadata
json_len = int(_ReadLine(lines))
json_str = file_obj.read(json_len)
headers = json.loads(json_str)
section_sizes = headers['section_sizes']
metadata = headers.get('metadata')
has_components = headers.get('has_components', False)
has_padding = headers.get('has_padding', False)
lines = iter(file_obj)
_ReadLine(lines)
# Path list
num_path_tuples = int(_ReadLine(lines)) # Line 4 - number of paths in list
# Read the path list values and store for later
path_tuples = [_ReadValuesFromLine(lines, split='\t')
for _ in xrange(num_path_tuples)]
# Component list
if has_components:
num_components = int(_ReadLine(lines)) # number of components in list
components = [_ReadLine(lines) for _ in xrange(num_components)]
# Symbol counts by section.
section_names = _ReadValuesFromLine(lines, split='\t')
section_counts = [int(c) for c in _ReadValuesFromLine(lines, split='\t')]
# Addresses, sizes, paddings, path indices, component indices
def read_numeric(delta=False):
"""Read numeric values, where each line corresponds to a symbol group.
The values in each line are space separated.
If |delta| is True, the numbers are read as a value to add to the sum of the
prior values in the line, or as the amount to change by.
"""
ret = []
delta_multiplier = int(delta)
for _ in section_counts:
value = 0
fields = []
for f in _ReadValuesFromLine(lines, split=' '):
value = value * delta_multiplier + int(f)
fields.append(value)
ret.append(fields)
return ret
addresses = read_numeric(delta=True)
sizes = read_numeric(delta=False)
if has_padding:
paddings = read_numeric(delta=False)
else:
paddings = [None] * len(section_names)
path_indices = read_numeric(delta=True)
if has_components:
component_indices = read_numeric(delta=True)
else:
component_indices = [None] * len(section_names)
raw_symbols = [None] * sum(section_counts)
symbol_idx = 0
for (cur_section_name, cur_section_count, cur_addresses, cur_sizes,
cur_paddings, cur_path_indices, cur_component_indices) in itertools.izip(
section_names, section_counts, addresses, sizes, paddings,
path_indices, component_indices):
alias_counter = 0
for i in xrange(cur_section_count):
parts = _ReadValuesFromLine(lines, split='\t')
full_name = parts[0]
flags_part = None
aliases_part = None
# aliases_part or flags_part may have been omitted.
if len(parts) == 3:
# full_name aliases_part flags_part
aliases_part = parts[1]
flags_part = parts[2]
elif len(parts) == 2:
if parts[1][0] == '0':
# full_name aliases_part
aliases_part = parts[1]
else:
# full_name flags_part
flags_part = parts[1]
# Use a bit less RAM by using the same instance for this common string.
if full_name == models.STRING_LITERAL_NAME:
full_name = models.STRING_LITERAL_NAME
flags = int(flags_part, 16) if flags_part else 0
num_aliases = int(aliases_part, 16) if aliases_part else 0
# Skip the constructor to avoid default value checks
new_sym = models.Symbol.__new__(models.Symbol)
new_sym.section_name = cur_section_name
new_sym.full_name = full_name
new_sym.address = cur_addresses[i]
new_sym.size = cur_sizes[i]
paths = path_tuples[cur_path_indices[i]]
new_sym.object_path, new_sym.source_path = paths
component = components[cur_component_indices[i]] if has_components else ''
new_sym.component = component
new_sym.flags = flags
# Derived
if cur_paddings:
new_sym.padding = cur_paddings[i]
new_sym.size += new_sym.padding
else:
# This will be computed during CreateSizeInfo()
new_sym.padding = 0
new_sym.template_name = ''
new_sym.name = ''
if num_aliases:
assert alias_counter == 0
new_sym.aliases = [new_sym]
alias_counter = num_aliases - 1
elif alias_counter > 0:
new_sym.aliases = raw_symbols[symbol_idx - 1].aliases
new_sym.aliases.append(new_sym)
alias_counter -= 1
else:
new_sym.aliases = None
raw_symbols[symbol_idx] = new_sym
symbol_idx += 1
if not has_padding:
CalculatePadding(raw_symbols)
return models.SizeInfo(section_sizes, raw_symbols, metadata=metadata,
size_path=size_path)
@contextlib.contextmanager
def _OpenGzipForWrite(path, file_obj=None):
# Open in a way that doesn't set any gzip header fields.
if file_obj:
with gzip.GzipFile(filename='', mode='wb', fileobj=file_obj, mtime=0) as fz:
yield fz
else:
with open(path, 'wb') as f:
with gzip.GzipFile(filename='', mode='wb', fileobj=f, mtime=0) as fz:
yield fz
def SaveSizeInfo(size_info,
path,
file_obj=None,
include_padding=False,
sparse_symbols=None):
"""Saves |size_info| to |path|."""
if os.environ.get('SUPERSIZE_MEASURE_GZIP') == '1':
with _OpenGzipForWrite(path, file_obj=file_obj) as f:
_SaveSizeInfoToFile(
size_info,
f,
include_padding=include_padding,
sparse_symbols=sparse_symbols)
else:
# It is seconds faster to do gzip in a separate step. 6s -> 3.5s.
# TODO(huangs): Use io.BytesIO for Python 3.
stringio = cStringIO.StringIO()
_SaveSizeInfoToFile(
size_info,
stringio,
include_padding=include_padding,
sparse_symbols=sparse_symbols)
logging.debug('Serialization complete. Gzipping...')
stringio.seek(0)
with _OpenGzipForWrite(path, file_obj=file_obj) as f:
shutil.copyfileobj(stringio, f)
def LoadSizeInfo(filename, file_obj=None):
"""Returns a SizeInfo loaded from |filename|."""
with gzip.GzipFile(filename=filename, fileobj=file_obj) as f:
return _LoadSizeInfoFromFile(f, filename)
def SaveDeltaSizeInfo(delta_size_info, path, file_obj=None):
"""Saves |delta_size_info| to |path|."""
changed_symbols = delta_size_info.raw_symbols \
.WhereDiffStatusIs(models.DIFF_STATUS_UNCHANGED).Inverted()
before_symbols = models.SymbolGroup(
[sym.before_symbol for sym in changed_symbols if sym.before_symbol])
after_symbols = models.SymbolGroup(
[sym.after_symbol for sym in changed_symbols if sym.after_symbol])
# TODO(huangs): Use io.BytesIO for Python 3.
before_size_file = cStringIO.StringIO()
after_size_file = cStringIO.StringIO()
after_promise = parallel.CallOnThread(
SaveSizeInfo,
delta_size_info.after,
'',
file_obj=after_size_file,
include_padding=True,
sparse_symbols=after_symbols)
SaveSizeInfo(
delta_size_info.before,
'',
file_obj=before_size_file,
include_padding=True,
sparse_symbols=before_symbols)
with file_obj or open(path, 'wb') as output_file:
w = _Writer(output_file)
# |_SIZEDIFF_HEADER| is multi-line with new line at end, so use
# WriteString() instead of WriteLine().
w.WriteString(_SIZEDIFF_HEADER)
# JSON metadata
headers = {
'version': 1,
'before_length': before_size_file.tell(),
}
metadata_str = json.dumps(headers, output_file, indent=2, sort_keys=True)
# TODO(huangs): Remove .replace() after transitioning to Python 3.
# Strip space at end of each line, injected by Python 2 json.dumps().
metadata_str = metadata_str.replace(' \n', '\n')
w.WriteLine(str(len(metadata_str)))
w.WriteLine(metadata_str)
before_size_file.seek(0)
shutil.copyfileobj(before_size_file, output_file)
after_promise.get()
after_size_file.seek(0)
shutil.copyfileobj(after_size_file, output_file)