Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Prints the size of each given file and optionally computes the size of | 6 """Prints the size of each given file and optionally computes the size of |
| 7 libchrome.so without the dependencies added for building with android NDK. | 7 libchrome.so without the dependencies added for building with android NDK. |
| 8 Also breaks down the contents of the APK to determine the installed size | 8 Also breaks down the contents of the APK to determine the installed size |
| 9 and assign size contributions to different classes of file. | 9 and assign size contributions to different classes of file. |
| 10 """ | 10 """ |
| 11 | 11 |
| 12 import collections | 12 import collections |
| 13 from contextlib import contextmanager | |
| 13 import json | 14 import json |
| 14 import logging | 15 import logging |
| 15 import operator | 16 import operator |
| 16 import optparse | 17 import optparse |
| 17 import os | 18 import os |
| 18 import re | 19 import re |
| 19 import shutil | |
| 20 import struct | 20 import struct |
| 21 import sys | 21 import sys |
| 22 import tempfile | 22 import tempfile |
| 23 import zipfile | 23 import zipfile |
| 24 import zlib | 24 import zlib |
| 25 | 25 |
| 26 import devil_chromium | 26 import devil_chromium |
| 27 from devil.android.sdk import build_tools | 27 from devil.android.sdk import build_tools |
| 28 from devil.utils import cmd_helper | 28 from devil.utils import cmd_helper |
| 29 from devil.utils import lazy | 29 from devil.utils import lazy |
| 30 import method_count | 30 import method_count |
| 31 from pylib import constants | 31 from pylib import constants |
| 32 from pylib.constants import host_paths | 32 from pylib.constants import host_paths |
| 33 | 33 |
| 34 _AAPT_PATH = lazy.WeakConstant(lambda: build_tools.GetPath('aapt')) | 34 _AAPT_PATH = lazy.WeakConstant(lambda: build_tools.GetPath('aapt')) |
| 35 _GRIT_PATH = os.path.join(host_paths.DIR_SOURCE_ROOT, 'tools', 'grit') | 35 _GRIT_PATH = os.path.join(host_paths.DIR_SOURCE_ROOT, 'tools', 'grit') |
| 36 _BUILD_UTILS_PATH = os.path.join( | |
| 37 host_paths.DIR_SOURCE_ROOT, 'build', 'android', 'gyp') | |
| 36 | 38 |
| 37 # Prepend the grit module from the source tree so it takes precedence over other | 39 # Prepend the grit module from the source tree so it takes precedence over other |
| 38 # grit versions that might present in the search path. | 40 # grit versions that might present in the search path. |
| 39 with host_paths.SysPath(_GRIT_PATH, 1): | 41 with host_paths.SysPath(_GRIT_PATH, 1): |
| 40 from grit.format import data_pack # pylint: disable=import-error | 42 from grit.format import data_pack # pylint: disable=import-error |
| 41 | 43 |
| 42 with host_paths.SysPath(host_paths.BUILD_COMMON_PATH): | 44 with host_paths.SysPath(host_paths.BUILD_COMMON_PATH): |
| 43 import perf_tests_results_helper # pylint: disable=import-error | 45 import perf_tests_results_helper # pylint: disable=import-error |
| 44 | 46 |
| 47 with host_paths.SysPath(_BUILD_UTILS_PATH, 1): | |
| 48 from util import build_utils # pylint: disable=import-error | |
| 49 | |
| 45 | 50 |
| 46 # Python had a bug in zipinfo parsing that triggers on ChromeModern.apk | 51 # Python had a bug in zipinfo parsing that triggers on ChromeModern.apk |
| 47 # https://bugs.python.org/issue14315 | 52 # https://bugs.python.org/issue14315 |
| 48 def _PatchedDecodeExtra(self): | 53 def _PatchedDecodeExtra(self): |
| 49 # Try to decode the extra field. | 54 # Try to decode the extra field. |
| 50 extra = self.extra | 55 extra = self.extra |
| 51 unpack = struct.unpack | 56 unpack = struct.unpack |
| 52 while len(extra) >= 4: | 57 while len(extra) >= 4: |
| 53 tp, ln = unpack('<HH', extra[:4]) | 58 tp, ln = unpack('<HH', extra[:4]) |
| 54 if tp == 1: | 59 if tp == 1: |
| (...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 108 'symbols': ['.dynsym', '.dynstr', '.dynamic', '.shstrtab', '.got', '.plt', | 113 'symbols': ['.dynsym', '.dynstr', '.dynamic', '.shstrtab', '.got', '.plt', |
| 109 '.got.plt', '.hash'], | 114 '.got.plt', '.hash'], |
| 110 'bss': ['.bss'], | 115 'bss': ['.bss'], |
| 111 'other': ['.init_array', '.fini_array', '.comment', '.note.gnu.gold-version', | 116 'other': ['.init_array', '.fini_array', '.comment', '.note.gnu.gold-version', |
| 112 '.ARM.attributes', '.note.gnu.build-id', '.gnu.version', | 117 '.ARM.attributes', '.note.gnu.build-id', '.gnu.version', |
| 113 '.gnu.version_d', '.gnu.version_r', '.interp', '.gcc_except_table'] | 118 '.gnu.version_d', '.gnu.version_r', '.interp', '.gcc_except_table'] |
| 114 } | 119 } |
| 115 | 120 |
| 116 | 121 |
| 117 def _ExtractMainLibSectionSizesFromApk(apk_path, main_lib_path): | 122 def _ExtractMainLibSectionSizesFromApk(apk_path, main_lib_path): |
| 118 tmpdir = tempfile.mkdtemp(suffix='_apk_extract') | 123 with Unzip(apk_path, pattern=main_lib_path) as extracted_lib_path: |
| 119 grouped_section_sizes = collections.defaultdict(int) | 124 grouped_section_sizes = collections.defaultdict(int) |
| 120 try: | 125 section_sizes = _CreateSectionNameSizeMap(extracted_lib_path) |
| 121 with zipfile.ZipFile(apk_path, 'r') as z: | 126 for group_name, section_names in _READELF_SIZES_METRICS.iteritems(): |
| 122 extracted_lib_path = z.extract(main_lib_path, tmpdir) | 127 for section_name in section_names: |
| 123 section_sizes = _CreateSectionNameSizeMap(extracted_lib_path) | 128 if section_name in section_sizes: |
| 129 grouped_section_sizes[group_name] += section_sizes.pop(section_name) | |
| 124 | 130 |
| 125 for group_name, section_names in _READELF_SIZES_METRICS.iteritems(): | 131 # Group any unknown section headers into the "other" group. |
| 126 for section_name in section_names: | 132 for section_header, section_size in section_sizes.iteritems(): |
| 127 if section_name in section_sizes: | 133 print "Unknown elf section header:", section_header |
| 128 grouped_section_sizes[group_name] += section_sizes.pop(section_name) | 134 grouped_section_sizes['other'] += section_size |
| 129 | 135 |
| 130 # Group any unknown section headers into the "other" group. | 136 return grouped_section_sizes |
| 131 for section_header, section_size in section_sizes.iteritems(): | |
| 132 print "Unknown elf section header:", section_header | |
| 133 grouped_section_sizes['other'] += section_size | |
| 134 | |
| 135 return grouped_section_sizes | |
| 136 finally: | |
| 137 shutil.rmtree(tmpdir) | |
| 138 | 137 |
| 139 | 138 |
| 140 def _CreateSectionNameSizeMap(so_path): | 139 def _CreateSectionNameSizeMap(so_path): |
| 141 stdout = cmd_helper.GetCmdOutput(['readelf', '-S', '--wide', so_path]) | 140 stdout = cmd_helper.GetCmdOutput(['readelf', '-S', '--wide', so_path]) |
| 142 section_sizes = {} | 141 section_sizes = {} |
| 143 # Matches [ 2] .hash HASH 00000000006681f0 0001f0 003154 04 A 3 0 8 | 142 # Matches [ 2] .hash HASH 00000000006681f0 0001f0 003154 04 A 3 0 8 |
| 144 for match in re.finditer(r'\[[\s\d]+\] (\..*)$', stdout, re.MULTILINE): | 143 for match in re.finditer(r'\[[\s\d]+\] (\..*)$', stdout, re.MULTILINE): |
| 145 items = match.group(1).split() | 144 items = match.group(1).split() |
| 146 section_sizes[items[0]] = int(items[4], 16) | 145 section_sizes[items[0]] = int(items[4], 16) |
| 147 | 146 |
| 148 return section_sizes | 147 return section_sizes |
| 149 | 148 |
| 150 | 149 |
| 150 def _ParseLibBuildId(so_path): | |
| 151 """Returns the Build ID of the given native library.""" | |
| 152 stdout = cmd_helper.GetCmdOutput(['readelf', '-n', so_path]) | |
| 153 match = re.search(r'Build ID: (\w{40})', stdout) | |
|
agrieve
2017/02/24 20:04:21
nit: BuildIDs don't need to be 40 characters (alth
estevenson
2017/02/28 00:58:45
Left it in. Done.
| |
| 154 return match.group(1) if match else None | |
| 155 | |
| 156 | |
| 151 def CountStaticInitializers(so_path): | 157 def CountStaticInitializers(so_path): |
| 152 # Static initializers expected in official builds. Note that this list is | 158 # Static initializers expected in official builds. Note that this list is |
| 153 # built using 'nm' on libchrome.so which results from a GCC official build | 159 # built using 'nm' on libchrome.so which results from a GCC official build |
| 154 # (i.e. Clang is not supported currently). | 160 # (i.e. Clang is not supported currently). |
| 155 def get_elf_section_size(readelf_stdout, section_name): | 161 def get_elf_section_size(readelf_stdout, section_name): |
| 156 # Matches: .ctors PROGBITS 000000000516add0 5169dd0 000010 00 WA 0 0 8 | 162 # Matches: .ctors PROGBITS 000000000516add0 5169dd0 000010 00 WA 0 0 8 |
| 157 match = re.search(r'\.%s.*$' % re.escape(section_name), | 163 match = re.search(r'\.%s.*$' % re.escape(section_name), |
| 158 readelf_stdout, re.MULTILINE) | 164 readelf_stdout, re.MULTILINE) |
| 159 if not match: | 165 if not match: |
| 160 return (False, -1) | 166 return (False, -1) |
| (...skipping 427 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 588 with zipfile.ZipFile(apk_filename) as z: | 594 with zipfile.ZipFile(apk_filename) as z: |
| 589 infolist = z.infolist() | 595 infolist = z.infolist() |
| 590 out_dir = constants.GetOutDirectory() | 596 out_dir = constants.GetOutDirectory() |
| 591 si_count = 0 | 597 si_count = 0 |
| 592 for zip_info in infolist: | 598 for zip_info in infolist: |
| 593 # Check file size to account for placeholder libraries. | 599 # Check file size to account for placeholder libraries. |
| 594 if zip_info.filename.endswith('.so') and zip_info.file_size > 0: | 600 if zip_info.filename.endswith('.so') and zip_info.file_size > 0: |
| 595 lib_name = os.path.basename(zip_info.filename).replace('crazy.', '') | 601 lib_name = os.path.basename(zip_info.filename).replace('crazy.', '') |
| 596 unstripped_path = os.path.join(out_dir, 'lib.unstripped', lib_name) | 602 unstripped_path = os.path.join(out_dir, 'lib.unstripped', lib_name) |
| 597 if os.path.exists(unstripped_path): | 603 if os.path.exists(unstripped_path): |
| 598 si_count += _PrintStaticInitializersCount(unstripped_path) | 604 si_count += _PrintStaticInitializersCount( |
| 605 apk_filename, zip_info.filename, unstripped_path) | |
| 599 else: | 606 else: |
| 600 raise Exception('Unstripped .so not found. Looked here: %s', | 607 raise Exception('Unstripped .so not found. Looked here: %s', |
| 601 unstripped_path) | 608 unstripped_path) |
| 602 ReportPerfResult(chartjson, 'StaticInitializersCount', 'count', si_count, | 609 ReportPerfResult(chartjson, 'StaticInitializersCount', 'count', si_count, |
| 603 'count') | 610 'count') |
| 604 | 611 |
| 605 | 612 |
| 606 def _PrintStaticInitializersCount(so_with_symbols_path): | 613 def _PrintStaticInitializersCount(apk_path, apk_so_name, so_with_symbols_path): |
| 607 """Counts the number of static initializers in the given shared library. | 614 """Counts the number of static initializers in the given shared library. |
| 608 Additionally, files for which static initializers were found are printed | 615 Additionally, files for which static initializers were found are printed |
| 609 on the standard output. | 616 on the standard output. |
| 610 | 617 |
| 611 Args: | 618 Args: |
| 612 so_with_symbols_path: Path to the unstripped libchrome.so file. | 619 apk_path: Path to the apk. |
| 613 | 620 apk_so_name: Name of the so. |
| 621 so_with_symbols_path: Path to the unstripped libchrome.so file. | |
| 614 Returns: | 622 Returns: |
| 615 The number of static initializers found. | 623 The number of static initializers found. |
| 616 """ | 624 """ |
| 617 # GetStaticInitializers uses get-static-initializers.py to get a list of all | 625 # GetStaticInitializers uses get-static-initializers.py to get a list of all |
| 618 # static initializers. This does not work on all archs (particularly arm). | 626 # static initializers. This does not work on all archs (particularly arm). |
| 619 # TODO(rnephew): Get rid of warning when crbug.com/585588 is fixed. | 627 # TODO(rnephew): Get rid of warning when crbug.com/585588 is fixed. |
| 620 si_count = CountStaticInitializers(so_with_symbols_path) | 628 with Unzip(apk_path, pattern="*%s" % apk_so_name) as unzipped_so: |
| 629 _VerifyLibBuildIdsMatch(unzipped_so, so_with_symbols_path) | |
| 630 si_count = CountStaticInitializers(unzipped_so) | |
| 621 static_initializers = GetStaticInitializers(so_with_symbols_path) | 631 static_initializers = GetStaticInitializers(so_with_symbols_path) |
| 622 static_initializers_count = len(static_initializers) - 1 # Minus summary. | 632 static_initializers_count = len(static_initializers) - 1 # Minus summary. |
| 623 if si_count != static_initializers_count: | 633 if si_count != static_initializers_count: |
| 624 print ('There are %d files with static initializers, but ' | 634 print ('There are %d files with static initializers, but ' |
| 625 'dump-static-initializers found %d:' % | 635 'dump-static-initializers found %d:' % |
| 626 (si_count, static_initializers_count)) | 636 (si_count, static_initializers_count)) |
| 627 else: | 637 else: |
| 628 print '%s - Found %d files with static initializers:' % ( | 638 print '%s - Found %d files with static initializers:' % ( |
| 629 os.path.basename(so_with_symbols_path), si_count) | 639 os.path.basename(so_with_symbols_path), si_count) |
| 630 print '\n'.join(static_initializers) | 640 print '\n'.join(static_initializers) |
| (...skipping 28 matching lines...) Expand all Loading... | |
| 659 graph_title = os.path.basename(apk_filename) + '_Dex' | 669 graph_title = os.path.basename(apk_filename) + '_Dex' |
| 660 dex_metrics = method_count.CONTRIBUTORS_TO_DEX_CACHE | 670 dex_metrics = method_count.CONTRIBUTORS_TO_DEX_CACHE |
| 661 for key, label in dex_metrics.iteritems(): | 671 for key, label in dex_metrics.iteritems(): |
| 662 ReportPerfResult(chartjson, graph_title, label, sizes[key], 'entries') | 672 ReportPerfResult(chartjson, graph_title, label, sizes[key], 'entries') |
| 663 | 673 |
| 664 graph_title = '%sCache' % graph_title | 674 graph_title = '%sCache' % graph_title |
| 665 ReportPerfResult(chartjson, graph_title, 'DexCache', sizes['dex_cache_size'], | 675 ReportPerfResult(chartjson, graph_title, 'DexCache', sizes['dex_cache_size'], |
| 666 'bytes') | 676 'bytes') |
| 667 | 677 |
| 668 | 678 |
| 679 @contextmanager | |
| 680 def Unzip(zip_file, pattern=None, predicate=None): | |
| 681 """Utility for temporary use of a set of files in a zip archive.""" | |
| 682 with build_utils.TempDir() as unzipped_dir: | |
| 683 unzipped_files = build_utils.ExtractAll( | |
| 684 zip_file, unzipped_dir, True, pattern=pattern, predicate=predicate) | |
| 685 yield unzipped_files[0] if len(unzipped_files) == 1 else unzipped_files | |
| 686 | |
| 687 | |
| 688 def _VerifyLibBuildIdsMatch(*so_files): | |
| 689 if len(set(_ParseLibBuildId(f) for f in so_files)) > 1: | |
| 690 raise Exception('Found differing build ids in output directory and apk. ' | |
| 691 'Your output directory is likely stale.') | |
| 692 | |
| 693 | |
| 669 def main(argv): | 694 def main(argv): |
| 670 usage = """Usage: %prog [options] file1 file2 ... | 695 usage = """Usage: %prog [options] file1 file2 ... |
| 671 | 696 |
| 672 Pass any number of files to graph their sizes. Any files with the extension | 697 Pass any number of files to graph their sizes. Any files with the extension |
| 673 '.apk' will be broken down into their components on a separate graph.""" | 698 '.apk' will be broken down into their components on a separate graph.""" |
| 674 option_parser = optparse.OptionParser(usage=usage) | 699 option_parser = optparse.OptionParser(usage=usage) |
| 675 option_parser.add_option('--so-path', | 700 option_parser.add_option('--so-path', |
| 676 help='Obsolete. Pass .so as positional arg instead.') | 701 help='Obsolete. Pass .so as positional arg instead.') |
| 677 option_parser.add_option('--so-with-symbols-path', | 702 option_parser.add_option('--so-with-symbols-path', |
| 678 help='Mostly obsolete. Use .so within .apk instead.') | 703 help='Mostly obsolete. Use .so within .apk instead.') |
| (...skipping 27 matching lines...) Expand all Loading... | |
| 706 | 731 |
| 707 # For backward compatibilty with buildbot scripts, treat --so-path as just | 732 # For backward compatibilty with buildbot scripts, treat --so-path as just |
| 708 # another file to print the size of. We don't need it for anything special any | 733 # another file to print the size of. We don't need it for anything special any |
| 709 # more. | 734 # more. |
| 710 if options.so_path: | 735 if options.so_path: |
| 711 files.append(options.so_path) | 736 files.append(options.so_path) |
| 712 | 737 |
| 713 if not files: | 738 if not files: |
| 714 option_parser.error('Must specify a file') | 739 option_parser.error('Must specify a file') |
| 715 | 740 |
| 716 if options.so_with_symbols_path: | |
| 717 si_count = _PrintStaticInitializersCount(options.so_with_symbols_path) | |
| 718 ReportPerfResult(chartjson, 'StaticInitializersCount', 'count', si_count, | |
| 719 'count') | |
| 720 | |
| 721 PrintResourceSizes(files, chartjson=chartjson) | 741 PrintResourceSizes(files, chartjson=chartjson) |
| 722 | 742 |
| 723 for f in files: | 743 for f in files: |
| 724 if f.endswith('.apk'): | 744 if f.endswith('.apk'): |
| 725 PrintApkAnalysis(f, chartjson=chartjson) | 745 PrintApkAnalysis(f, chartjson=chartjson) |
| 726 _PrintDexAnalysis(f, chartjson=chartjson) | 746 _PrintDexAnalysis(f, chartjson=chartjson) |
| 727 if not options.no_output_dir: | 747 if not options.no_output_dir: |
| 728 PrintPakAnalysis(f, options.min_pak_resource_size) | 748 PrintPakAnalysis(f, options.min_pak_resource_size) |
| 729 if not options.so_with_symbols_path: | 749 so_path = options.so_with_symbols_path |
|
agrieve
2017/02/24 20:04:21
This actually makes less sense inside the loop. Ho
estevenson
2017/02/28 00:58:45
I removed obsolete args and changed the script to
| |
| 750 if so_path: | |
| 751 si_count = _PrintStaticInitializersCount( | |
| 752 f, os.path.basename(so_path), so_path) | |
| 753 ReportPerfResult( | |
| 754 chartjson, 'StaticInitializersCount', 'count', si_count, 'count') | |
| 755 else: | |
| 730 _PrintStaticInitializersCountFromApk(f, chartjson=chartjson) | 756 _PrintStaticInitializersCountFromApk(f, chartjson=chartjson) |
| 731 | 757 |
| 732 if chartjson: | 758 if chartjson: |
| 733 results_path = os.path.join(options.output_dir, 'results-chart.json') | 759 results_path = os.path.join(options.output_dir, 'results-chart.json') |
| 734 logging.critical('Dumping json to %s', results_path) | 760 logging.critical('Dumping json to %s', results_path) |
| 735 with open(results_path, 'w') as json_file: | 761 with open(results_path, 'w') as json_file: |
| 736 json.dump(chartjson, json_file) | 762 json.dump(chartjson, json_file) |
| 737 | 763 |
| 738 | 764 |
| 739 if __name__ == '__main__': | 765 if __name__ == '__main__': |
| 740 sys.exit(main(sys.argv)) | 766 sys.exit(main(sys.argv)) |
| OLD | NEW |