Index: build/android/resource_sizes.py |
diff --git a/build/android/resource_sizes.py b/build/android/resource_sizes.py |
index 6f531f052d764884f80eab68525b518591fe8637..976f888a6e877c4133088e783eb1f9f33d4f87f7 100755 |
--- a/build/android/resource_sizes.py |
+++ b/build/android/resource_sizes.py |
@@ -9,14 +9,14 @@ |
and assign size contributions to different classes of file. |
""" |
+import argparse |
import collections |
+from contextlib import contextmanager |
import json |
import logging |
import operator |
-import optparse |
import os |
import re |
-import shutil |
import struct |
import sys |
import tempfile |
@@ -33,6 +33,8 @@ from pylib.constants import host_paths |
_AAPT_PATH = lazy.WeakConstant(lambda: build_tools.GetPath('aapt')) |
_GRIT_PATH = os.path.join(host_paths.DIR_SOURCE_ROOT, 'tools', 'grit') |
+_BUILD_UTILS_PATH = os.path.join( |
+ host_paths.DIR_SOURCE_ROOT, 'build', 'android', 'gyp') |
# Prepend the grit module from the source tree so it takes precedence over other |
# grit versions that might present in the search path. |
@@ -42,6 +44,9 @@ with host_paths.SysPath(_GRIT_PATH, 1): |
with host_paths.SysPath(host_paths.BUILD_COMMON_PATH): |
import perf_tests_results_helper # pylint: disable=import-error |
+with host_paths.SysPath(_BUILD_UTILS_PATH, 1): |
+ from util import build_utils # pylint: disable=import-error |
+ |
# Python had a bug in zipinfo parsing that triggers on ChromeModern.apk |
# https://bugs.python.org/issue14315 |
@@ -114,31 +119,30 @@ _READELF_SIZES_METRICS = { |
} |
-def _ExtractMainLibSectionSizesFromApk(apk_path, main_lib_path): |
- tmpdir = tempfile.mkdtemp(suffix='_apk_extract') |
- grouped_section_sizes = collections.defaultdict(int) |
- try: |
- with zipfile.ZipFile(apk_path, 'r') as z: |
- extracted_lib_path = z.extract(main_lib_path, tmpdir) |
- section_sizes = _CreateSectionNameSizeMap(extracted_lib_path) |
+def _RunReadelf(so_path, options, tools_prefix=''): |
+ return cmd_helper.GetCmdOutput( |
+ [tools_prefix + 'readelf'] + options + [so_path]) |
- for group_name, section_names in _READELF_SIZES_METRICS.iteritems(): |
- for section_name in section_names: |
- if section_name in section_sizes: |
- grouped_section_sizes[group_name] += section_sizes.pop(section_name) |
- # Group any unknown section headers into the "other" group. |
- for section_header, section_size in section_sizes.iteritems(): |
- print "Unknown elf section header:", section_header |
- grouped_section_sizes['other'] += section_size |
+def _ExtractMainLibSectionSizesFromApk(apk_path, main_lib_path, tools_prefix): |
+ with Unzip(apk_path, filename=main_lib_path) as extracted_lib_path: |
+ grouped_section_sizes = collections.defaultdict(int) |
+ section_sizes = _CreateSectionNameSizeMap(extracted_lib_path, tools_prefix) |
+ for group_name, section_names in _READELF_SIZES_METRICS.iteritems(): |
+ for section_name in section_names: |
+ if section_name in section_sizes: |
+ grouped_section_sizes[group_name] += section_sizes.pop(section_name) |
- return grouped_section_sizes |
- finally: |
- shutil.rmtree(tmpdir) |
+ # Group any unknown section headers into the "other" group. |
+ for section_header, section_size in section_sizes.iteritems(): |
+ print "Unknown elf section header:", section_header |
+ grouped_section_sizes['other'] += section_size |
+ return grouped_section_sizes |
-def _CreateSectionNameSizeMap(so_path): |
- stdout = cmd_helper.GetCmdOutput(['readelf', '-S', '--wide', so_path]) |
+ |
+def _CreateSectionNameSizeMap(so_path, tools_prefix): |
+ stdout = _RunReadelf(so_path, ['-S', '--wide'], tools_prefix) |
section_sizes = {} |
# Matches [ 2] .hash HASH 00000000006681f0 0001f0 003154 04 A 3 0 8 |
for match in re.finditer(r'\[[\s\d]+\] (\..*)$', stdout, re.MULTILINE): |
@@ -148,7 +152,14 @@ def _CreateSectionNameSizeMap(so_path): |
return section_sizes |
-def CountStaticInitializers(so_path): |
+def _ParseLibBuildId(so_path, tools_prefix): |
+ """Returns the Build ID of the given native library.""" |
+ stdout = _RunReadelf(so_path, ['n'], tools_prefix) |
+ match = re.search(r'Build ID: (\w+)', stdout) |
+ return match.group(1) if match else None |
+ |
+ |
+def CountStaticInitializers(so_path, tools_prefix): |
# Static initializers expected in official builds. Note that this list is |
# built using 'nm' on libchrome.so which results from a GCC official build |
# (i.e. Clang is not supported currently). |
@@ -163,7 +174,7 @@ def CountStaticInitializers(so_path): |
# Find the number of files with at least one static initializer. |
# First determine if we're 32 or 64 bit |
- stdout = cmd_helper.GetCmdOutput(['readelf', '-h', so_path]) |
+ stdout = _RunReadelf(so_path, ['-h'], tools_prefix) |
elf_class_line = re.search('Class:.*$', stdout, re.MULTILINE).group(0) |
elf_class = re.split(r'\W+', elf_class_line)[1] |
if elf_class == 'ELF32': |
@@ -175,7 +186,7 @@ def CountStaticInitializers(so_path): |
# NOTE: this is very implementation-specific and makes assumptions |
# about how compiler and linker implement global static initializers. |
si_count = 0 |
- stdout = cmd_helper.GetCmdOutput(['readelf', '-SW', so_path]) |
+ stdout = _RunReadelf(so_path, ['-SW'], tools_prefix) |
has_init_array, init_array_size = get_elf_section_size(stdout, 'init_array') |
if has_init_array: |
si_count = init_array_size / word_size |
@@ -183,10 +194,11 @@ def CountStaticInitializers(so_path): |
return si_count |
-def GetStaticInitializers(so_path): |
+def GetStaticInitializers(so_path, tools_prefix): |
output = cmd_helper.GetCmdOutput([_DUMP_STATIC_INITIALIZERS_PATH, '-d', |
- so_path]) |
- return output.splitlines() |
+ so_path, '-t', tools_prefix]) |
+ summary = re.search(r'Found \d+ static initializers in (\d+) files.', output) |
+ return output.splitlines()[:-1], int(summary.group(1)) |
def _NormalizeResourcesArsc(apk_path): |
@@ -258,17 +270,6 @@ def ReportPerfResult(chart_data, graph_title, trace_title, value, units, |
graph_title, trace_title, [value], units) |
-def PrintResourceSizes(files, chartjson=None): |
- """Prints the sizes of each given file. |
- |
- Args: |
- files: List of files to print sizes for. |
- """ |
- for f in files: |
- ReportPerfResult(chartjson, 'ResourceSizes', os.path.basename(f) + ' size', |
- os.path.getsize(f), 'bytes') |
- |
- |
class _FileGroup(object): |
"""Represents a category that apk files can fall into.""" |
@@ -311,7 +312,7 @@ class _FileGroup(object): |
return self.ComputeExtractedSize() + self.ComputeZippedSize() |
-def PrintApkAnalysis(apk_filename, chartjson=None): |
+def PrintApkAnalysis(apk_filename, tools_prefix, chartjson=None): |
"""Analyse APK to determine size contributions of different file classes.""" |
file_groups = [] |
@@ -415,7 +416,7 @@ def PrintApkAnalysis(apk_filename, chartjson=None): |
'other lib size', secondary_size, 'bytes') |
main_lib_section_sizes = _ExtractMainLibSectionSizesFromApk( |
- apk_filename, main_lib_info.filename) |
+ apk_filename, main_lib_info.filename, tools_prefix) |
for metric_name, size in main_lib_section_sizes.iteritems(): |
ReportPerfResult(chartjson, apk_basename + '_MainLibInfo', |
metric_name, size, 'bytes') |
@@ -583,7 +584,8 @@ def _AnnotatePakResources(): |
return id_name_map, id_header_map |
-def _PrintStaticInitializersCountFromApk(apk_filename, chartjson=None): |
+def _PrintStaticInitializersCountFromApk(apk_filename, tools_prefix, |
+ chartjson=None): |
print 'Finding static initializers (can take a minute)' |
with zipfile.ZipFile(apk_filename) as z: |
infolist = z.infolist() |
@@ -595,7 +597,8 @@ def _PrintStaticInitializersCountFromApk(apk_filename, chartjson=None): |
lib_name = os.path.basename(zip_info.filename).replace('crazy.', '') |
unstripped_path = os.path.join(out_dir, 'lib.unstripped', lib_name) |
if os.path.exists(unstripped_path): |
- si_count += _PrintStaticInitializersCount(unstripped_path) |
+ si_count += _PrintStaticInitializersCount( |
+ apk_filename, zip_info.filename, unstripped_path, tools_prefix) |
else: |
raise Exception('Unstripped .so not found. Looked here: %s', |
unstripped_path) |
@@ -603,33 +606,38 @@ def _PrintStaticInitializersCountFromApk(apk_filename, chartjson=None): |
'count') |
-def _PrintStaticInitializersCount(so_with_symbols_path): |
+def _PrintStaticInitializersCount(apk_path, apk_so_name, so_with_symbols_path, |
+ tools_prefix): |
"""Counts the number of static initializers in the given shared library. |
Additionally, files for which static initializers were found are printed |
on the standard output. |
Args: |
- so_with_symbols_path: Path to the unstripped libchrome.so file. |
- |
+ apk_path: Path to the apk. |
+ apk_so_name: Name of the so. |
+ so_with_symbols_path: Path to the unstripped libchrome.so file. |
+ tools_prefix: Prefix for arch-specific version of binary utility tools. |
Returns: |
The number of static initializers found. |
""" |
# GetStaticInitializers uses get-static-initializers.py to get a list of all |
# static initializers. This does not work on all archs (particularly arm). |
# TODO(rnephew): Get rid of warning when crbug.com/585588 is fixed. |
- si_count = CountStaticInitializers(so_with_symbols_path) |
- static_initializers = GetStaticInitializers(so_with_symbols_path) |
- static_initializers_count = len(static_initializers) - 1 # Minus summary. |
- if si_count != static_initializers_count: |
+ with Unzip(apk_path, filename=apk_so_name) as unzipped_so: |
+ _VerifyLibBuildIdsMatch(tools_prefix, unzipped_so, so_with_symbols_path) |
+ readelf_si_count = CountStaticInitializers(unzipped_so, tools_prefix) |
+ sis, dump_si_count = GetStaticInitializers( |
+ so_with_symbols_path, tools_prefix) |
+ if readelf_si_count != dump_si_count: |
print ('There are %d files with static initializers, but ' |
- 'dump-static-initializers found %d:' % |
- (si_count, static_initializers_count)) |
+ 'dump-static-initializers found %d: files' % |
+ (readelf_si_count, dump_si_count)) |
else: |
print '%s - Found %d files with static initializers:' % ( |
- os.path.basename(so_with_symbols_path), si_count) |
- print '\n'.join(static_initializers) |
+ os.path.basename(so_with_symbols_path), dump_si_count) |
+ print '\n'.join(sis) |
- return si_count |
+ return readelf_si_count |
def _FormatBytes(byts): |
"""Pretty-print a number of bytes.""" |
@@ -666,75 +674,71 @@ def _PrintDexAnalysis(apk_filename, chartjson=None): |
'bytes') |
-def main(argv): |
- usage = """Usage: %prog [options] file1 file2 ... |
- |
-Pass any number of files to graph their sizes. Any files with the extension |
-'.apk' will be broken down into their components on a separate graph.""" |
- option_parser = optparse.OptionParser(usage=usage) |
- option_parser.add_option('--so-path', |
- help='Obsolete. Pass .so as positional arg instead.') |
- option_parser.add_option('--so-with-symbols-path', |
- help='Mostly obsolete. Use .so within .apk instead.') |
- option_parser.add_option('--min-pak-resource-size', type='int', |
- default=20*1024, |
- help='Minimum byte size of displayed pak resources.') |
- option_parser.add_option('--build_type', dest='build_type', default='Debug', |
- help='Obsoleted by --chromium-output-directory.') |
- option_parser.add_option('--chromium-output-directory', |
- help='Location of the build artifacts. ' |
- 'Takes precidence over --build_type.') |
- option_parser.add_option('--chartjson', action='store_true', |
- help='Sets output mode to chartjson.') |
- option_parser.add_option('--output-dir', default='.', |
- help='Directory to save chartjson to.') |
- option_parser.add_option('--no-output-dir', action='store_true', |
- help='Skip all measurements that rely on having ' |
- 'output-dir') |
- option_parser.add_option('-d', '--device', |
- help='Dummy option for perf runner.') |
- options, args = option_parser.parse_args(argv) |
- files = args[1:] |
- chartjson = _BASE_CHART.copy() if options.chartjson else None |
- |
- constants.SetBuildType(options.build_type) |
- if options.chromium_output_directory: |
- constants.SetOutputDirectory(options.chromium_output_directory) |
- if not options.no_output_dir: |
+@contextmanager |
+def Unzip(zip_file, filename=None): |
+ """Utility for temporary use of a single file in a zip archive.""" |
+ with build_utils.TempDir() as unzipped_dir: |
+ unzipped_files = build_utils.ExtractAll( |
+ zip_file, unzipped_dir, True, pattern=filename) |
+ if len(unzipped_files) == 0: |
+ raise Exception( |
+ '%s not found in %s' % (filename, zip_file)) |
+ yield unzipped_files[0] |
+ |
+ |
+def _VerifyLibBuildIdsMatch(tools_prefix, *so_files): |
+ if len(set(_ParseLibBuildId(f, tools_prefix) for f in so_files)) > 1: |
+ raise Exception('Found differing build ids in output directory and apk. ' |
+ 'Your output directory is likely stale.') |
+ |
+ |
+def _ReadBuildVars(output_dir): |
+ with open(os.path.join(output_dir, 'build_vars.txt')) as f: |
+ return dict(l.replace('//', '').rstrip().split('=', 1) for l in f) |
+ |
+ |
+def main(): |
+ argparser = argparse.ArgumentParser(description='Print APK size metrics.') |
+ argparser.add_argument('--min-pak-resource-size', type=int, default=20*1024, |
+ help='Minimum byte size of displayed pak resources.') |
+ argparser.add_argument('--chromium-output-directory', |
+ help='Location of the build artifacts.') |
+ argparser.add_argument('--chartjson', action='store_true', |
+ help='Sets output mode to chartjson.') |
+ argparser.add_argument('--output-dir', default='.', |
+ help='Directory to save chartjson to.') |
+ argparser.add_argument('--no-output-dir', action='store_true', |
+ help='Skip all measurements that rely on having ' |
+ 'output-dir') |
+ argparser.add_argument('-d', '--device', |
+ help='Dummy option for perf runner.') |
+ argparser.add_argument('apk', help='APK file path.') |
+ args = argparser.parse_args() |
+ |
+ chartjson = _BASE_CHART.copy() if args.chartjson else None |
+ |
+ if args.chromium_output_directory: |
+ constants.SetOutputDirectory(args.chromium_output_directory) |
+ if not args.no_output_dir: |
constants.CheckOutputDirectory() |
devil_chromium.Initialize() |
- |
- # For backward compatibilty with buildbot scripts, treat --so-path as just |
- # another file to print the size of. We don't need it for anything special any |
- # more. |
- if options.so_path: |
- files.append(options.so_path) |
- |
- if not files: |
- option_parser.error('Must specify a file') |
- |
- if options.so_with_symbols_path: |
- si_count = _PrintStaticInitializersCount(options.so_with_symbols_path) |
- ReportPerfResult(chartjson, 'StaticInitializersCount', 'count', si_count, |
- 'count') |
- |
- PrintResourceSizes(files, chartjson=chartjson) |
- |
- for f in files: |
- if f.endswith('.apk'): |
- PrintApkAnalysis(f, chartjson=chartjson) |
- _PrintDexAnalysis(f, chartjson=chartjson) |
- if not options.no_output_dir: |
- PrintPakAnalysis(f, options.min_pak_resource_size) |
- if not options.so_with_symbols_path: |
- _PrintStaticInitializersCountFromApk(f, chartjson=chartjson) |
- |
+ build_vars = _ReadBuildVars(constants.GetOutDirectory()) |
+ tools_prefix = build_vars['android_tool_prefix'] |
+ else: |
+ tools_prefix = '' |
+ |
+ PrintApkAnalysis(args.apk, tools_prefix, chartjson=chartjson) |
+ _PrintDexAnalysis(args.apk, chartjson=chartjson) |
+ if not args.no_output_dir: |
+ PrintPakAnalysis(args.apk, args.min_pak_resource_size) |
+ _PrintStaticInitializersCountFromApk( |
+ args.apk, tools_prefix, chartjson=chartjson) |
if chartjson: |
- results_path = os.path.join(options.output_dir, 'results-chart.json') |
+ results_path = os.path.join(args.output_dir, 'results-chart.json') |
logging.critical('Dumping json to %s', results_path) |
with open(results_path, 'w') as json_file: |
json.dump(chartjson, json_file) |
if __name__ == '__main__': |
- sys.exit(main(sys.argv)) |
+ sys.exit(main()) |