| 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 APK bloat. |
| 7 | 7 |
| 8 Run diagnose_apk_bloat.py -h for detailed usage help. | 8 Run diagnose_apk_bloat.py -h for detailed usage help. |
| 9 """ | 9 """ |
| 10 | 10 |
| 11 import argparse | 11 import argparse |
| 12 import collections | 12 import collections |
| 13 from contextlib import contextmanager |
| 13 import distutils.spawn | 14 import distutils.spawn |
| 14 import itertools | |
| 15 import json | 15 import json |
| 16 import multiprocessing | 16 import multiprocessing |
| 17 import os | 17 import os |
| 18 import shutil | 18 import shutil |
| 19 import subprocess | 19 import subprocess |
| 20 import sys | 20 import sys |
| 21 import tempfile | 21 import tempfile |
| 22 import zipfile | 22 import zipfile |
| 23 | 23 |
| 24 _COMMIT_COUNT_WARN_THRESHOLD = 15 | 24 _COMMIT_COUNT_WARN_THRESHOLD = 15 |
| 25 _ALLOWED_CONSECUTIVE_FAILURES = 2 | 25 _ALLOWED_CONSECUTIVE_FAILURES = 2 |
| 26 _BUILDER_URL = \ | 26 _BUILDER_URL = \ |
| 27 'https://build.chromium.org/p/chromium.perf/builders/Android%20Builder' | 27 'https://build.chromium.org/p/chromium.perf/builders/Android%20Builder' |
| 28 _CLOUD_OUT_DIR = os.path.join('out', 'Release') | 28 _CLOUD_OUT_DIR = os.path.join('out', 'Release') |
| 29 _SRC_ROOT = os.path.abspath( | 29 _SRC_ROOT = os.path.abspath( |
| 30 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) | 30 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) |
| 31 _DEFAULT_ARCHIVE_DIR = os.path.join(_SRC_ROOT, 'binary-size-bloat') | 31 _DEFAULT_ARCHIVE_DIR = os.path.join(_SRC_ROOT, 'binary-size-bloat') |
| 32 _DEFAULT_OUT_DIR = os.path.join(_SRC_ROOT, 'out', 'diagnose-apk-bloat') | 32 _DEFAULT_OUT_DIR = os.path.join(_SRC_ROOT, 'out', 'diagnose-apk-bloat') |
| 33 _DEFAULT_TARGET = 'monochrome_public_apk' | 33 _DEFAULT_TARGET = 'monochrome_public_apk' |
| 34 | 34 |
| 35 | 35 |
| 36 _global_restore_checkout_func = None | 36 _global_restore_checkout_func = None |
| 37 | 37 |
| 38 | 38 |
| 39 def _RestoreFunc(subrepo): | 39 def _SetRestoreFunc(subrepo): |
| 40 branch = _GitCmd(['rev-parse', '--abbrev-ref', 'HEAD'], subrepo) | 40 branch = _GitCmd(['rev-parse', '--abbrev-ref', 'HEAD'], subrepo) |
| 41 return lambda: _GitCmd(['checkout', branch], subrepo) | 41 global _global_restore_checkout_func |
| 42 _global_restore_checkout_func = lambda: _GitCmd(['checkout', branch], subrepo) |
| 42 | 43 |
| 43 | 44 |
| 44 class BaseDiff(object): | 45 class BaseDiff(object): |
| 45 """Base class capturing binary size diffs.""" | 46 """Base class capturing binary size diffs.""" |
| 46 def __init__(self, name): | 47 def __init__(self, name): |
| 47 self.name = name | 48 self.name = name |
| 48 self.banner = '\n' + '*' * 30 + name + '*' * 30 | 49 self.banner = '\n' + '*' * 30 + name + '*' * 30 |
| 49 | 50 |
| 50 def AppendResults(self, logfile): | 51 def AppendResults(self, logfile): |
| 51 """Print and write diff results to an open |logfile|.""" | 52 """Print and write diff results to an open |logfile|.""" |
| (...skipping 182 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 234 | 235 |
| 235 class _BuildArchive(object): | 236 class _BuildArchive(object): |
| 236 """Class for managing a directory with build results and build metadata.""" | 237 """Class for managing a directory with build results and build metadata.""" |
| 237 def __init__(self, rev, base_archive_dir, build, subrepo): | 238 def __init__(self, rev, base_archive_dir, build, subrepo): |
| 238 self.build = build | 239 self.build = build |
| 239 self.dir = os.path.join(base_archive_dir, rev) | 240 self.dir = os.path.join(base_archive_dir, rev) |
| 240 metadata_path = os.path.join(self.dir, 'metadata.txt') | 241 metadata_path = os.path.join(self.dir, 'metadata.txt') |
| 241 self.rev = rev | 242 self.rev = rev |
| 242 self.metadata = _GenerateMetadata([self], build, metadata_path, subrepo) | 243 self.metadata = _GenerateMetadata([self], build, metadata_path, subrepo) |
| 243 | 244 |
| 244 def ArchiveBuildResults(self): | 245 def ArchiveBuildResults(self, bs_dir): |
| 245 """Save build artifacts necessary for diffing.""" | 246 """Save build artifacts necessary for diffing.""" |
| 246 _Print('Saving build results to: {}', self.dir) | 247 _Print('Saving build results to: {}', self.dir) |
| 247 _EnsureDirsExist(self.dir) | 248 _EnsureDirsExist(self.dir) |
| 248 build = self.build | 249 build = self.build |
| 249 self._ArchiveFile(build.main_lib_path) | 250 self._ArchiveFile(build.main_lib_path) |
| 250 lib_name_noext = os.path.splitext(os.path.basename(build.main_lib_path))[0] | 251 lib_name_noext = os.path.splitext(os.path.basename(build.main_lib_path))[0] |
| 251 size_path = os.path.join(self.dir, lib_name_noext + '.size') | 252 size_path = os.path.join(self.dir, lib_name_noext + '.size') |
| 252 supersize_path = os.path.join(_SRC_ROOT, 'tools/binary_size/supersize') | 253 supersize_path = os.path.join(bs_dir, 'supersize') |
| 253 tool_prefix = _FindToolPrefix(build.output_directory) | 254 tool_prefix = _FindToolPrefix(build.output_directory) |
| 254 supersize_cmd = [supersize_path, 'archive', size_path, '--elf-file', | 255 supersize_cmd = [supersize_path, 'archive', size_path, '--elf-file', |
| 255 build.main_lib_path, '--tool-prefix', tool_prefix, | 256 build.main_lib_path, '--tool-prefix', tool_prefix, |
| 256 '--output-directory', build.output_directory, | 257 '--output-directory', build.output_directory, |
| 257 '--no-source-paths'] | 258 '--no-source-paths'] |
| 258 if build.IsAndroid(): | 259 if build.IsAndroid(): |
| 259 supersize_cmd += ['--apk-file', build.abs_apk_path] | 260 supersize_cmd += ['--apk-file', build.abs_apk_path] |
| 260 self._ArchiveFile(build.abs_apk_path) | 261 self._ArchiveFile(build.abs_apk_path) |
| 261 | 262 |
| 262 _RunCmd(supersize_cmd) | 263 _RunCmd(supersize_cmd) |
| (...skipping 208 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 471 _Print('Failure: please ensure working directory is clean.') | 472 _Print('Failure: please ensure working directory is clean.') |
| 472 sys.exit() | 473 sys.exit() |
| 473 | 474 |
| 474 | 475 |
| 475 def _Die(s, *args, **kwargs): | 476 def _Die(s, *args, **kwargs): |
| 476 _Print('Failure: ' + s, *args, **kwargs) | 477 _Print('Failure: ' + s, *args, **kwargs) |
| 477 _global_restore_checkout_func() | 478 _global_restore_checkout_func() |
| 478 sys.exit(1) | 479 sys.exit(1) |
| 479 | 480 |
| 480 | 481 |
| 481 def _DownloadBuildArtifacts(archive, build, depot_tools_path=None): | 482 def _DownloadBuildArtifacts(archive, build, bs_dir, depot_tools_path): |
| 482 """Download artifacts from arm32 chromium perf builder.""" | 483 """Download artifacts from arm32 chromium perf builder.""" |
| 483 if depot_tools_path: | 484 if depot_tools_path: |
| 484 gsutil_path = os.path.join(depot_tools_path, 'gsutil.py') | 485 gsutil_path = os.path.join(depot_tools_path, 'gsutil.py') |
| 485 else: | 486 else: |
| 486 gsutil_path = distutils.spawn.find_executable('gsutil.py') | 487 gsutil_path = distutils.spawn.find_executable('gsutil.py') |
| 487 | 488 |
| 488 if not gsutil_path: | 489 if not gsutil_path: |
| 489 _Die('gsutil.py not found, please provide path to depot_tools via ' | 490 _Die('gsutil.py not found, please provide path to depot_tools via ' |
| 490 '--depot-tools-path or add it to your PATH') | 491 '--depot-tools-path or add it to your PATH') |
| 491 | 492 |
| 492 download_dir = tempfile.mkdtemp(dir=_SRC_ROOT) | 493 download_dir = tempfile.mkdtemp(dir=_SRC_ROOT) |
| 493 try: | 494 try: |
| 494 _DownloadAndArchive(gsutil_path, archive, download_dir, build) | 495 _DownloadAndArchive(gsutil_path, archive, download_dir, build, bs_dir) |
| 495 finally: | 496 finally: |
| 496 shutil.rmtree(download_dir) | 497 shutil.rmtree(download_dir) |
| 497 | 498 |
| 498 | 499 |
| 499 def _DownloadAndArchive(gsutil_path, archive, dl_dir, build): | 500 def _DownloadAndArchive(gsutil_path, archive, dl_dir, build, bs_dir): |
| 500 dl_file = 'full-build-linux_%s.zip' % archive.rev | 501 dl_file = 'full-build-linux_%s.zip' % archive.rev |
| 501 dl_url = 'gs://chrome-perf/Android Builder/%s' % dl_file | 502 dl_url = 'gs://chrome-perf/Android Builder/%s' % dl_file |
| 502 dl_dst = os.path.join(dl_dir, dl_file) | 503 dl_dst = os.path.join(dl_dir, dl_file) |
| 503 _Print('Downloading build artifacts for {}', archive.rev) | 504 _Print('Downloading build artifacts for {}', archive.rev) |
| 504 # gsutil writes stdout and stderr to stderr, so pipe stdout and stderr to | 505 # gsutil writes stdout and stderr to stderr, so pipe stdout and stderr to |
| 505 # sys.stdout. | 506 # sys.stdout. |
| 506 retcode = subprocess.call([gsutil_path, 'cp', dl_url, dl_dir], | 507 retcode = subprocess.call([gsutil_path, 'cp', dl_url, dl_dir], |
| 507 stdout=sys.stdout, stderr=subprocess.STDOUT) | 508 stdout=sys.stdout, stderr=subprocess.STDOUT) |
| 508 if retcode: | 509 if retcode: |
| 509 _Die('unexpected error while downloading {}. It may no longer exist on ' | 510 _Die('unexpected error while downloading {}. It may no longer exist on ' |
| 510 'the server or it may not have been uploaded yet (check {}). ' | 511 'the server or it may not have been uploaded yet (check {}). ' |
| 511 'Otherwise, you may not have the correct access permissions.', | 512 'Otherwise, you may not have the correct access permissions.', |
| 512 dl_url, _BUILDER_URL) | 513 dl_url, _BUILDER_URL) |
| 513 | 514 |
| 514 # Files needed for supersize and resource_sizes. Paths relative to out dir. | 515 # Files needed for supersize and resource_sizes. Paths relative to out dir. |
| 515 to_extract = [build.main_lib_name, build.map_file_name, 'args.gn', | 516 to_extract = [build.main_lib_name, build.map_file_name, 'args.gn', |
| 516 'build_vars.txt', build.apk_path] | 517 'build_vars.txt', build.apk_path] |
| 517 extract_dir = os.path.join(os.path.splitext(dl_dst)[0], 'unzipped') | 518 extract_dir = os.path.join(os.path.splitext(dl_dst)[0], 'unzipped') |
| 518 # Storage bucket stores entire output directory including out/Release prefix. | 519 # Storage bucket stores entire output directory including out/Release prefix. |
| 519 _Print('Extracting build artifacts') | 520 _Print('Extracting build artifacts') |
| 520 with zipfile.ZipFile(dl_dst, 'r') as z: | 521 with zipfile.ZipFile(dl_dst, 'r') as z: |
| 521 _ExtractFiles(to_extract, _CLOUD_OUT_DIR, extract_dir, z) | 522 _ExtractFiles(to_extract, _CLOUD_OUT_DIR, extract_dir, z) |
| 522 dl_out = os.path.join(extract_dir, _CLOUD_OUT_DIR) | 523 dl_out = os.path.join(extract_dir, _CLOUD_OUT_DIR) |
| 523 build.output_directory, output_directory = dl_out, build.output_directory | 524 build.output_directory, output_directory = dl_out, build.output_directory |
| 524 archive.ArchiveBuildResults() | 525 archive.ArchiveBuildResults(bs_dir) |
| 525 build.output_directory = output_directory | 526 build.output_directory = output_directory |
| 526 | 527 |
| 527 | 528 |
| 528 def _ExtractFiles(to_extract, prefix, dst, z): | 529 def _ExtractFiles(to_extract, prefix, dst, z): |
| 529 zip_infos = z.infolist() | 530 zip_infos = z.infolist() |
| 530 assert all(info.filename.startswith(prefix) for info in zip_infos), ( | 531 assert all(info.filename.startswith(prefix) for info in zip_infos), ( |
| 531 'Storage bucket folder structure doesn\'t start with %s' % prefix) | 532 'Storage bucket folder structure doesn\'t start with %s' % prefix) |
| 532 to_extract = [os.path.join(prefix, f) for f in to_extract] | 533 to_extract = [os.path.join(prefix, f) for f in to_extract] |
| 533 for f in to_extract: | 534 for f in to_extract: |
| 534 z.extract(f, path=dst) | 535 z.extract(f, path=dst) |
| 535 | 536 |
| 536 | 537 |
| 537 def _Print(s, *args, **kwargs): | 538 def _Print(s, *args, **kwargs): |
| 538 print s.format(*args, **kwargs) | 539 print s.format(*args, **kwargs) |
| 539 | 540 |
| 540 | 541 |
| 541 def _PrintAndWriteToFile(logfile, s): | 542 def _PrintAndWriteToFile(logfile, s): |
| 542 """Print |s| to |logfile| and stdout.""" | 543 """Print |s| to |logfile| and stdout.""" |
| 543 _Print(s) | 544 _Print(s) |
| 544 logfile.write('%s\n' % s) | 545 logfile.write('%s\n' % s) |
| 545 | 546 |
| 546 | 547 |
| 548 @contextmanager |
| 549 def _TmpBinarySizeDir(): |
| 550 """Recursively copy files to a temp dir and yield the tmp binary_size dir.""" |
| 551 # Needs to be at same level of nesting as the real //tools/binary_size |
| 552 # since supersize uses this to find d3 in //third_party. |
| 553 tmp_dir = tempfile.mkdtemp(dir=_SRC_ROOT) |
| 554 try: |
| 555 bs_dir = os.path.join(tmp_dir, 'binary_size') |
| 556 shutil.copytree(os.path.join(_SRC_ROOT, 'tools', 'binary_size'), bs_dir) |
| 557 yield bs_dir |
| 558 finally: |
| 559 shutil.rmtree(tmp_dir) |
| 560 |
| 561 |
| 547 def main(): | 562 def main(): |
| 548 parser = argparse.ArgumentParser( | 563 parser = argparse.ArgumentParser( |
| 549 description='Find the cause of APK size bloat.') | 564 description='Find the cause of APK size bloat.') |
| 550 parser.add_argument('--archive-dir', | 565 parser.add_argument('--archive-directory', |
| 551 default=_DEFAULT_ARCHIVE_DIR, | 566 default=_DEFAULT_ARCHIVE_DIR, |
| 552 help='Where results are stored.') | 567 help='Where results are stored.') |
| 553 parser.add_argument('rev', | 568 parser.add_argument('rev', |
| 554 help='Find binary size bloat for this commit.') | 569 help='Find binary size bloat for this commit.') |
| 555 parser.add_argument('--reference-rev', | 570 parser.add_argument('--reference-rev', |
| 556 help='Older rev to diff against. If not supplied, ' | 571 help='Older rev to diff against. If not supplied, ' |
| 557 'the previous commit to rev will be used.') | 572 'the previous commit to rev will be used.') |
| 558 parser.add_argument('--all', | 573 parser.add_argument('--all', |
| 559 action='store_true', | 574 action='store_true', |
| 560 help='Build/download all revs from --reference-rev to ' | 575 help='Build/download all revs from --reference-rev to ' |
| (...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 611 args = parser.parse_args() | 626 args = parser.parse_args() |
| 612 build = _BuildHelper(args) | 627 build = _BuildHelper(args) |
| 613 if build.IsCloud(): | 628 if build.IsCloud(): |
| 614 if build.IsLinux(): | 629 if build.IsLinux(): |
| 615 parser.error('--cloud only works for android') | 630 parser.error('--cloud only works for android') |
| 616 if args.subrepo: | 631 if args.subrepo: |
| 617 parser.error('--subrepo doesn\'t work with --cloud') | 632 parser.error('--subrepo doesn\'t work with --cloud') |
| 618 | 633 |
| 619 subrepo = args.subrepo or _SRC_ROOT | 634 subrepo = args.subrepo or _SRC_ROOT |
| 620 _EnsureDirectoryClean(subrepo) | 635 _EnsureDirectoryClean(subrepo) |
| 621 _global_restore_checkout_func = _RestoreFunc(subrepo) | 636 _SetRestoreFunc(subrepo) |
| 622 revs = _GenerateRevList(args.rev, | 637 revs = _GenerateRevList(args.rev, |
| 623 args.reference_rev or args.rev + '^', | 638 args.reference_rev or args.rev + '^', |
| 624 args.all, | 639 args.all, |
| 625 subrepo) | 640 subrepo) |
| 626 diffs = [] | 641 diffs = [] |
| 627 if build.IsAndroid(): | 642 if build.IsAndroid(): |
| 628 diffs += [ | 643 diffs += [ |
| 629 ResourceSizesDiff( | 644 ResourceSizesDiff( |
| 630 build.apk_name, slow_options=args.include_slow_options) | 645 build.apk_name, slow_options=args.include_slow_options) |
| 631 ] | 646 ] |
| 632 diff_mngr = _DiffArchiveManager(revs, args.archive_dir, diffs, build, subrepo) | 647 diff_mngr = _DiffArchiveManager( |
| 648 revs, args.archive_directory, diffs, build, subrepo) |
| 633 consecutive_failures = 0 | 649 consecutive_failures = 0 |
| 634 for i, archive in enumerate(diff_mngr.IterArchives()): | 650 with _TmpBinarySizeDir() as bs_dir: |
| 635 if archive.Exists(): | 651 for i, archive in enumerate(diff_mngr.IterArchives()): |
| 636 _Print('Found matching metadata for {}, skipping build step.', | 652 if archive.Exists(): |
| 637 archive.rev) | 653 _Print('Found matching metadata for {}, skipping build step.', |
| 638 else: | 654 archive.rev) |
| 639 if build.IsCloud(): | |
| 640 _DownloadBuildArtifacts(archive, build, | |
| 641 depot_tools_path=args.depot_tools_path) | |
| 642 else: | 655 else: |
| 643 build_success = _SyncAndBuild(archive, build, subrepo) | 656 if build.IsCloud(): |
| 644 if not build_success: | 657 _DownloadBuildArtifacts(archive, build, bs_dir, args.depot_tools_path) |
| 645 consecutive_failures += 1 | |
| 646 if consecutive_failures > _ALLOWED_CONSECUTIVE_FAILURES: | |
| 647 _Die('{} builds failed in a row, last failure was {}.', | |
| 648 consecutive_failures, archive.rev) | |
| 649 else: | 658 else: |
| 650 archive.ArchiveBuildResults() | 659 build_success = _SyncAndBuild(archive, build, subrepo) |
| 651 consecutive_failures = 0 | 660 if not build_success: |
| 661 consecutive_failures += 1 |
| 662 if consecutive_failures > _ALLOWED_CONSECUTIVE_FAILURES: |
| 663 _Die('{} builds failed in a row, last failure was {}.', |
| 664 consecutive_failures, archive.rev) |
| 665 else: |
| 666 archive.ArchiveBuildResults(bs_dir) |
| 667 consecutive_failures = 0 |
| 652 | 668 |
| 653 if i != 0: | 669 if i != 0: |
| 654 diff_mngr.MaybeDiff(i - 1, i) | 670 diff_mngr.MaybeDiff(i - 1, i) |
| 655 | 671 |
| 656 _global_restore_checkout_func() | 672 _global_restore_checkout_func() |
| 657 | 673 |
| 658 if __name__ == '__main__': | 674 if __name__ == '__main__': |
| 659 sys.exit(main()) | 675 sys.exit(main()) |
| 660 | 676 |
| OLD | NEW |