| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2017 The Chromium Authors. All rights reserved. | 2 # Copyright 2017 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 """Tool for finding the cause of APK bloat. | 6 """Tool for finding the cause of binary size bloat. |
| 7 | 7 |
| 8 Run diagnose_apk_bloat.py -h for detailed usage help. | 8 See //tools/binary_size/README.md for example usage. |
| 9 |
| 10 Note: this tool will perform gclient sync/git checkout on your local repo if |
| 11 you don't use the --cloud option. |
| 9 """ | 12 """ |
| 10 | 13 |
| 11 import atexit | 14 import atexit |
| 12 import argparse | 15 import argparse |
| 13 import collections | 16 import collections |
| 14 from contextlib import contextmanager | 17 from contextlib import contextmanager |
| 15 import distutils.spawn | 18 import distutils.spawn |
| 16 import json | 19 import json |
| 17 import logging | 20 import logging |
| 18 import multiprocessing | 21 import multiprocessing |
| 19 import os | 22 import os |
| 20 import re | 23 import re |
| 21 import shutil | 24 import shutil |
| 22 import subprocess | 25 import subprocess |
| 23 import sys | 26 import sys |
| 24 import tempfile | 27 import tempfile |
| 25 import zipfile | 28 import zipfile |
| 26 | 29 |
| 27 _COMMIT_COUNT_WARN_THRESHOLD = 15 | 30 _COMMIT_COUNT_WARN_THRESHOLD = 15 |
| 28 _ALLOWED_CONSECUTIVE_FAILURES = 2 | 31 _ALLOWED_CONSECUTIVE_FAILURES = 2 |
| 29 _DIFF_DETAILS_LINES_THRESHOLD = 100 | 32 _DIFF_DETAILS_LINES_THRESHOLD = 100 |
| 30 _BUILDER_URL = \ | |
| 31 'https://build.chromium.org/p/chromium.perf/builders/Android%20Builder' | |
| 32 _SRC_ROOT = os.path.abspath( | 33 _SRC_ROOT = os.path.abspath( |
| 33 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) | 34 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) |
| 34 _DEFAULT_ARCHIVE_DIR = os.path.join(_SRC_ROOT, 'binary-size-bloat') | 35 _DEFAULT_ARCHIVE_DIR = os.path.join(_SRC_ROOT, 'binary-size-bloat') |
| 35 _DEFAULT_OUT_DIR = os.path.join(_SRC_ROOT, 'out', 'diagnose-apk-bloat') | 36 _DEFAULT_OUT_DIR = os.path.join(_SRC_ROOT, 'out', 'diagnose-apk-bloat') |
| 36 _DEFAULT_ANDROID_TARGET = 'monochrome_public_apk' | 37 _DEFAULT_ANDROID_TARGET = 'monochrome_public_apk' |
| 37 | 38 |
| 38 | 39 |
| 39 _DiffResult = collections.namedtuple('DiffResult', ['name', 'value', 'units']) | 40 _DiffResult = collections.namedtuple('DiffResult', ['name', 'value', 'units']) |
| 40 | 41 |
| 41 | 42 |
| (...skipping 165 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 207 elif 'monochrome' in self.target: | 208 elif 'monochrome' in self.target: |
| 208 return 'lib.unstripped/libmonochrome.so' | 209 return 'lib.unstripped/libmonochrome.so' |
| 209 else: | 210 else: |
| 210 return 'lib.unstripped/libchrome.so' | 211 return 'lib.unstripped/libchrome.so' |
| 211 | 212 |
| 212 @property | 213 @property |
| 213 def abs_main_lib_path(self): | 214 def abs_main_lib_path(self): |
| 214 return os.path.join(self.output_directory, self.main_lib_path) | 215 return os.path.join(self.output_directory, self.main_lib_path) |
| 215 | 216 |
| 216 @property | 217 @property |
| 218 def builder_url(self): |
| 219 url = 'https://build.chromium.org/p/chromium.perf/builders/%s%%20Builder' |
| 220 return url % self.target_os.title() |
| 221 |
| 222 @property |
| 217 def download_bucket(self): | 223 def download_bucket(self): |
| 218 return 'gs://chrome-perf/%s Builder/' % self.target_os.title() | 224 return 'gs://chrome-perf/%s Builder/' % self.target_os.title() |
| 219 | 225 |
| 220 @property | 226 @property |
| 221 def download_output_dir(self): | 227 def download_output_dir(self): |
| 222 return 'out/Release' if self.IsAndroid() else 'full-build-linux' | 228 return 'out/Release' if self.IsAndroid() else 'full-build-linux' |
| 223 | 229 |
| 224 @property | 230 @property |
| 225 def map_file_path(self): | 231 def map_file_path(self): |
| 226 return self.main_lib_path + '.map.gz' | 232 return self.main_lib_path + '.map.gz' |
| (...skipping 14 matching lines...) Expand all Loading... |
| 241 self.extra_gn_args_str = ' is_chrome_branded=true' | 247 self.extra_gn_args_str = ' is_chrome_branded=true' |
| 242 else: | 248 else: |
| 243 self.extra_gn_args_str = (' exclude_unwind_tables=true ' | 249 self.extra_gn_args_str = (' exclude_unwind_tables=true ' |
| 244 'ffmpeg_branding="Chrome" proprietary_codecs=true') | 250 'ffmpeg_branding="Chrome" proprietary_codecs=true') |
| 245 self.target = self.target if self.IsAndroid() else 'chrome' | 251 self.target = self.target if self.IsAndroid() else 'chrome' |
| 246 | 252 |
| 247 def _GenGnCmd(self): | 253 def _GenGnCmd(self): |
| 248 gn_args = 'is_official_build=true symbol_level=1' | 254 gn_args = 'is_official_build=true symbol_level=1' |
| 249 gn_args += ' use_goma=%s' % str(self.use_goma).lower() | 255 gn_args += ' use_goma=%s' % str(self.use_goma).lower() |
| 250 gn_args += ' target_os="%s"' % self.target_os | 256 gn_args += ' target_os="%s"' % self.target_os |
| 251 gn_args += (' enable_chrome_android_internal=%s' % | 257 if self.IsAndroid(): |
| 252 str(self.enable_chrome_android_internal).lower()) | 258 gn_args += (' enable_chrome_android_internal=%s' % |
| 259 str(self.enable_chrome_android_internal).lower()) |
| 253 gn_args += self.extra_gn_args_str | 260 gn_args += self.extra_gn_args_str |
| 254 return ['gn', 'gen', self.output_directory, '--args=%s' % gn_args] | 261 return ['gn', 'gen', self.output_directory, '--args=%s' % gn_args] |
| 255 | 262 |
| 256 def _GenNinjaCmd(self): | 263 def _GenNinjaCmd(self): |
| 257 cmd = ['ninja', '-C', self.output_directory] | 264 cmd = ['ninja', '-C', self.output_directory] |
| 258 cmd += ['-j', self.max_jobs] if self.max_jobs else [] | 265 cmd += ['-j', self.max_jobs] if self.max_jobs else [] |
| 259 cmd += ['-l', self.max_load_average] if self.max_load_average else [] | 266 cmd += ['-l', self.max_load_average] if self.max_load_average else [] |
| 260 cmd += [self.target] | 267 cmd += [self.target] |
| 261 return cmd | 268 return cmd |
| 262 | 269 |
| 263 def Run(self): | 270 def Run(self): |
| 264 """Run GN gen/ninja build and return the process returncode.""" | 271 """Run GN gen/ninja build and return the process returncode.""" |
| 265 logging.info('Building: %s.', self.target) | 272 logging.info('Building: %s (this might take a while).', self.target) |
| 266 retcode = _RunCmd( | 273 retcode = _RunCmd( |
| 267 self._GenGnCmd(), verbose=True, exit_on_failure=False)[1] | 274 self._GenGnCmd(), verbose=True, exit_on_failure=False)[1] |
| 268 if retcode: | 275 if retcode: |
| 269 return retcode | 276 return retcode |
| 270 return _RunCmd( | 277 return _RunCmd( |
| 271 self._GenNinjaCmd(), verbose=True, exit_on_failure=False)[1] | 278 self._GenNinjaCmd(), verbose=True, exit_on_failure=False)[1] |
| 272 | 279 |
| 273 def DownloadUrl(self, rev): | 280 def DownloadUrl(self, rev): |
| 274 return self.download_bucket + 'full-build-linux_%s.zip' % rev | 281 return self.download_bucket + 'full-build-linux_%s.zip' % rev |
| 275 | 282 |
| (...skipping 329 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 605 logging.info('Downloading build artifacts for %s', archive.rev) | 612 logging.info('Downloading build artifacts for %s', archive.rev) |
| 606 # gsutil writes stdout and stderr to stderr, so pipe stdout and stderr to | 613 # gsutil writes stdout and stderr to stderr, so pipe stdout and stderr to |
| 607 # sys.stdout. | 614 # sys.stdout. |
| 608 retcode = subprocess.call( | 615 retcode = subprocess.call( |
| 609 [gsutil_path, 'cp', build.DownloadUrl(archive.rev), dl_dst], | 616 [gsutil_path, 'cp', build.DownloadUrl(archive.rev), dl_dst], |
| 610 stdout=sys.stdout, stderr=subprocess.STDOUT) | 617 stdout=sys.stdout, stderr=subprocess.STDOUT) |
| 611 if retcode: | 618 if retcode: |
| 612 _Die('unexpected error while downloading %s. It may no longer exist on ' | 619 _Die('unexpected error while downloading %s. It may no longer exist on ' |
| 613 'the server or it may not have been uploaded yet (check %s). ' | 620 'the server or it may not have been uploaded yet (check %s). ' |
| 614 'Otherwise, you may not have the correct access permissions.', | 621 'Otherwise, you may not have the correct access permissions.', |
| 615 build.DownloadUrl(archive.rev), _BUILDER_URL) | 622 build.DownloadUrl(archive.rev), build.builder_url) |
| 616 | 623 |
| 617 # Files needed for supersize and resource_sizes. Paths relative to out dir. | 624 # Files needed for supersize and resource_sizes. Paths relative to out dir. |
| 618 to_extract = [build.main_lib_path, build.map_file_path, 'args.gn'] | 625 to_extract = [build.main_lib_path, build.map_file_path, 'args.gn'] |
| 619 if build.IsAndroid(): | 626 if build.IsAndroid(): |
| 620 to_extract += ['build_vars.txt', build.apk_path, build.apk_path + '.size'] | 627 to_extract += ['build_vars.txt', build.apk_path, build.apk_path + '.size'] |
| 621 extract_dir = dl_dst + '_' + 'unzipped' | 628 extract_dir = dl_dst + '_' + 'unzipped' |
| 622 # Storage bucket stores entire output directory including out/Release prefix. | 629 # Storage bucket stores entire output directory including out/Release prefix. |
| 623 logging.info('Extracting build artifacts') | 630 logging.info('Extracting build artifacts') |
| 624 with zipfile.ZipFile(dl_dst, 'r') as z: | 631 with zipfile.ZipFile(dl_dst, 'r') as z: |
| 625 _ExtractFiles(to_extract, build.download_output_dir, extract_dir, z) | 632 _ExtractFiles(to_extract, build.download_output_dir, extract_dir, z) |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 668 shutil.rmtree(tmp_dir) | 675 shutil.rmtree(tmp_dir) |
| 669 | 676 |
| 670 | 677 |
| 671 def _SetRestoreFunc(subrepo): | 678 def _SetRestoreFunc(subrepo): |
| 672 branch = _GitCmd(['rev-parse', '--abbrev-ref', 'HEAD'], subrepo) | 679 branch = _GitCmd(['rev-parse', '--abbrev-ref', 'HEAD'], subrepo) |
| 673 atexit.register(lambda: _GitCmd(['checkout', branch], subrepo)) | 680 atexit.register(lambda: _GitCmd(['checkout', branch], subrepo)) |
| 674 | 681 |
| 675 | 682 |
| 676 def main(): | 683 def main(): |
| 677 parser = argparse.ArgumentParser( | 684 parser = argparse.ArgumentParser( |
| 678 description='Find the cause of APK size bloat.') | 685 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) |
| 679 parser.add_argument('--archive-directory', | 686 parser.add_argument('--archive-directory', |
| 680 default=_DEFAULT_ARCHIVE_DIR, | 687 default=_DEFAULT_ARCHIVE_DIR, |
| 681 help='Where results are stored.') | 688 help='Where results are stored.') |
| 682 parser.add_argument('rev', | 689 parser.add_argument('rev', |
| 683 help='Find binary size bloat for this commit.') | 690 help='Find binary size bloat for this commit.') |
| 684 parser.add_argument('--reference-rev', | 691 parser.add_argument('--reference-rev', |
| 685 help='Older rev to diff against. If not supplied, ' | 692 help='Older rev to diff against. If not supplied, ' |
| 686 'the previous commit to rev will be used.') | 693 'the previous commit to rev will be used.') |
| 687 parser.add_argument('--all', | 694 parser.add_argument('--all', |
| 688 action='store_true', | 695 action='store_true', |
| 689 help='Build/download all revs from --reference-rev to ' | 696 help='Build/download all revs from --reference-rev to ' |
| 690 'rev and diff the contiguous revisions.') | 697 'rev and diff the contiguous revisions.') |
| 691 parser.add_argument('--include-slow-options', | 698 parser.add_argument('--include-slow-options', |
| 692 action='store_true', | 699 action='store_true', |
| 693 help='Run some extra steps that take longer to complete. ' | 700 help='Run some extra steps that take longer to complete. ' |
| 694 'This includes apk-patch-size estimation and ' | 701 'This includes apk-patch-size estimation and ' |
| 695 'static-initializer counting.') | 702 'static-initializer counting.') |
| 696 parser.add_argument('--cloud', | 703 parser.add_argument('--cloud', |
| 697 action='store_true', | 704 action='store_true', |
| 698 help='Download build artifacts from perf builders ' | 705 help='Download build artifacts from perf builders ' |
| 699 '(Android only, Googlers only).') | 706 '(Googlers only).') |
| 700 parser.add_argument('--depot-tools-path', | 707 parser.add_argument('--depot-tools-path', |
| 701 help='Custom path to depot tools. Needed for --cloud if ' | 708 help='Custom path to depot tools. Needed for --cloud if ' |
| 702 'depot tools isn\'t in your PATH.') | 709 'depot tools isn\'t in your PATH.') |
| 703 parser.add_argument('--subrepo', | 710 parser.add_argument('--subrepo', |
| 704 help='Specify a subrepo directory to use. Gclient sync ' | 711 help='Specify a subrepo directory to use. Gclient sync ' |
| 705 'will be skipped if this option is used and all git ' | 712 'will be skipped if this option is used and all git ' |
| 706 'commands will be executed from the subrepo directory. ' | 713 'commands will be executed from the subrepo ' |
| 707 'This option doesn\'t work with --cloud.') | 714 'directory. This option doesn\'t work with --cloud.') |
| 708 parser.add_argument('--silent', | 715 parser.add_argument('-v', |
| 716 '--verbose', |
| 709 action='store_true', | 717 action='store_true', |
| 710 help='Less logging, no Ninja/GN output.') | 718 help='Show commands executed, extra debugging output' |
| 719 ', and Ninja/GN output') |
| 711 | 720 |
| 712 build_group = parser.add_argument_group('ninja', 'Args to use with ninja/gn') | 721 build_group = parser.add_argument_group('ninja arguments') |
| 713 build_group.add_argument('-j', | 722 build_group.add_argument('-j', |
| 714 dest='max_jobs', | 723 dest='max_jobs', |
| 715 help='Run N jobs in parallel.') | 724 help='Run N jobs in parallel.') |
| 716 build_group.add_argument('-l', | 725 build_group.add_argument('-l', |
| 717 dest='max_load_average', | 726 dest='max_load_average', |
| 718 help='Do not start new jobs if the load average is ' | 727 help='Do not start new jobs if the load average is ' |
| 719 'greater than N.') | 728 'greater than N.') |
| 720 build_group.add_argument('--no-goma', | 729 build_group.add_argument('--no-goma', |
| 721 action='store_false', | 730 action='store_false', |
| 722 dest='use_goma', | 731 dest='use_goma', |
| 723 default=True, | 732 default=True, |
| 724 help='Do not use goma when building with ninja.') | 733 help='Do not use goma when building with ninja.') |
| 725 build_group.add_argument('--target-os', | 734 build_group.add_argument('--target-os', |
| 726 default='android', | 735 default='android', |
| 727 choices=['android', 'linux'], | 736 choices=['android', 'linux'], |
| 728 help='target_os gn arg. Default: android.') | 737 help='target_os gn arg. Default: android.') |
| 729 build_group.add_argument('--output-directory', | 738 build_group.add_argument('--output-directory', |
| 730 default=_DEFAULT_OUT_DIR, | 739 default=_DEFAULT_OUT_DIR, |
| 731 help='ninja output directory. ' | 740 help='ninja output directory. ' |
| 732 'Default: %s.' % _DEFAULT_OUT_DIR) | 741 'Default: %s.' % _DEFAULT_OUT_DIR) |
| 733 build_group.add_argument('--enable-chrome-android-internal', | 742 build_group.add_argument('--enable-chrome-android-internal', |
| 734 action='store_true', | 743 action='store_true', |
| 735 help='Allow downstream targets to be built.') | 744 help='Allow downstream targets to be built.') |
| 736 build_group.add_argument('--target', | 745 build_group.add_argument('--target', |
| 737 default=_DEFAULT_ANDROID_TARGET, | 746 default=_DEFAULT_ANDROID_TARGET, |
| 738 help='GN APK target to build. Ignored for Linux. ' | 747 help='GN APK target to build. Ignored for Linux. ' |
| 739 'Default %s.' % _DEFAULT_ANDROID_TARGET) | 748 'Default %s.' % _DEFAULT_ANDROID_TARGET) |
| 740 if len(sys.argv) == 1: | 749 if len(sys.argv) == 1: |
| 741 parser.print_help() | 750 parser.print_help() |
| 742 sys.exit() | 751 sys.exit() |
| 743 args = parser.parse_args() | 752 args = parser.parse_args() |
| 744 log_level = logging.INFO if args.silent else logging.DEBUG | 753 log_level = logging.DEBUG if args.verbose else logging.INFO |
| 745 logging.basicConfig(level=log_level, | 754 logging.basicConfig(level=log_level, |
| 746 format='%(levelname).1s %(relativeCreated)6d %(message)s') | 755 format='%(levelname).1s %(relativeCreated)6d %(message)s') |
| 747 build = _BuildHelper(args) | 756 build = _BuildHelper(args) |
| 748 if build.IsCloud() and args.subrepo: | 757 if build.IsCloud() and args.subrepo: |
| 749 parser.error('--subrepo doesn\'t work with --cloud') | 758 parser.error('--subrepo doesn\'t work with --cloud') |
| 750 | 759 |
| 751 subrepo = args.subrepo or _SRC_ROOT | 760 subrepo = args.subrepo or _SRC_ROOT |
| 752 _EnsureDirectoryClean(subrepo) | 761 _EnsureDirectoryClean(subrepo) |
| 753 _SetRestoreFunc(subrepo) | 762 _SetRestoreFunc(subrepo) |
| 754 if build.IsLinux(): | 763 if build.IsLinux(): |
| 755 _VerifyUserAccepts('Linux diffs have known deficiencies (crbug/717550).') | 764 _VerifyUserAccepts('Linux diffs have known deficiencies (crbug/717550). ') |
| 756 | 765 |
| 757 rev, reference_rev = _ValidateRevs( | 766 rev, reference_rev = _ValidateRevs( |
| 758 args.rev, args.reference_rev or args.rev + '^', subrepo) | 767 args.rev, args.reference_rev or args.rev + '^', subrepo) |
| 759 revs = _GenerateRevList(rev, reference_rev, args.all, subrepo) | 768 revs = _GenerateRevList(rev, reference_rev, args.all, subrepo) |
| 760 with _TmpCopyBinarySizeDir() as supersize_path: | 769 with _TmpCopyBinarySizeDir() as supersize_path: |
| 761 diffs = [NativeDiff(build.size_name, supersize_path)] | 770 diffs = [NativeDiff(build.size_name, supersize_path)] |
| 762 if build.IsAndroid(): | 771 if build.IsAndroid(): |
| 763 diffs += [ | 772 diffs += [ |
| 764 ResourceSizesDiff( | 773 ResourceSizesDiff( |
| 765 build.apk_name, slow_options=args.include_slow_options) | 774 build.apk_name, slow_options=args.include_slow_options) |
| (...skipping 26 matching lines...) Expand all Loading... |
| 792 | 801 |
| 793 if i != 0: | 802 if i != 0: |
| 794 diff_mngr.MaybeDiff(i - 1, i) | 803 diff_mngr.MaybeDiff(i - 1, i) |
| 795 | 804 |
| 796 diff_mngr.Summarize() | 805 diff_mngr.Summarize() |
| 797 | 806 |
| 798 | 807 |
| 799 if __name__ == '__main__': | 808 if __name__ == '__main__': |
| 800 sys.exit(main()) | 809 sys.exit(main()) |
| 801 | 810 |
| OLD | NEW |