399 lines
14 KiB
Python
Executable File
399 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright 2020 The Chromium Authors
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
"""Reports binary size metrics for LaCrOS build artifacts.
|
|
|
|
More information at //docs/speed/binary_size/metrics.md.
|
|
"""
|
|
|
|
import argparse
|
|
import collections
|
|
import contextlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
SRC_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
sys.path.insert(0, os.path.join(SRC_DIR, 'build', 'util'))
|
|
from lib.results import result_sink
|
|
from lib.results import result_types
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _SysPath(path):
|
|
"""Library import context that temporarily appends |path| to |sys.path|."""
|
|
if path and path not in sys.path:
|
|
sys.path.insert(0, path)
|
|
else:
|
|
path = None # Indicates that |sys.path| is not modified.
|
|
try:
|
|
yield
|
|
finally:
|
|
if path:
|
|
sys.path.pop(0)
|
|
|
|
|
|
DIR_SOURCE_ROOT = os.environ.get(
|
|
'CHECKOUT_SOURCE_ROOT',
|
|
os.path.abspath(
|
|
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)))
|
|
|
|
BUILD_COMMON_PATH = os.path.join(DIR_SOURCE_ROOT, 'build', 'util', 'lib',
|
|
'common')
|
|
|
|
TRACING_PATH = os.path.join(DIR_SOURCE_ROOT, 'third_party', 'catapult',
|
|
'tracing')
|
|
|
|
EU_STRIP_PATH = os.path.join(DIR_SOURCE_ROOT, 'buildtools', 'third_party',
|
|
'eu-strip', 'bin', 'eu-strip')
|
|
|
|
with _SysPath(BUILD_COMMON_PATH):
|
|
import perf_tests_results_helper # pylint: disable=import-error
|
|
|
|
with _SysPath(TRACING_PATH):
|
|
from tracing.value import convert_chart_json # pylint: disable=import-error
|
|
|
|
_BASE_CHART = {
|
|
'format_version': '0.1',
|
|
'benchmark_name': 'resource_sizes',
|
|
'trace_rerun_options': [],
|
|
'charts': {}
|
|
}
|
|
|
|
_KEY_RAW = 'raw'
|
|
_KEY_GZIPPED = 'gzipped'
|
|
_KEY_STRIPPED = 'stripped'
|
|
_KEY_STRIPPED_GZIPPED = 'stripped_then_gzipped'
|
|
|
|
|
|
class _Group:
|
|
"""A group of build artifacts whose file sizes are summed and tracked.
|
|
|
|
Build artifacts for size tracking fall under these categories:
|
|
* File: A single file.
|
|
* Group: A collection of files.
|
|
* Dir: All files under a directory.
|
|
|
|
Attributes:
|
|
paths: A list of files or directories to be tracked together.
|
|
title: The display name of the group.
|
|
track_stripped: Whether to also track summed stripped ELF sizes.
|
|
track_compressed: Whether to also track summed compressed sizes.
|
|
"""
|
|
|
|
def __init__(self, paths, title, track_stripped=False,
|
|
track_compressed=False):
|
|
self.paths = paths
|
|
self.title = title
|
|
self.track_stripped = track_stripped
|
|
self.track_compressed = track_compressed
|
|
|
|
def __eq__(self, other):
|
|
"""Overrides the default implementation"""
|
|
if isinstance(other, _Group):
|
|
return (self.paths == other.paths) & (self.title == other.title) & (
|
|
self.track_stripped == other.track_stripped) & (
|
|
self.track_compressed == other.track_compressed)
|
|
return False
|
|
|
|
# Common artifacts in official builder lacros-arm32 and lacros64 in
|
|
# src-internal. The artifcts can be found in
|
|
# chromium/src-internal/testing/buildbot/archive/lacros64.json and
|
|
# chromium/src-internal/testing/buildbot/archive/lacros-arm32.json
|
|
# chromium/src-internal/testing/buildbot/archive/lacros-arm64.json
|
|
_TRACKED_GROUPS = [
|
|
_Group(paths=['chrome'],
|
|
title='File: chrome',
|
|
track_stripped=True,
|
|
track_compressed=True),
|
|
_Group(paths=['chrome_crashpad_handler'],
|
|
title='File: chrome_crashpad_handler'),
|
|
_Group(paths=['icudtl.dat'], title='File: icudtl.dat'),
|
|
_Group(paths=['icudtl.dat.hash'], title='File: icudtl.dat.hash'),
|
|
_Group(paths=['libEGL.so'], title='File: libEGL.so'),
|
|
_Group(paths=['libGLESv2.so'], title='File: libGLESv2.so'),
|
|
_Group(paths=['nacl_helper'], title='File: nacl_helper'),
|
|
_Group(paths=['resources.pak'], title='File: resources.pak'),
|
|
_Group(paths=[
|
|
'chrome_100_percent.pak', 'chrome_200_percent.pak',
|
|
'headless_lib_data.pak', 'headless_lib_strings.pak'
|
|
],
|
|
title='Group: Other PAKs'),
|
|
_Group(paths=['snapshot_blob.bin'], title='Group: Misc'),
|
|
_Group(paths=['locales/'], title='Dir: locales'),
|
|
_Group(paths=['WidevineCdm/'], title='Dir: WidevineCdm'),
|
|
]
|
|
|
|
|
|
def _visit_paths(base_dir, paths):
|
|
"""Itemizes files specified by a list of paths.
|
|
|
|
Args:
|
|
base_dir: Base directory for all elements in |paths|.
|
|
paths: A list of filenames or directory names to specify files whose sizes
|
|
to be counted. Directories are recursed. There's no de-duping effort.
|
|
Non-existing files or directories are ignored (with warning message).
|
|
"""
|
|
for path in paths:
|
|
full_path = os.path.join(base_dir, path)
|
|
if os.path.exists(full_path):
|
|
if os.path.isdir(full_path):
|
|
for dirpath, _, filenames in os.walk(full_path):
|
|
for filename in filenames:
|
|
yield os.path.join(dirpath, filename)
|
|
else: # Assume is file.
|
|
yield full_path
|
|
else:
|
|
logging.critical('Not found: %s', path)
|
|
|
|
|
|
def _is_probably_elf(filename):
|
|
"""Heuristically decides whether |filename| is ELF via magic signature."""
|
|
with open(filename, 'rb') as fh:
|
|
return fh.read(4) == '\x7FELF'
|
|
|
|
|
|
def _is_unstrippable_elf(filename):
|
|
"""Identifies known-unstrippable ELF files to denoise the system."""
|
|
return filename.endswith('.nexe') or filename.endswith('libwidevinecdm.so')
|
|
|
|
|
|
def _get_filesize(filename):
|
|
"""Returns the size of a file, or 0 if file is not found."""
|
|
try:
|
|
return os.path.getsize(filename)
|
|
except OSError:
|
|
logging.critical('Failed to get size: %s', filename)
|
|
return 0
|
|
|
|
|
|
def _get_gzipped_filesize(filename):
|
|
"""Returns the gzipped size of a file, or 0 if file is not found."""
|
|
BUFFER_SIZE = 65536
|
|
if not os.path.isfile(filename):
|
|
return 0
|
|
try:
|
|
# Call gzip externally instead of using gzip package since it's > 2x faster.
|
|
cmd = ['gzip', '-c', filename]
|
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
|
# Manually counting bytes instead of using len(p.communicate()[0]) to avoid
|
|
# buffering the entire compressed data (can be ~100 MB).
|
|
ret = 0
|
|
while True:
|
|
chunk = len(p.stdout.read(BUFFER_SIZE))
|
|
if chunk == 0:
|
|
break
|
|
ret += chunk
|
|
return ret
|
|
except OSError:
|
|
logging.critical('Failed to get gzipped size: %s', filename)
|
|
return 0
|
|
|
|
|
|
def _get_catagorized_filesizes(filename):
|
|
"""Measures |filename| sizes under various transforms.
|
|
|
|
Returns: A Counter (keyed by _Key_* constants) that stores measured sizes.
|
|
"""
|
|
sizes = collections.Counter()
|
|
sizes[_KEY_RAW] = _get_filesize(filename)
|
|
sizes[_KEY_GZIPPED] = _get_gzipped_filesize(filename)
|
|
|
|
# Pre-assign values for non-ELF, or in case of failure for ELF.
|
|
sizes[_KEY_STRIPPED] = sizes[_KEY_RAW]
|
|
sizes[_KEY_STRIPPED_GZIPPED] = sizes[_KEY_GZIPPED]
|
|
|
|
if _is_probably_elf(filename) and not _is_unstrippable_elf(filename):
|
|
try:
|
|
fd, temp_file = tempfile.mkstemp()
|
|
os.close(fd)
|
|
cmd = [EU_STRIP_PATH, filename, '-o', temp_file]
|
|
subprocess.check_output(cmd)
|
|
sizes[_KEY_STRIPPED] = _get_filesize(temp_file)
|
|
sizes[_KEY_STRIPPED_GZIPPED] = _get_gzipped_filesize(temp_file)
|
|
if sizes[_KEY_STRIPPED] > sizes[_KEY_RAW]:
|
|
# This weird case has been observed for libwidevinecdm.so.
|
|
logging.critical('Stripping made things worse for %s' % filename)
|
|
except subprocess.CalledProcessError:
|
|
logging.critical('Failed to strip file: %s' % filename)
|
|
finally:
|
|
os.unlink(temp_file)
|
|
return sizes
|
|
|
|
|
|
def _dump_chart_json(output_dir, chartjson):
|
|
"""Writes chart histogram to JSON files.
|
|
|
|
Output files:
|
|
results-chart.json contains the chart JSON.
|
|
perf_results.json contains histogram JSON for Catapult.
|
|
|
|
Args:
|
|
output_dir: Directory to place the JSON files.
|
|
chartjson: Source JSON data for output files.
|
|
"""
|
|
results_path = os.path.join(output_dir, 'results-chart.json')
|
|
logging.critical('Dumping chartjson to %s', results_path)
|
|
with open(results_path, 'w') as json_file:
|
|
json.dump(chartjson, json_file, indent=2)
|
|
|
|
# We would ideally generate a histogram set directly instead of generating
|
|
# chartjson then converting. However, perf_tests_results_helper is in
|
|
# //build, which doesn't seem to have any precedent for depending on
|
|
# anything in Catapult. This can probably be fixed, but since this doesn't
|
|
# need to be super fast or anything, converting is a good enough solution
|
|
# for the time being.
|
|
histogram_result = convert_chart_json.ConvertChartJson(results_path)
|
|
if histogram_result.returncode != 0:
|
|
raise Exception('chartjson conversion failed with error: ' +
|
|
histogram_result.stdout)
|
|
|
|
histogram_path = os.path.join(output_dir, 'perf_results.json')
|
|
logging.critical('Dumping histograms to %s', histogram_path)
|
|
with open(histogram_path, 'wb') as json_file:
|
|
json_file.write(histogram_result.stdout)
|
|
|
|
|
|
def _run_resource_sizes(args):
|
|
"""Main flow to extract and output size data."""
|
|
chartjson = _BASE_CHART.copy()
|
|
chartjson.update({
|
|
'benchmark_description':
|
|
('LaCrOS %s resource size information.' % args.arch)
|
|
})
|
|
report_func = perf_tests_results_helper.ReportPerfResult
|
|
total_sizes = collections.Counter()
|
|
|
|
def report_sizes(sizes, title, track_stripped, track_compressed):
|
|
report_func(chart_data=chartjson,
|
|
graph_title=title,
|
|
trace_title='size',
|
|
value=sizes[_KEY_RAW],
|
|
units='bytes')
|
|
|
|
if track_stripped:
|
|
report_func(chart_data=chartjson,
|
|
graph_title=title + ' (Stripped)',
|
|
trace_title='size',
|
|
value=sizes[_KEY_STRIPPED],
|
|
units='bytes')
|
|
|
|
if track_compressed:
|
|
report_func(chart_data=chartjson,
|
|
graph_title=title + ' (Gzipped)',
|
|
trace_title='size',
|
|
value=sizes[_KEY_GZIPPED],
|
|
units='bytes')
|
|
|
|
if track_stripped and track_compressed:
|
|
report_func(chart_data=chartjson,
|
|
graph_title=title + ' (Stripped, Gzipped)',
|
|
trace_title='size',
|
|
value=sizes[_KEY_STRIPPED_GZIPPED],
|
|
units='bytes')
|
|
|
|
tracked_groups = _TRACKED_GROUPS.copy()
|
|
# Architecture amd64 requires artifact nacl_irt_x86_64.nexe.
|
|
if args.arch == 'amd64':
|
|
tracked_groups.append(
|
|
_Group(paths=['nacl_irt_x86_64.nexe'],
|
|
title='File: nacl_irt_x86_64.nexe'))
|
|
# Architecture arm32 requires artifact nacl_irt_arm.nexe.
|
|
elif args.arch == 'arm32':
|
|
tracked_groups.append(
|
|
_Group(paths=['nacl_irt_arm.nexe'], title='File: nacl_irt_arm.nexe'))
|
|
tracked_groups.append(
|
|
_Group(paths=['nacl_helper_bootstrap'],
|
|
title='File: nacl_helper_bootstrap'))
|
|
# TODO(https://crbug.com/1356761): remove the following part once nacl files
|
|
# are available.
|
|
elif args.arch == 'arm64':
|
|
tracked_groups.remove(
|
|
_Group(paths=['nacl_helper'], title='File: nacl_helper'))
|
|
for g in tracked_groups:
|
|
sizes = sum(
|
|
map(_get_catagorized_filesizes, _visit_paths(args.out_dir, g.paths)),
|
|
collections.Counter())
|
|
report_sizes(sizes, g.title, g.track_stripped, g.track_compressed)
|
|
|
|
# Total compressed size is summed over individual compressed sizes, instead
|
|
# of concatanating first, then compress everything. This is done for
|
|
# simplicity. It also gives a conservative size estimate (assuming file
|
|
# metadata and overheads are negligible).
|
|
total_sizes += sizes
|
|
|
|
report_sizes(total_sizes, 'Total', True, True)
|
|
|
|
_dump_chart_json(args.output_dir, chartjson)
|
|
|
|
|
|
def main():
|
|
"""Parses arguments and runs high level flows."""
|
|
argparser = argparse.ArgumentParser(description='Writes LaCrOS size metrics.')
|
|
|
|
argparser.add_argument('--chromium-output-directory',
|
|
dest='out_dir',
|
|
required=True,
|
|
type=os.path.realpath,
|
|
help='Location of the build artifacts.')
|
|
argparser.add_argument('--arch',
|
|
required=True,
|
|
type=str,
|
|
help='The architecture of lacros, valid values: amd64,'
|
|
' arm32, arm64')
|
|
|
|
output_group = argparser.add_mutually_exclusive_group()
|
|
|
|
output_group.add_argument('--output-dir',
|
|
default='.',
|
|
help='Directory to save chartjson to.')
|
|
|
|
# Accepted to conform to the isolated script interface, but ignored.
|
|
argparser.add_argument('--isolated-script-test-filter',
|
|
help=argparse.SUPPRESS)
|
|
argparser.add_argument('--isolated-script-test-perf-output',
|
|
type=os.path.realpath,
|
|
help=argparse.SUPPRESS)
|
|
|
|
output_group.add_argument(
|
|
'--isolated-script-test-output',
|
|
type=os.path.realpath,
|
|
help='File to which results will be written in the simplified JSON '
|
|
'output format.')
|
|
|
|
args = argparser.parse_args()
|
|
|
|
isolated_script_output = {'valid': False, 'failures': []}
|
|
if args.isolated_script_test_output:
|
|
test_name = 'lacros_resource_sizes'
|
|
args.output_dir = os.path.join(
|
|
os.path.dirname(args.isolated_script_test_output), test_name)
|
|
if not os.path.exists(args.output_dir):
|
|
os.makedirs(args.output_dir)
|
|
|
|
try:
|
|
_run_resource_sizes(args)
|
|
isolated_script_output = {'valid': True, 'failures': []}
|
|
finally:
|
|
if args.isolated_script_test_output:
|
|
results_path = os.path.join(args.output_dir, 'test_results.json')
|
|
with open(results_path, 'w') as output_file:
|
|
json.dump(isolated_script_output, output_file)
|
|
with open(args.isolated_script_test_output, 'w') as output_file:
|
|
json.dump(isolated_script_output, output_file)
|
|
result_sink_client = result_sink.TryInitClient()
|
|
if result_sink_client:
|
|
status = result_types.PASS
|
|
if not isolated_script_output['valid']:
|
|
status = result_types.UNKNOWN
|
|
elif isolated_script_output['failures']:
|
|
status = result_types.FAIL
|
|
result_sink_client.Post(test_name, status, None, None, None)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|