Index: tools/binary_size/diagnose_apk_bloat.py |
diff --git a/tools/binary_size/diagnose_apk_bloat.py b/tools/binary_size/diagnose_apk_bloat.py |
deleted file mode 100755 |
index 8a1d0ee9fc5216ee0ad70f3e522e8f2fb80e9fc4..0000000000000000000000000000000000000000 |
--- a/tools/binary_size/diagnose_apk_bloat.py |
+++ /dev/null |
@@ -1,779 +0,0 @@ |
-#!/usr/bin/env python |
-# Copyright 2017 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
- |
-"""Tool for finding the cause of APK bloat. |
- |
-Run diagnose_apk_bloat.py -h for detailed usage help. |
-""" |
- |
-import argparse |
-import collections |
-from contextlib import contextmanager |
-import distutils.spawn |
-import json |
-import multiprocessing |
-import os |
-import re |
-import shutil |
-import subprocess |
-import sys |
-import tempfile |
-import zipfile |
- |
-_COMMIT_COUNT_WARN_THRESHOLD = 15 |
-_ALLOWED_CONSECUTIVE_FAILURES = 2 |
-_DIFF_DETAILS_LINES_THRESHOLD = 100 |
-_BUILDER_URL = \ |
- 'https://build.chromium.org/p/chromium.perf/builders/Android%20Builder' |
-_SRC_ROOT = os.path.abspath( |
- os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) |
-_DEFAULT_ARCHIVE_DIR = os.path.join(_SRC_ROOT, 'binary-size-bloat') |
-_DEFAULT_OUT_DIR = os.path.join(_SRC_ROOT, 'out', 'diagnose-apk-bloat') |
-_DEFAULT_ANDROID_TARGET = 'monochrome_public_apk' |
- |
-_global_restore_checkout_func = None |
- |
- |
-def _SetRestoreFunc(subrepo): |
- branch = _GitCmd(['rev-parse', '--abbrev-ref', 'HEAD'], subrepo) |
- global _global_restore_checkout_func |
- _global_restore_checkout_func = lambda: _GitCmd(['checkout', branch], subrepo) |
- |
- |
-_DiffResult = collections.namedtuple('DiffResult', ['name', 'value', 'units']) |
- |
- |
-class BaseDiff(object): |
- """Base class capturing binary size diffs.""" |
- def __init__(self, name): |
- self.name = name |
- self.banner = '\n' + '*' * 30 + name + '*' * 30 |
- |
- def AppendResults(self, logfile): |
- """Print and write diff results to an open |logfile|.""" |
- _PrintAndWriteToFile(logfile, self.banner) |
- _PrintAndWriteToFile(logfile, 'Summary:') |
- _PrintAndWriteToFile(logfile, self.Summary()) |
- _PrintAndWriteToFile(logfile, '\nDetails:') |
- _PrintAndWriteToFile(logfile, self.DetailedResults()) |
- |
- @property |
- def summary_stat(self): |
- return None |
- |
- def Summary(self): |
- """A short description that summarizes the source of binary size bloat.""" |
- raise NotImplementedError() |
- |
- def DetailedResults(self): |
- """An iterable description of the cause of binary size bloat.""" |
- raise NotImplementedError() |
- |
- def ProduceDiff(self, before_dir, after_dir): |
- """Prepare a binary size diff with ready to print results.""" |
- raise NotImplementedError() |
- |
- def RunDiff(self, logfile, before_dir, after_dir): |
- self.ProduceDiff(before_dir, after_dir) |
- self.AppendResults(logfile) |
- |
- |
-class NativeDiff(BaseDiff): |
- _RE_SUMMARY = re.compile( |
- r'.*(Section Sizes .*? object files added, \d+ removed).*', |
- flags=re.DOTALL) |
- _RE_SUMMARY_STAT = re.compile( |
- r'Section Sizes \(Total=(?P<value>\d+) (?P<units>\w+)\)') |
- _SUMMARY_STAT_NAME = 'Native Library Delta' |
- |
- def __init__(self, size_name, supersize_path): |
- self._size_name = size_name |
- self._supersize_path = supersize_path |
- self._diff = [] |
- super(NativeDiff, self).__init__('Native Diff') |
- |
- @property |
- def summary_stat(self): |
- m = NativeDiff._RE_SUMMARY_STAT.search(self._diff) |
- if m: |
- return _DiffResult( |
- NativeDiff._SUMMARY_STAT_NAME, m.group('value'), m.group('units')) |
- return None |
- |
- def DetailedResults(self): |
- return self._diff.splitlines() |
- |
- def Summary(self): |
- return NativeDiff._RE_SUMMARY.match(self._diff).group(1) |
- |
- def ProduceDiff(self, before_dir, after_dir): |
- before_size = os.path.join(before_dir, self._size_name) |
- after_size = os.path.join(after_dir, self._size_name) |
- cmd = [self._supersize_path, 'diff', before_size, after_size] |
- self._diff = _RunCmd(cmd)[0].replace('{', '{{').replace('}', '}}') |
- |
- |
-class ResourceSizesDiff(BaseDiff): |
- _RESOURCE_SIZES_PATH = os.path.join( |
- _SRC_ROOT, 'build', 'android', 'resource_sizes.py') |
- |
- def __init__(self, apk_name, slow_options=False): |
- self._apk_name = apk_name |
- self._slow_options = slow_options |
- self._diff = None # Set by |ProduceDiff()| |
- super(ResourceSizesDiff, self).__init__('Resource Sizes Diff') |
- |
- @property |
- def summary_stat(self): |
- for s in self._diff: |
- if 'normalized' in s.name: |
- return s |
- return None |
- |
- def DetailedResults(self): |
- return ['{:>+10,} {} {}'.format(value, units, name) |
- for name, value, units in self._diff] |
- |
- def Summary(self): |
- return 'Normalized APK size: {:+,} {}'.format( |
- self.summary_stat.value, self.summary_stat.units) |
- |
- def ProduceDiff(self, before_dir, after_dir): |
- before = self._RunResourceSizes(before_dir) |
- after = self._RunResourceSizes(after_dir) |
- diff = [] |
- for section, section_dict in after.iteritems(): |
- for subsection, v in section_dict.iteritems(): |
- # Ignore entries when resource_sizes.py chartjson format has changed. |
- if (section not in before or |
- subsection not in before[section] or |
- v['units'] != before[section][subsection]['units']): |
- _Print('Found differing dict structures for resource_sizes.py, ' |
- 'skipping {} {}', section, subsection) |
- else: |
- diff.append( |
- _DiffResult( |
- '%s %s' % (section, subsection), |
- v['value'] - before[section][subsection]['value'], |
- v['units'])) |
- self._diff = sorted(diff, key=lambda x: abs(x.value), reverse=True) |
- |
- def _RunResourceSizes(self, archive_dir): |
- apk_path = os.path.join(archive_dir, self._apk_name) |
- chartjson_file = os.path.join(archive_dir, 'results-chart.json') |
- cmd = [self._RESOURCE_SIZES_PATH, apk_path,'--output-dir', archive_dir, |
- '--no-output-dir', '--chartjson'] |
- if self._slow_options: |
- cmd += ['--estimate-patch-size'] |
- else: |
- cmd += ['--no-static-initializer-check'] |
- _RunCmd(cmd) |
- with open(chartjson_file) as f: |
- chartjson = json.load(f) |
- return chartjson['charts'] |
- |
- |
-class _BuildHelper(object): |
- """Helper class for generating and building targets.""" |
- def __init__(self, args): |
- self.cloud = args.cloud |
- self.enable_chrome_android_internal = args.enable_chrome_android_internal |
- self.extra_gn_args_str = '' |
- self.max_jobs = args.max_jobs |
- self.max_load_average = args.max_load_average |
- self.output_directory = args.output_directory |
- self.target = args.target |
- self.target_os = args.target_os |
- self.use_goma = args.use_goma |
- self._SetDefaults() |
- |
- @property |
- def abs_apk_path(self): |
- return os.path.join(self.output_directory, self.apk_path) |
- |
- @property |
- def apk_name(self): |
- # Only works on apk targets that follow: my_great_apk naming convention. |
- apk_name = ''.join(s.title() for s in self.target.split('_')[:-1]) + '.apk' |
- return apk_name.replace('Webview', 'WebView') |
- |
- @property |
- def apk_path(self): |
- return os.path.join('apks', self.apk_name) |
- |
- @property |
- def main_lib_path(self): |
- # TODO(estevenson): Get this from GN instead of hardcoding. |
- if self.IsLinux(): |
- return 'chrome' |
- elif 'monochrome' in self.target: |
- return 'lib.unstripped/libmonochrome.so' |
- else: |
- return 'lib.unstripped/libchrome.so' |
- |
- @property |
- def abs_main_lib_path(self): |
- return os.path.join(self.output_directory, self.main_lib_path) |
- |
- @property |
- def download_bucket(self): |
- return 'gs://chrome-perf/%s Builder/' % self.target_os.title() |
- |
- @property |
- def download_output_dir(self): |
- return 'out/Release' if self.IsAndroid() else 'full-build-linux' |
- |
- @property |
- def map_file_path(self): |
- return self.main_lib_path + '.map.gz' |
- |
- @property |
- def size_name(self): |
- return os.path.splitext(os.path.basename(self.main_lib_path))[0] + '.size' |
- |
- def _SetDefaults(self): |
- has_goma_dir = os.path.exists(os.path.join(os.path.expanduser('~'), 'goma')) |
- self.use_goma = self.use_goma or has_goma_dir |
- self.max_load_average = (self.max_load_average or |
- str(multiprocessing.cpu_count())) |
- if not self.max_jobs: |
- self.max_jobs = '10000' if self.use_goma else '500' |
- |
- if os.path.exists(os.path.join(os.path.dirname(_SRC_ROOT), 'src-internal')): |
- self.extra_gn_args_str = ' is_chrome_branded=true' |
- else: |
- self.extra_gn_args_str = (' exclude_unwind_tables=true ' |
- 'ffmpeg_branding="Chrome" proprietary_codecs=true') |
- self.target = self.target if self.IsAndroid() else 'chrome' |
- |
- def _GenGnCmd(self): |
- gn_args = 'is_official_build=true symbol_level=1' |
- gn_args += ' use_goma=%s' % str(self.use_goma).lower() |
- gn_args += ' target_os="%s"' % self.target_os |
- gn_args += (' enable_chrome_android_internal=%s' % |
- str(self.enable_chrome_android_internal).lower()) |
- gn_args += self.extra_gn_args_str |
- return ['gn', 'gen', self.output_directory, '--args=%s' % gn_args] |
- |
- def _GenNinjaCmd(self): |
- cmd = ['ninja', '-C', self.output_directory] |
- cmd += ['-j', self.max_jobs] if self.max_jobs else [] |
- cmd += ['-l', self.max_load_average] if self.max_load_average else [] |
- cmd += [self.target] |
- return cmd |
- |
- def Run(self): |
- """Run GN gen/ninja build and return the process returncode.""" |
- _Print('Building: {}.', self.target) |
- retcode = _RunCmd( |
- self._GenGnCmd(), print_stdout=True, exit_on_failure=False)[1] |
- if retcode: |
- return retcode |
- return _RunCmd( |
- self._GenNinjaCmd(), print_stdout=True, exit_on_failure=False)[1] |
- |
- def DownloadUrl(self, rev): |
- return self.download_bucket + 'full-build-linux_%s.zip' % rev |
- |
- def IsAndroid(self): |
- return self.target_os == 'android' |
- |
- def IsLinux(self): |
- return self.target_os == 'linux' |
- |
- def IsCloud(self): |
- return self.cloud |
- |
- |
-class _BuildArchive(object): |
- """Class for managing a directory with build results and build metadata.""" |
- def __init__(self, rev, base_archive_dir, build, subrepo): |
- self.build = build |
- self.dir = os.path.join(base_archive_dir, rev) |
- metadata_path = os.path.join(self.dir, 'metadata.txt') |
- self.rev = rev |
- self.metadata = _GenerateMetadata([self], build, metadata_path, subrepo) |
- |
- def ArchiveBuildResults(self, supersize_path): |
- """Save build artifacts necessary for diffing.""" |
- _Print('Saving build results to: {}', self.dir) |
- _EnsureDirsExist(self.dir) |
- build = self.build |
- self._ArchiveFile(build.abs_main_lib_path) |
- tool_prefix = _FindToolPrefix(build.output_directory) |
- size_path = os.path.join(self.dir, build.size_name) |
- supersize_cmd = [supersize_path, 'archive', size_path, '--elf-file', |
- build.abs_main_lib_path, '--tool-prefix', tool_prefix, |
- '--output-directory', build.output_directory, |
- '--no-source-paths'] |
- if build.IsAndroid(): |
- supersize_cmd += ['--apk-file', build.abs_apk_path] |
- self._ArchiveFile(build.abs_apk_path) |
- |
- _RunCmd(supersize_cmd) |
- _WriteMetadata(self.metadata) |
- |
- def Exists(self): |
- return _MetadataExists(self.metadata) |
- |
- def _ArchiveFile(self, filename): |
- if not os.path.exists(filename): |
- _Die('missing expected file: {}', filename) |
- shutil.copy(filename, self.dir) |
- |
- |
-class _DiffArchiveManager(object): |
- """Class for maintaining BuildArchives and their related diff artifacts.""" |
- def __init__(self, revs, archive_dir, diffs, build, subrepo): |
- self.archive_dir = archive_dir |
- self.build = build |
- self.build_archives = [_BuildArchive(rev, archive_dir, build, subrepo) |
- for rev in revs] |
- self.diffs = diffs |
- self.subrepo = subrepo |
- self._summary_stats = [] |
- |
- def IterArchives(self): |
- return iter(self.build_archives) |
- |
- def MaybeDiff(self, before_id, after_id): |
- """Perform diffs given two build archives.""" |
- before = self.build_archives[before_id] |
- after = self.build_archives[after_id] |
- diff_path = self._DiffFilePath(before, after) |
- if not self._CanDiff(before, after): |
- _Print('Skipping diff for {} due to missing build archives.', diff_path) |
- return |
- |
- metadata_path = self._DiffMetadataPath(before, after) |
- metadata = _GenerateMetadata( |
- [before, after], self.build, metadata_path, self.subrepo) |
- if _MetadataExists(metadata): |
- _Print('Skipping diff for {} and {}. Matching diff already exists: {}', |
- before.rev, after.rev, diff_path) |
- else: |
- if os.path.exists(diff_path): |
- os.remove(diff_path) |
- with open(diff_path, 'a') as diff_file: |
- for d in self.diffs: |
- d.RunDiff(diff_file, before.dir, after.dir) |
- _Print('\nSee detailed diff results here: {}.', diff_path) |
- _WriteMetadata(metadata) |
- self._AddDiffSummaryStat(before, after) |
- |
- def Summarize(self): |
- if self._summary_stats: |
- path = os.path.join(self.archive_dir, 'last_diff_summary.txt') |
- with open(path, 'w') as f: |
- stats = sorted( |
- self._summary_stats, key=lambda x: x[0].value, reverse=True) |
- _PrintAndWriteToFile(f, '\nDiff Summary') |
- for s, before, after in stats: |
- _PrintAndWriteToFile(f, '{:>+10} {} {} for range: {}..{}', |
- s.value, s.units, s.name, before, after) |
- |
- def _AddDiffSummaryStat(self, before, after): |
- stat = None |
- if self.build.IsAndroid(): |
- summary_diff_type = ResourceSizesDiff |
- else: |
- summary_diff_type = NativeDiff |
- for d in self.diffs: |
- if isinstance(d, summary_diff_type): |
- stat = d.summary_stat |
- if stat: |
- self._summary_stats.append((stat, before.rev, after.rev)) |
- |
- def _CanDiff(self, before, after): |
- return before.Exists() and after.Exists() |
- |
- def _DiffFilePath(self, before, after): |
- return os.path.join(self._DiffDir(before, after), 'diff_results.txt') |
- |
- def _DiffMetadataPath(self, before, after): |
- return os.path.join(self._DiffDir(before, after), 'metadata.txt') |
- |
- def _DiffDir(self, before, after): |
- archive_range = '%s..%s' % (before.rev, after.rev) |
- diff_path = os.path.join(self.archive_dir, 'diffs', archive_range) |
- _EnsureDirsExist(diff_path) |
- return diff_path |
- |
- |
-def _EnsureDirsExist(path): |
- if not os.path.exists(path): |
- os.makedirs(path) |
- |
- |
-def _GenerateMetadata(archives, build, path, subrepo): |
- return { |
- 'revs': [a.rev for a in archives], |
- 'archive_dirs': [a.dir for a in archives], |
- 'target': build.target, |
- 'target_os': build.target_os, |
- 'is_cloud': build.IsCloud(), |
- 'subrepo': subrepo, |
- 'path': path, |
- 'gn_args': { |
- 'extra_gn_args_str': build.extra_gn_args_str, |
- 'enable_chrome_android_internal': build.enable_chrome_android_internal, |
- } |
- } |
- |
- |
-def _WriteMetadata(metadata): |
- with open(metadata['path'], 'w') as f: |
- json.dump(metadata, f) |
- |
- |
-def _MetadataExists(metadata): |
- old_metadata = {} |
- path = metadata['path'] |
- if os.path.exists(path): |
- with open(path, 'r') as f: |
- old_metadata = json.load(f) |
- ret = len(metadata) == len(old_metadata) |
- ret &= all(v == old_metadata[k] |
- for k, v in metadata.items() if k != 'gn_args') |
- # GN args don't matter when artifacts are downloaded. For local builds |
- # they need to be the same so that diffs are accurate (differing GN args |
- # will change the final APK/native library). |
- if not metadata['is_cloud']: |
- ret &= metadata['gn_args'] == old_metadata['gn_args'] |
- return ret |
- return False |
- |
- |
-def _RunCmd(cmd, print_stdout=False, exit_on_failure=True): |
- """Convenience function for running commands. |
- |
- Args: |
- cmd: the command to run. |
- print_stdout: if this is True, then the stdout of the process will be |
- printed instead of returned. |
- exit_on_failure: die if an error occurs when this is True. |
- |
- Returns: |
- Tuple of (process stdout, process returncode). |
- """ |
- cmd_str = ' '.join(c for c in cmd) |
- _Print('Running: {}', cmd_str) |
- proc_stdout = sys.stdout if print_stdout else subprocess.PIPE |
- |
- proc = subprocess.Popen(cmd, stdout=proc_stdout, stderr=subprocess.PIPE) |
- stdout, stderr = proc.communicate() |
- |
- if proc.returncode and exit_on_failure: |
- _Die('command failed: {}\nstderr:\n{}', cmd_str, stderr) |
- |
- stdout = stdout.strip() if stdout else '' |
- return stdout, proc.returncode |
- |
- |
-def _GitCmd(args, subrepo): |
- return _RunCmd(['git', '-C', subrepo] + args)[0] |
- |
- |
-def _GclientSyncCmd(rev, subrepo): |
- cwd = os.getcwd() |
- os.chdir(subrepo) |
- _RunCmd(['gclient', 'sync', '-r', 'src@' + rev], print_stdout=True) |
- os.chdir(cwd) |
- |
- |
-def _FindToolPrefix(output_directory): |
- build_vars_path = os.path.join(output_directory, 'build_vars.txt') |
- if os.path.exists(build_vars_path): |
- with open(build_vars_path) as f: |
- build_vars = dict(l.rstrip().split('=', 1) for l in f if '=' in l) |
- # Tool prefix is relative to output dir, rebase to source root. |
- tool_prefix = build_vars['android_tool_prefix'] |
- while os.path.sep in tool_prefix: |
- rebased_tool_prefix = os.path.join(_SRC_ROOT, tool_prefix) |
- if os.path.exists(rebased_tool_prefix + 'readelf'): |
- return rebased_tool_prefix |
- tool_prefix = tool_prefix[tool_prefix.find(os.path.sep) + 1:] |
- return '' |
- |
- |
-def _SyncAndBuild(archive, build, subrepo): |
- # Simply do a checkout if subrepo is used. |
- if subrepo != _SRC_ROOT: |
- _GitCmd(['checkout', archive.rev], subrepo) |
- else: |
- # Move to a detached state since gclient sync doesn't work with local |
- # commits on a branch. |
- _GitCmd(['checkout', '--detach'], subrepo) |
- _GclientSyncCmd(archive.rev, subrepo) |
- retcode = build.Run() |
- return retcode == 0 |
- |
- |
-def _GenerateRevList(rev, reference_rev, all_in_range, subrepo): |
- """Normalize and optionally generate a list of commits in the given range. |
- |
- Returns: |
- A list of revisions ordered from oldest to newest. |
- """ |
- rev_seq = '%s^..%s' % (reference_rev, rev) |
- stdout = _GitCmd(['rev-list', rev_seq], subrepo) |
- all_revs = stdout.splitlines()[::-1] |
- if all_in_range: |
- revs = all_revs |
- else: |
- revs = [all_revs[0], all_revs[-1]] |
- if len(revs) >= _COMMIT_COUNT_WARN_THRESHOLD: |
- _VerifyUserAccepts( |
- 'You\'ve provided a commit range that contains %d commits' % len(revs)) |
- return revs |
- |
- |
-def _ValidateRevs(rev, reference_rev, subrepo): |
- def git_fatal(args, message): |
- devnull = open(os.devnull, 'wb') |
- retcode = subprocess.call( |
- ['git', '-C', subrepo] + args, stdout=devnull, stderr=subprocess.STDOUT) |
- if retcode: |
- _Die(message) |
- |
- if rev == reference_rev: |
- _Die('rev and reference-rev cannot be equal') |
- no_obj_message = ('%s either doesn\'t exist or your local repo is out of ' |
- 'date, try "git fetch origin master"') |
- git_fatal(['cat-file', '-e', rev], no_obj_message % rev) |
- git_fatal(['cat-file', '-e', reference_rev], no_obj_message % reference_rev) |
- git_fatal(['merge-base', '--is-ancestor', reference_rev, rev], |
- 'reference-rev is newer than rev') |
- return rev, reference_rev |
- |
- |
-def _VerifyUserAccepts(message): |
- _Print(message + 'Do you want to proceed? [y/n]') |
- if raw_input('> ').lower() != 'y': |
- _global_restore_checkout_func() |
- sys.exit() |
- |
- |
-def _EnsureDirectoryClean(subrepo): |
- _Print('Checking source directory') |
- stdout = _GitCmd(['status', '--porcelain'], subrepo) |
- # Ignore untracked files. |
- if stdout and stdout[:2] != '??': |
- _Print('Failure: please ensure working directory is clean.') |
- sys.exit() |
- |
- |
-def _Die(s, *args, **kwargs): |
- _Print('Failure: ' + s, *args, **kwargs) |
- _global_restore_checkout_func() |
- sys.exit(1) |
- |
- |
-def _DownloadBuildArtifacts(archive, build, supersize_path, depot_tools_path): |
- """Download artifacts from arm32 chromium perf builder.""" |
- if depot_tools_path: |
- gsutil_path = os.path.join(depot_tools_path, 'gsutil.py') |
- else: |
- gsutil_path = distutils.spawn.find_executable('gsutil.py') |
- |
- if not gsutil_path: |
- _Die('gsutil.py not found, please provide path to depot_tools via ' |
- '--depot-tools-path or add it to your PATH') |
- |
- download_dir = tempfile.mkdtemp(dir=_SRC_ROOT) |
- try: |
- _DownloadAndArchive( |
- gsutil_path, archive, download_dir, build, supersize_path) |
- finally: |
- shutil.rmtree(download_dir) |
- |
- |
-def _DownloadAndArchive(gsutil_path, archive, dl_dir, build, supersize_path): |
- dl_dst = os.path.join(dl_dir, archive.rev) |
- _Print('Downloading build artifacts for {}', archive.rev) |
- # gsutil writes stdout and stderr to stderr, so pipe stdout and stderr to |
- # sys.stdout. |
- retcode = subprocess.call( |
- [gsutil_path, 'cp', build.DownloadUrl(archive.rev), dl_dst], |
- stdout=sys.stdout, stderr=subprocess.STDOUT) |
- if retcode: |
- _Die('unexpected error while downloading {}. It may no longer exist on ' |
- 'the server or it may not have been uploaded yet (check {}). ' |
- 'Otherwise, you may not have the correct access permissions.', |
- build.DownloadUrl(archive.rev), _BUILDER_URL) |
- |
- # Files needed for supersize and resource_sizes. Paths relative to out dir. |
- to_extract = [build.main_lib_path, build.map_file_path, 'args.gn'] |
- if build.IsAndroid(): |
- to_extract += ['build_vars.txt', build.apk_path] |
- extract_dir = dl_dst + '_' + 'unzipped' |
- # Storage bucket stores entire output directory including out/Release prefix. |
- _Print('Extracting build artifacts') |
- with zipfile.ZipFile(dl_dst, 'r') as z: |
- _ExtractFiles(to_extract, build.download_output_dir, extract_dir, z) |
- dl_out = os.path.join(extract_dir, build.download_output_dir) |
- build.output_directory, output_directory = dl_out, build.output_directory |
- archive.ArchiveBuildResults(supersize_path) |
- build.output_directory = output_directory |
- |
- |
-def _ExtractFiles(to_extract, prefix, dst, z): |
- zip_infos = z.infolist() |
- assert all(info.filename.startswith(prefix) for info in zip_infos), ( |
- 'Storage bucket folder structure doesn\'t start with %s' % prefix) |
- to_extract = [os.path.join(prefix, f) for f in to_extract] |
- for f in to_extract: |
- z.extract(f, path=dst) |
- |
- |
-def _Print(s, *args, **kwargs): |
- print s.format(*args, **kwargs) |
- |
- |
-def _PrintAndWriteToFile(logfile, s, *args, **kwargs): |
- """Write and print |s| thottling output if |s| is a large list.""" |
- if isinstance(s, basestring): |
- s = s.format(*args, **kwargs) |
- _Print(s) |
- logfile.write('%s\n' % s) |
- else: |
- for l in s[:_DIFF_DETAILS_LINES_THRESHOLD]: |
- _Print(l) |
- if len(s) > _DIFF_DETAILS_LINES_THRESHOLD: |
- _Print('\nOutput truncated, see {} for more.', logfile.name) |
- logfile.write('\n'.join(s)) |
- |
- |
-@contextmanager |
-def _TmpCopyBinarySizeDir(): |
- """Recursively copy files to a temp dir and yield supersize path.""" |
- # Needs to be at same level of nesting as the real //tools/binary_size |
- # since supersize uses this to find d3 in //third_party. |
- tmp_dir = tempfile.mkdtemp(dir=_SRC_ROOT) |
- try: |
- bs_dir = os.path.join(tmp_dir, 'binary_size') |
- shutil.copytree(os.path.join(_SRC_ROOT, 'tools', 'binary_size'), bs_dir) |
- yield os.path.join(bs_dir, 'supersize') |
- finally: |
- shutil.rmtree(tmp_dir) |
- |
- |
-def main(): |
- parser = argparse.ArgumentParser( |
- description='Find the cause of APK size bloat.') |
- parser.add_argument('--archive-directory', |
- default=_DEFAULT_ARCHIVE_DIR, |
- help='Where results are stored.') |
- parser.add_argument('rev', |
- help='Find binary size bloat for this commit.') |
- parser.add_argument('--reference-rev', |
- help='Older rev to diff against. If not supplied, ' |
- 'the previous commit to rev will be used.') |
- parser.add_argument('--all', |
- action='store_true', |
- help='Build/download all revs from --reference-rev to ' |
- 'rev and diff the contiguous revisions.') |
- parser.add_argument('--include-slow-options', |
- action='store_true', |
- help='Run some extra steps that take longer to complete. ' |
- 'This includes apk-patch-size estimation and ' |
- 'static-initializer counting.') |
- parser.add_argument('--cloud', |
- action='store_true', |
- help='Download build artifacts from perf builders ' |
- '(Android only, Googlers only).') |
- parser.add_argument('--depot-tools-path', |
- help='Custom path to depot tools. Needed for --cloud if ' |
- 'depot tools isn\'t in your PATH.') |
- parser.add_argument('--subrepo', |
- help='Specify a subrepo directory to use. Gclient sync ' |
- 'will be skipped if this option is used and all git ' |
- 'commands will be executed from the subrepo directory. ' |
- 'This option doesn\'t work with --cloud.') |
- |
- build_group = parser.add_argument_group('ninja', 'Args to use with ninja/gn') |
- build_group.add_argument('-j', |
- dest='max_jobs', |
- help='Run N jobs in parallel.') |
- build_group.add_argument('-l', |
- dest='max_load_average', |
- help='Do not start new jobs if the load average is ' |
- 'greater than N.') |
- build_group.add_argument('--no-goma', |
- action='store_false', |
- dest='use_goma', |
- default=True, |
- help='Do not use goma when building with ninja.') |
- build_group.add_argument('--target-os', |
- default='android', |
- choices=['android', 'linux'], |
- help='target_os gn arg. Default: android.') |
- build_group.add_argument('--output-directory', |
- default=_DEFAULT_OUT_DIR, |
- help='ninja output directory. ' |
- 'Default: %s.' % _DEFAULT_OUT_DIR) |
- build_group.add_argument('--enable-chrome-android-internal', |
- action='store_true', |
- help='Allow downstream targets to be built.') |
- build_group.add_argument('--target', |
- default=_DEFAULT_ANDROID_TARGET, |
- help='GN APK target to build. Ignored for Linux. ' |
- 'Default %s.' % _DEFAULT_ANDROID_TARGET) |
- if len(sys.argv) == 1: |
- parser.print_help() |
- sys.exit() |
- args = parser.parse_args() |
- build = _BuildHelper(args) |
- if build.IsCloud() and args.subrepo: |
- parser.error('--subrepo doesn\'t work with --cloud') |
- |
- subrepo = args.subrepo or _SRC_ROOT |
- _EnsureDirectoryClean(subrepo) |
- _SetRestoreFunc(subrepo) |
- if build.IsLinux(): |
- _VerifyUserAccepts('Linux diffs have known deficiencies (crbug/717550).') |
- |
- rev, reference_rev = _ValidateRevs( |
- args.rev, args.reference_rev or args.rev + '^', subrepo) |
- revs = _GenerateRevList(rev, reference_rev, args.all, subrepo) |
- with _TmpCopyBinarySizeDir() as supersize_path: |
- diffs = [NativeDiff(build.size_name, supersize_path)] |
- if build.IsAndroid(): |
- diffs += [ |
- ResourceSizesDiff( |
- build.apk_name, slow_options=args.include_slow_options) |
- ] |
- diff_mngr = _DiffArchiveManager( |
- revs, args.archive_directory, diffs, build, subrepo) |
- consecutive_failures = 0 |
- for i, archive in enumerate(diff_mngr.IterArchives()): |
- if archive.Exists(): |
- _Print('Found matching metadata for {}, skipping build step.', |
- archive.rev) |
- else: |
- if build.IsCloud(): |
- _DownloadBuildArtifacts( |
- archive, build, supersize_path, args.depot_tools_path) |
- else: |
- build_success = _SyncAndBuild(archive, build, subrepo) |
- if not build_success: |
- consecutive_failures += 1 |
- if consecutive_failures > _ALLOWED_CONSECUTIVE_FAILURES: |
- _Die('{} builds failed in a row, last failure was {}.', |
- consecutive_failures, archive.rev) |
- else: |
- archive.ArchiveBuildResults(supersize_path) |
- consecutive_failures = 0 |
- |
- if i != 0: |
- diff_mngr.MaybeDiff(i - 1, i) |
- |
- diff_mngr.Summarize() |
- |
- _global_restore_checkout_func() |
- |
-if __name__ == '__main__': |
- sys.exit(main()) |
- |