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 |