Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(9)

Unified Diff: tools/binary_size/diagnose_apk_bloat.py

Issue 2834103002: diagnose_apk_bloat.py: handle more rev options. (Closed)
Patch Set: Remove unused field Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
index 510ee1314f59a615cdb331962a5b17018e4c995a..23eacdbdabc23e0d41d32ed10275cd5a0717d261 100755
--- a/tools/binary_size/diagnose_apk_bloat.py
+++ b/tools/binary_size/diagnose_apk_bloat.py
@@ -21,6 +21,8 @@ import sys
import tempfile
import zipfile
+_ALLOWED_CONSECUTIVE_BUILDS = 15
agrieve 2017/04/21 16:57:31 Limit seems a bit small. I've seen rolls go in tha
estevenson 2017/04/21 20:15:49 Done.
+_ALLOWED_CONSECUTIVE_FAILURES = 2
_BUILDER_URL = \
'https://build.chromium.org/p/chromium.perf/builders/Android%20Builder'
_CLOUD_OUT_DIR = os.path.join('out', 'Release')
@@ -34,13 +36,15 @@ _DEFAULT_TARGET = 'monochrome_public_apk'
# so that it doesn't need to be passed everywhere in case we fail and exit.
_initial_branch = None
+# Global variable for storing the subrepo directory.
+_subrepo = None
agrieve 2017/04/21 16:57:31 Having the global makes the code a bit harder to f
+
class BaseDiff(object):
"""Base class capturing binary size diffs."""
def __init__(self, name):
self.name = name
self.banner = '\n' + '*' * 30 + name + '*' * 30
- self.RunDiff()
def AppendResults(self, logfile):
"""Print and write diff results to an open |logfile|."""
@@ -59,13 +63,14 @@ class BaseDiff(object):
"""An iterable description of the cause of binary size bloat."""
raise NotImplementedError()
- def ProduceDiff(self):
+ def ProduceDiff(self, archive_dirs):
"""Prepare a binary size diff with ready to print results."""
raise NotImplementedError()
- def RunDiff(self):
+ def RunDiff(self, logfile, archive_dirs):
_Print('Creating {}', self.name)
- self.ProduceDiff()
+ self.ProduceDiff(archive_dirs)
+ self.AppendResults(logfile)
_ResourceSizesDiffResult = collections.namedtuple(
@@ -76,8 +81,7 @@ class ResourceSizesDiff(BaseDiff):
_RESOURCE_SIZES_PATH = os.path.join(
_SRC_ROOT, 'build', 'android', 'resource_sizes.py')
- def __init__(self, archive_dirs, apk_name, slow_options=False):
- self._archive_dirs = archive_dirs
+ def __init__(self, apk_name, slow_options=False):
self._apk_name = apk_name
self._slow_options = slow_options
self._diff = None # Set by |ProduceDiff()|
@@ -93,8 +97,8 @@ class ResourceSizesDiff(BaseDiff):
return 'Normalized APK size: {:+,} {}'.format(s.value, s.units)
return ''
- def ProduceDiff(self):
- chartjsons = self._RunResourceSizes()
+ def ProduceDiff(self, archive_dirs):
+ chartjsons = self._RunResourceSizes(archive_dirs)
diff = []
with_patch = chartjsons[0]['charts']
without_patch = chartjsons[1]['charts']
@@ -114,9 +118,9 @@ class ResourceSizesDiff(BaseDiff):
v['units']))
self._diff = sorted(diff, key=lambda x: abs(x.value), reverse=True)
- def _RunResourceSizes(self):
+ def _RunResourceSizes(self, archive_dirs):
chartjsons = []
- for archive_dir in self._archive_dirs:
+ for archive_dir in archive_dirs:
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,
@@ -134,6 +138,7 @@ class ResourceSizesDiff(BaseDiff):
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
@@ -207,9 +212,14 @@ class _BuildHelper(object):
return cmd
def Run(self):
+ """Run GN gen/ninja build and return the process returncode."""
_Print('Building: {}.', self.target)
- _RunCmd(self._GenGnCmd(), print_stdout=True)
- _RunCmd(self._GenNinjaCmd(), print_stdout=True)
+ 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 IsAndroid(self):
return self.target_os == 'android'
@@ -217,36 +227,168 @@ class _BuildHelper(object):
def IsLinux(self):
return self.target_os == 'linux'
-
-def _RunCmd(cmd, print_stdout=False):
+ 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):
+ self.build = build
+ self.dir = os.path.join(base_archive_dir, rev)
+ _EnsureDirsExist(self.dir)
agrieve 2017/04/21 16:57:31 nit: Generally best to make constructors as light-
estevenson 2017/04/21 20:15:49 Done.
+ metadata_path = os.path.join(self.dir, 'metadata.txt')
+ self.rev = rev
+ self.metadata = _GenerateMetadata([self], build, metadata_path)
+
+ def ArchiveBuildResults(self):
+ """Save build artifacts necessary for diffing."""
+ _Print('Saving build results to: {}', self.dir)
+ build = self.build
+ self._ArchiveFile(build.main_lib_path)
+ lib_name_noext = os.path.splitext(os.path.basename(build.main_lib_path))[0]
+ size_path = os.path.join(self.dir, lib_name_noext + '.size')
+ supersize_path = os.path.join(_SRC_ROOT, 'tools/binary_size/supersize')
+ tool_prefix = _FindToolPrefix(build.output_directory)
+ supersize_cmd = [supersize_path, 'archive', size_path, '--elf-file',
+ build.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):
+ self.archive_dir = archive_dir
+ _EnsureDirsExist(archive_dir)
+ self.build = build
+ self.build_archives = [_BuildArchive(rev, archive_dir, build)
+ for rev in revs]
+ self.diffs = diffs
+
+ def IterArchives(self):
+ return iter(self.build_archives)
+
+ def MaybeDiff(self, first_id, second_id):
+ """Perform diffs given two build archives."""
+ archives = [
+ self.build_archives[first_id], self.build_archives[second_id]]
+ diff_path = self._DiffFilePath(archives)
+ if not self._CanDiff(archives):
+ _Print('Skipping diff for {} due to missing build archives.', diff_path)
+ return
+
+ metadata_path = self._DiffMetadataPath(archives)
+ metadata = _GenerateMetadata(archives, self.build, metadata_path)
+ if _MetadataExists(metadata):
+ _Print('Skipping diff for {} and {}. Matching diff already exists: {}',
+ archives[0].rev, archives[1].rev, diff_path)
+ else:
+ archive_dirs = [archives[0].dir, archives[1].dir]
+ with open(diff_path, 'a') as diff_file:
+ for d in self.diffs:
+ d.RunDiff(diff_file, archive_dirs)
+ _WriteMetadata(metadata)
+
+ def _CanDiff(self, archives):
+ return all(a.Exists() for a in archives)
+
+ def _DiffFilePath(self, archives):
+ return os.path.join(self._DiffDir(archives), 'diff_results.txt')
+
+ def _DiffMetadataPath(self, archives):
+ return os.path.join(self._DiffDir(archives), 'metadata.txt')
+
+ def _DiffDir(self, archives):
+ diff_path = os.path.join(
+ self.archive_dir, 'diffs', '_'.join(a.rev for a in archives))
+ _EnsureDirsExist(diff_path)
+ return diff_path
+
+
+def _EnsureDirsExist(path):
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+
+def _GenerateMetadata(archives, build, path):
+ 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')
+ if not metadata['is_cloud']:
agrieve 2017/04/21 16:57:31 Can you add a comment saying why this check is nec
estevenson 2017/04/21 20:15:49 Done.
+ 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, otherwise stdout will be returned.
+ printed instead of returned.
+ exit_on_failure: die if an error occurs when this is True.
Returns:
- Command stdout if |print_stdout| is False otherwise ''.
+ Tuple of (process stdout, process returncode).
"""
cmd_str = ' '.join(c for c in cmd)
_Print('Running: {}', cmd_str)
- if print_stdout:
- proc_stdout = sys.stdout
- else:
- proc_stdout = subprocess.PIPE
+ 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:
+ if proc.returncode and exit_on_failure:
_Die('command failed: {}\nstderr:\n{}', cmd_str, stderr)
- return stdout.strip() if stdout else ''
+ stdout = stdout.strip() if stdout else ''
+ return stdout, proc.returncode
-def _GitCmd(args):
- return _RunCmd(['git', '-C', _SRC_ROOT] + args)
+def _GitCmd(args, retcode=False):
agrieve 2017/04/21 16:57:31 I don't see retcode being used anywhere. You also
estevenson 2017/04/21 20:15:49 Oops, meant to use it in _GenerateRevList. Changed
+ return _RunCmd(['git', '-C', _subrepo] + args)[int(retcode)]
def _GclientSyncCmd(rev):
@@ -256,36 +398,6 @@ def _GclientSyncCmd(rev):
os.chdir(cwd)
-def _ArchiveBuildResult(archive_dir, build):
- """Save build artifacts necessary for diffing.
-
- Expects |build.output_directory| to be correct.
- """
- _Print('Saving build results to: {}', archive_dir)
- if not os.path.exists(archive_dir):
- os.makedirs(archive_dir)
-
- def ArchiveFile(filename):
- if not os.path.exists(filename):
- _Die('missing expected file: {}', filename)
- shutil.copy(filename, archive_dir)
-
- ArchiveFile(build.main_lib_path)
- lib_name_noext = os.path.splitext(os.path.basename(build.main_lib_path))[0]
- size_path = os.path.join(archive_dir, lib_name_noext + '.size')
- supersize_path = os.path.join(_SRC_ROOT, 'tools/binary_size/supersize')
- tool_prefix = _FindToolPrefix(build.output_directory)
- supersize_cmd = [supersize_path, 'archive', size_path, '--elf-file',
- build.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]
- ArchiveFile(build.abs_apk_path)
-
- _RunCmd(supersize_cmd)
-
-
def _FindToolPrefix(output_directory):
build_vars_path = os.path.join(output_directory, 'build_vars.txt')
if os.path.exists(build_vars_path):
@@ -301,19 +413,40 @@ def _FindToolPrefix(output_directory):
return ''
-def _SyncAndBuild(revs, archive_dirs, build):
- # Move to a detached state since gclient sync doesn't work with local commits
- # on a branch.
- _GitCmd(['checkout', '--detach'])
- for rev, archive_dir in itertools.izip(revs, archive_dirs):
- _GclientSyncCmd(rev)
- build.Run()
- _ArchiveBuildResult(archive_dir, build)
+def _SyncAndBuild(archive, build, use_subrepo):
+ if use_subrepo:
+ _GitCmd(['checkout', archive.rev])
+ else:
+ # Move to a detached state since gclient sync doesn't work with local
+ # commits on a branch.
+ _GitCmd(['checkout', '--detach'])
+ _GclientSyncCmd(archive.rev)
+ retcode = build.Run()
+ return retcode == 0
+
+def _GenerateRevList(with_patch, without_patch, all_in_range):
+ """Normalize and optionally generate a list of commits in the given range.
+
+ Returns a list of revisions ordered from newest to oldest.
+ """
+ retcode = _GitCmd(['merge-base', '--is-ancestor', without_patch, with_patch])
+ assert not retcode and with_patch != without_patch, (
+ 'Invalid revision arguments, rev_without_patch (%s) is newer than '
+ 'rev_with_patch (%s)' % (without_patch, with_patch))
+
+ rev_seq = '%s^..%s' % (without_patch, with_patch)
+ stdout = _GitCmd(['rev-list', rev_seq])
+ all_revs = stdout.splitlines()
+ if all_in_range:
+ revs = all_revs
+ else:
+ revs = [all_revs[0], all_revs[-1]]
-def _NormalizeRev(rev):
- """Use actual revs instead of HEAD, HEAD^, etc."""
- return _GitCmd(['rev-parse', rev])
+ assert len(revs) <= _ALLOWED_CONSECUTIVE_BUILDS, (
+ 'Too many commits in range %s..%s, allowed: %d, found %d' % (
+ without_patch, with_patch, _ALLOWED_CONSECUTIVE_BUILDS, len(revs)))
+ return revs
def _EnsureDirectoryClean():
@@ -334,13 +467,18 @@ def _RestoreInitialBranch():
_GitCmd(['checkout', _initial_branch])
+def _SetSubrepo(subrepo):
+ global _subrepo
+ _subrepo = subrepo or _SRC_ROOT
+
+
def _Die(s, *args, **kwargs):
_Print('Failure: ' + s, *args, **kwargs)
_RestoreInitialBranch()
sys.exit(1)
-def _DownloadBuildArtifacts(revs, archive_dirs, build, depot_tools_path=None):
+def _DownloadBuildArtifacts(archive, build, depot_tools_path=None):
"""Download artifacts from arm32 chromium perf builder."""
if depot_tools_path:
gsutil_path = os.path.join(depot_tools_path, 'gsutil.py')
@@ -353,17 +491,16 @@ def _DownloadBuildArtifacts(revs, archive_dirs, build, depot_tools_path=None):
download_dir = tempfile.mkdtemp(dir=_SRC_ROOT)
try:
- for rev, archive_dir in itertools.izip(revs, archive_dirs):
- _DownloadAndArchive(gsutil_path, rev, archive_dir, download_dir, build)
+ _DownloadAndArchive(gsutil_path, archive, download_dir, build)
finally:
shutil.rmtree(download_dir)
-def _DownloadAndArchive(gsutil_path, rev, archive_dir, dl_dir, build):
- dl_file = 'full-build-linux_%s.zip' % rev
+def _DownloadAndArchive(gsutil_path, archive, dl_dir, build):
+ dl_file = 'full-build-linux_%s.zip' % archive.rev
dl_url = 'gs://chrome-perf/Android Builder/%s' % dl_file
dl_dst = os.path.join(dl_dir, dl_file)
- _Print('Downloading build artifacts for {}', 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', dl_url, dl_dir],
@@ -384,7 +521,7 @@ def _DownloadAndArchive(gsutil_path, rev, archive_dir, dl_dir, build):
_ExtractFiles(to_extract, _CLOUD_OUT_DIR, extract_dir, z)
dl_out = os.path.join(extract_dir, _CLOUD_OUT_DIR)
build.output_directory, output_directory = dl_out, build.output_directory
- _ArchiveBuildResult(archive_dir, build)
+ archive.ArchiveBuildResults()
build.output_directory = output_directory
@@ -420,6 +557,10 @@ def main():
parser.add_argument('--rev-without-patch',
help='Older patch to diff against. If not supplied, '
'the previous commit to rev_with_patch will be used.')
+ parser.add_argument('--all',
+ action='store_true',
+ help='Build all revs from rev_with_patch to '
agrieve 2017/04/21 16:57:31 I've been finding the flags a bit unintuitive, I t
estevenson 2017/04/21 20:15:49 Spoke offline a bit about this, I've change it so
+ 'rev_without_patch and diff contiguous revisions.')
parser.add_argument('--include-slow-options',
action='store_true',
help='Run some extra steps that take longer to complete. '
@@ -432,6 +573,11 @@ def main():
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',
@@ -453,7 +599,7 @@ def main():
build_group.add_argument('--output-directory',
default=_DEFAULT_OUT_DIR,
help='ninja output directory.')
- build_group.add_argument('--enable_chrome_android_internal',
+ build_group.add_argument('--enable-chrome-android-internal',
action='store_true',
help='Allow downstream targets to be built.')
build_group.add_argument('--target',
@@ -461,37 +607,50 @@ def main():
help='GN APK target to build.')
args = parser.parse_args()
build = _BuildHelper(args)
- if args.cloud and build.IsLinux():
- parser.error('--cloud only works for android')
+ _SetSubrepo(args.subrepo)
+ use_subrepo = bool(args.subrepo)
+ if build.IsCloud():
+ if build.IsLinux():
+ parser.error('--cloud only works for android')
+ if use_subrepo:
+ parser.error('--subrepo doesn\'t work with --cloud')
_EnsureDirectoryClean()
_SetInitialBranch()
- revs = [args.rev_with_patch,
- args.rev_without_patch or args.rev_with_patch + '^']
- revs = [_NormalizeRev(r) for r in revs]
- archive_dirs = [os.path.join(args.archive_dir, '%d-%s' % (len(revs) - i, rev))
- for i, rev in enumerate(revs)]
- if args.cloud:
- _DownloadBuildArtifacts(revs, archive_dirs, build,
- depot_tools_path=args.depot_tools_path)
- else:
- _SetInitialBranch()
- _SyncAndBuild(revs, archive_dirs, build)
- _RestoreInitialBranch()
-
- output_file = os.path.join(args.archive_dir,
- 'diff_result_{}_{}.txt'.format(*revs))
- if os.path.exists(output_file):
- os.remove(output_file)
+ revs = _GenerateRevList(args.rev_with_patch,
+ args.rev_without_patch or args.rev_with_patch + '^',
+ args.all)
diffs = []
if build.IsAndroid():
diffs += [
- ResourceSizesDiff(archive_dirs, build.apk_name,
- slow_options=args.include_slow_options)
+ ResourceSizesDiff(
+ build.apk_name, slow_options=args.include_slow_options)
]
- with open(output_file, 'a') as logfile:
- for d in diffs:
- d.AppendResults(logfile)
+ diff_mngr = _DiffArchiveManager(revs, args.archive_dir, diffs, build)
+ 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,
+ depot_tools_path=args.depot_tools_path)
+ else:
+ build_success = _SyncAndBuild(archive, build, use_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()
+ consecutive_failures = 0
+
+ if i != 0:
+ diff_mngr.MaybeDiff(i - 1, i)
+
+ _RestoreInitialBranch()
if __name__ == '__main__':
sys.exit(main())
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698