Chromium Code Reviews| 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 import distutils.spawn | 13 import distutils.spawn |
| 14 import itertools | 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 _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.
| |
| 25 _ALLOWED_CONSECUTIVE_FAILURES = 2 | |
| 24 _BUILDER_URL = \ | 26 _BUILDER_URL = \ |
| 25 'https://build.chromium.org/p/chromium.perf/builders/Android%20Builder' | 27 'https://build.chromium.org/p/chromium.perf/builders/Android%20Builder' |
| 26 _CLOUD_OUT_DIR = os.path.join('out', 'Release') | 28 _CLOUD_OUT_DIR = os.path.join('out', 'Release') |
| 27 _SRC_ROOT = os.path.abspath( | 29 _SRC_ROOT = os.path.abspath( |
| 28 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) | 30 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) |
| 29 _DEFAULT_ARCHIVE_DIR = os.path.join(_SRC_ROOT, 'binary-size-bloat') | 31 _DEFAULT_ARCHIVE_DIR = os.path.join(_SRC_ROOT, 'binary-size-bloat') |
| 30 _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') |
| 31 _DEFAULT_TARGET = 'monochrome_public_apk' | 33 _DEFAULT_TARGET = 'monochrome_public_apk' |
| 32 | 34 |
| 33 # Global variable for storing the initial branch before the script was launched | 35 # Global variable for storing the initial branch before the script was launched |
| 34 # so that it doesn't need to be passed everywhere in case we fail and exit. | 36 # so that it doesn't need to be passed everywhere in case we fail and exit. |
| 35 _initial_branch = None | 37 _initial_branch = None |
| 36 | 38 |
| 39 # Global variable for storing the subrepo directory. | |
| 40 _subrepo = None | |
|
agrieve
2017/04/21 16:57:31
Having the global makes the code a bit harder to f
| |
| 41 | |
| 37 | 42 |
| 38 class BaseDiff(object): | 43 class BaseDiff(object): |
| 39 """Base class capturing binary size diffs.""" | 44 """Base class capturing binary size diffs.""" |
| 40 def __init__(self, name): | 45 def __init__(self, name): |
| 41 self.name = name | 46 self.name = name |
| 42 self.banner = '\n' + '*' * 30 + name + '*' * 30 | 47 self.banner = '\n' + '*' * 30 + name + '*' * 30 |
| 43 self.RunDiff() | |
| 44 | 48 |
| 45 def AppendResults(self, logfile): | 49 def AppendResults(self, logfile): |
| 46 """Print and write diff results to an open |logfile|.""" | 50 """Print and write diff results to an open |logfile|.""" |
| 47 _PrintAndWriteToFile(logfile, self.banner) | 51 _PrintAndWriteToFile(logfile, self.banner) |
| 48 _PrintAndWriteToFile(logfile, 'Summary:') | 52 _PrintAndWriteToFile(logfile, 'Summary:') |
| 49 _PrintAndWriteToFile(logfile, self.Summary()) | 53 _PrintAndWriteToFile(logfile, self.Summary()) |
| 50 _PrintAndWriteToFile(logfile, '\nDetails:') | 54 _PrintAndWriteToFile(logfile, '\nDetails:') |
| 51 for l in self.DetailedResults(): | 55 for l in self.DetailedResults(): |
| 52 _PrintAndWriteToFile(logfile, l) | 56 _PrintAndWriteToFile(logfile, l) |
| 53 | 57 |
| 54 def Summary(self): | 58 def Summary(self): |
| 55 """A short description that summarizes the source of binary size bloat.""" | 59 """A short description that summarizes the source of binary size bloat.""" |
| 56 raise NotImplementedError() | 60 raise NotImplementedError() |
| 57 | 61 |
| 58 def DetailedResults(self): | 62 def DetailedResults(self): |
| 59 """An iterable description of the cause of binary size bloat.""" | 63 """An iterable description of the cause of binary size bloat.""" |
| 60 raise NotImplementedError() | 64 raise NotImplementedError() |
| 61 | 65 |
| 62 def ProduceDiff(self): | 66 def ProduceDiff(self, archive_dirs): |
| 63 """Prepare a binary size diff with ready to print results.""" | 67 """Prepare a binary size diff with ready to print results.""" |
| 64 raise NotImplementedError() | 68 raise NotImplementedError() |
| 65 | 69 |
| 66 def RunDiff(self): | 70 def RunDiff(self, logfile, archive_dirs): |
| 67 _Print('Creating {}', self.name) | 71 _Print('Creating {}', self.name) |
| 68 self.ProduceDiff() | 72 self.ProduceDiff(archive_dirs) |
| 73 self.AppendResults(logfile) | |
| 69 | 74 |
| 70 | 75 |
| 71 _ResourceSizesDiffResult = collections.namedtuple( | 76 _ResourceSizesDiffResult = collections.namedtuple( |
| 72 'ResourceSizesDiffResult', ['section', 'value', 'units']) | 77 'ResourceSizesDiffResult', ['section', 'value', 'units']) |
| 73 | 78 |
| 74 | 79 |
| 75 class ResourceSizesDiff(BaseDiff): | 80 class ResourceSizesDiff(BaseDiff): |
| 76 _RESOURCE_SIZES_PATH = os.path.join( | 81 _RESOURCE_SIZES_PATH = os.path.join( |
| 77 _SRC_ROOT, 'build', 'android', 'resource_sizes.py') | 82 _SRC_ROOT, 'build', 'android', 'resource_sizes.py') |
| 78 | 83 |
| 79 def __init__(self, archive_dirs, apk_name, slow_options=False): | 84 def __init__(self, apk_name, slow_options=False): |
| 80 self._archive_dirs = archive_dirs | |
| 81 self._apk_name = apk_name | 85 self._apk_name = apk_name |
| 82 self._slow_options = slow_options | 86 self._slow_options = slow_options |
| 83 self._diff = None # Set by |ProduceDiff()| | 87 self._diff = None # Set by |ProduceDiff()| |
| 84 super(ResourceSizesDiff, self).__init__('Resource Sizes Diff') | 88 super(ResourceSizesDiff, self).__init__('Resource Sizes Diff') |
| 85 | 89 |
| 86 def DetailedResults(self): | 90 def DetailedResults(self): |
| 87 for section, value, units in self._diff: | 91 for section, value, units in self._diff: |
| 88 yield '{:>+10,} {} {}'.format(value, units, section) | 92 yield '{:>+10,} {} {}'.format(value, units, section) |
| 89 | 93 |
| 90 def Summary(self): | 94 def Summary(self): |
| 91 for s in self._diff: | 95 for s in self._diff: |
| 92 if 'normalized' in s.section: | 96 if 'normalized' in s.section: |
| 93 return 'Normalized APK size: {:+,} {}'.format(s.value, s.units) | 97 return 'Normalized APK size: {:+,} {}'.format(s.value, s.units) |
| 94 return '' | 98 return '' |
| 95 | 99 |
| 96 def ProduceDiff(self): | 100 def ProduceDiff(self, archive_dirs): |
| 97 chartjsons = self._RunResourceSizes() | 101 chartjsons = self._RunResourceSizes(archive_dirs) |
| 98 diff = [] | 102 diff = [] |
| 99 with_patch = chartjsons[0]['charts'] | 103 with_patch = chartjsons[0]['charts'] |
| 100 without_patch = chartjsons[1]['charts'] | 104 without_patch = chartjsons[1]['charts'] |
| 101 for section, section_dict in with_patch.iteritems(): | 105 for section, section_dict in with_patch.iteritems(): |
| 102 for subsection, v in section_dict.iteritems(): | 106 for subsection, v in section_dict.iteritems(): |
| 103 # Ignore entries when resource_sizes.py chartjson format has changed. | 107 # Ignore entries when resource_sizes.py chartjson format has changed. |
| 104 if (section not in without_patch or | 108 if (section not in without_patch or |
| 105 subsection not in without_patch[section] or | 109 subsection not in without_patch[section] or |
| 106 v['units'] != without_patch[section][subsection]['units']): | 110 v['units'] != without_patch[section][subsection]['units']): |
| 107 _Print('Found differing dict structures for resource_sizes.py, ' | 111 _Print('Found differing dict structures for resource_sizes.py, ' |
| 108 'skipping {} {}', section, subsection) | 112 'skipping {} {}', section, subsection) |
| 109 else: | 113 else: |
| 110 diff.append( | 114 diff.append( |
| 111 _ResourceSizesDiffResult( | 115 _ResourceSizesDiffResult( |
| 112 '%s %s' % (section, subsection), | 116 '%s %s' % (section, subsection), |
| 113 v['value'] - without_patch[section][subsection]['value'], | 117 v['value'] - without_patch[section][subsection]['value'], |
| 114 v['units'])) | 118 v['units'])) |
| 115 self._diff = sorted(diff, key=lambda x: abs(x.value), reverse=True) | 119 self._diff = sorted(diff, key=lambda x: abs(x.value), reverse=True) |
| 116 | 120 |
| 117 def _RunResourceSizes(self): | 121 def _RunResourceSizes(self, archive_dirs): |
| 118 chartjsons = [] | 122 chartjsons = [] |
| 119 for archive_dir in self._archive_dirs: | 123 for archive_dir in archive_dirs: |
| 120 apk_path = os.path.join(archive_dir, self._apk_name) | 124 apk_path = os.path.join(archive_dir, self._apk_name) |
| 121 chartjson_file = os.path.join(archive_dir, 'results-chart.json') | 125 chartjson_file = os.path.join(archive_dir, 'results-chart.json') |
| 122 cmd = [self._RESOURCE_SIZES_PATH, apk_path,'--output-dir', archive_dir, | 126 cmd = [self._RESOURCE_SIZES_PATH, apk_path,'--output-dir', archive_dir, |
| 123 '--no-output-dir', '--chartjson'] | 127 '--no-output-dir', '--chartjson'] |
| 124 if self._slow_options: | 128 if self._slow_options: |
| 125 cmd += ['--estimate-patch-size'] | 129 cmd += ['--estimate-patch-size'] |
| 126 else: | 130 else: |
| 127 cmd += ['--no-static-initializer-check'] | 131 cmd += ['--no-static-initializer-check'] |
| 128 _RunCmd(cmd) | 132 _RunCmd(cmd) |
| 129 with open(chartjson_file) as f: | 133 with open(chartjson_file) as f: |
| 130 chartjsons.append(json.load(f)) | 134 chartjsons.append(json.load(f)) |
| 131 return chartjsons | 135 return chartjsons |
| 132 | 136 |
| 133 | 137 |
| 134 class _BuildHelper(object): | 138 class _BuildHelper(object): |
| 135 """Helper class for generating and building targets.""" | 139 """Helper class for generating and building targets.""" |
| 136 def __init__(self, args): | 140 def __init__(self, args): |
| 141 self.cloud = args.cloud | |
| 137 self.enable_chrome_android_internal = args.enable_chrome_android_internal | 142 self.enable_chrome_android_internal = args.enable_chrome_android_internal |
| 138 self.extra_gn_args_str = '' | 143 self.extra_gn_args_str = '' |
| 139 self.max_jobs = args.max_jobs | 144 self.max_jobs = args.max_jobs |
| 140 self.max_load_average = args.max_load_average | 145 self.max_load_average = args.max_load_average |
| 141 self.output_directory = args.output_directory | 146 self.output_directory = args.output_directory |
| 142 self.target = args.target | 147 self.target = args.target |
| 143 self.target_os = args.target_os | 148 self.target_os = args.target_os |
| 144 self.use_goma = args.use_goma | 149 self.use_goma = args.use_goma |
| 145 self._SetDefaults() | 150 self._SetDefaults() |
| 146 | 151 |
| (...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 200 return ['gn', 'gen', self.output_directory, '--args=%s' % gn_args] | 205 return ['gn', 'gen', self.output_directory, '--args=%s' % gn_args] |
| 201 | 206 |
| 202 def _GenNinjaCmd(self): | 207 def _GenNinjaCmd(self): |
| 203 cmd = ['ninja', '-C', self.output_directory] | 208 cmd = ['ninja', '-C', self.output_directory] |
| 204 cmd += ['-j', self.max_jobs] if self.max_jobs else [] | 209 cmd += ['-j', self.max_jobs] if self.max_jobs else [] |
| 205 cmd += ['-l', self.max_load_average] if self.max_load_average else [] | 210 cmd += ['-l', self.max_load_average] if self.max_load_average else [] |
| 206 cmd += [self.target] | 211 cmd += [self.target] |
| 207 return cmd | 212 return cmd |
| 208 | 213 |
| 209 def Run(self): | 214 def Run(self): |
| 215 """Run GN gen/ninja build and return the process returncode.""" | |
| 210 _Print('Building: {}.', self.target) | 216 _Print('Building: {}.', self.target) |
| 211 _RunCmd(self._GenGnCmd(), print_stdout=True) | 217 retcode = _RunCmd( |
| 212 _RunCmd(self._GenNinjaCmd(), print_stdout=True) | 218 self._GenGnCmd(), print_stdout=True, exit_on_failure=False)[1] |
| 219 if retcode: | |
| 220 return retcode | |
| 221 return _RunCmd( | |
| 222 self._GenNinjaCmd(), print_stdout=True, exit_on_failure=False)[1] | |
| 213 | 223 |
| 214 def IsAndroid(self): | 224 def IsAndroid(self): |
| 215 return self.target_os == 'android' | 225 return self.target_os == 'android' |
| 216 | 226 |
| 217 def IsLinux(self): | 227 def IsLinux(self): |
| 218 return self.target_os == 'linux' | 228 return self.target_os == 'linux' |
| 219 | 229 |
| 230 def IsCloud(self): | |
| 231 return self.cloud | |
| 220 | 232 |
| 221 def _RunCmd(cmd, print_stdout=False): | 233 |
| 234 class _BuildArchive(object): | |
| 235 """Class for managing a directory with build results and build metadata.""" | |
| 236 def __init__(self, rev, base_archive_dir, build): | |
| 237 self.build = build | |
| 238 self.dir = os.path.join(base_archive_dir, rev) | |
| 239 _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.
| |
| 240 metadata_path = os.path.join(self.dir, 'metadata.txt') | |
| 241 self.rev = rev | |
| 242 self.metadata = _GenerateMetadata([self], build, metadata_path) | |
| 243 | |
| 244 def ArchiveBuildResults(self): | |
| 245 """Save build artifacts necessary for diffing.""" | |
| 246 _Print('Saving build results to: {}', self.dir) | |
| 247 build = self.build | |
| 248 self._ArchiveFile(build.main_lib_path) | |
| 249 lib_name_noext = os.path.splitext(os.path.basename(build.main_lib_path))[0] | |
| 250 size_path = os.path.join(self.dir, lib_name_noext + '.size') | |
| 251 supersize_path = os.path.join(_SRC_ROOT, 'tools/binary_size/supersize') | |
| 252 tool_prefix = _FindToolPrefix(build.output_directory) | |
| 253 supersize_cmd = [supersize_path, 'archive', size_path, '--elf-file', | |
| 254 build.main_lib_path, '--tool-prefix', tool_prefix, | |
| 255 '--output-directory', build.output_directory, | |
| 256 '--no-source-paths'] | |
| 257 if build.IsAndroid(): | |
| 258 supersize_cmd += ['--apk-file', build.abs_apk_path] | |
| 259 self._ArchiveFile(build.abs_apk_path) | |
| 260 | |
| 261 _RunCmd(supersize_cmd) | |
| 262 _WriteMetadata(self.metadata) | |
| 263 | |
| 264 def Exists(self): | |
| 265 return _MetadataExists(self.metadata) | |
| 266 | |
| 267 def _ArchiveFile(self, filename): | |
| 268 if not os.path.exists(filename): | |
| 269 _Die('missing expected file: {}', filename) | |
| 270 shutil.copy(filename, self.dir) | |
| 271 | |
| 272 | |
| 273 class _DiffArchiveManager(object): | |
| 274 """Class for maintaining BuildArchives and their related diff artifacts.""" | |
| 275 def __init__(self, revs, archive_dir, diffs, build): | |
| 276 self.archive_dir = archive_dir | |
| 277 _EnsureDirsExist(archive_dir) | |
| 278 self.build = build | |
| 279 self.build_archives = [_BuildArchive(rev, archive_dir, build) | |
| 280 for rev in revs] | |
| 281 self.diffs = diffs | |
| 282 | |
| 283 def IterArchives(self): | |
| 284 return iter(self.build_archives) | |
| 285 | |
| 286 def MaybeDiff(self, first_id, second_id): | |
| 287 """Perform diffs given two build archives.""" | |
| 288 archives = [ | |
| 289 self.build_archives[first_id], self.build_archives[second_id]] | |
| 290 diff_path = self._DiffFilePath(archives) | |
| 291 if not self._CanDiff(archives): | |
| 292 _Print('Skipping diff for {} due to missing build archives.', diff_path) | |
| 293 return | |
| 294 | |
| 295 metadata_path = self._DiffMetadataPath(archives) | |
| 296 metadata = _GenerateMetadata(archives, self.build, metadata_path) | |
| 297 if _MetadataExists(metadata): | |
| 298 _Print('Skipping diff for {} and {}. Matching diff already exists: {}', | |
| 299 archives[0].rev, archives[1].rev, diff_path) | |
| 300 else: | |
| 301 archive_dirs = [archives[0].dir, archives[1].dir] | |
| 302 with open(diff_path, 'a') as diff_file: | |
| 303 for d in self.diffs: | |
| 304 d.RunDiff(diff_file, archive_dirs) | |
| 305 _WriteMetadata(metadata) | |
| 306 | |
| 307 def _CanDiff(self, archives): | |
| 308 return all(a.Exists() for a in archives) | |
| 309 | |
| 310 def _DiffFilePath(self, archives): | |
| 311 return os.path.join(self._DiffDir(archives), 'diff_results.txt') | |
| 312 | |
| 313 def _DiffMetadataPath(self, archives): | |
| 314 return os.path.join(self._DiffDir(archives), 'metadata.txt') | |
| 315 | |
| 316 def _DiffDir(self, archives): | |
| 317 diff_path = os.path.join( | |
| 318 self.archive_dir, 'diffs', '_'.join(a.rev for a in archives)) | |
| 319 _EnsureDirsExist(diff_path) | |
| 320 return diff_path | |
| 321 | |
| 322 | |
| 323 def _EnsureDirsExist(path): | |
| 324 if not os.path.exists(path): | |
| 325 os.makedirs(path) | |
| 326 | |
| 327 | |
| 328 def _GenerateMetadata(archives, build, path): | |
| 329 return { | |
| 330 'revs': [a.rev for a in archives], | |
| 331 'archive_dirs': [a.dir for a in archives], | |
| 332 'target': build.target, | |
| 333 'target_os': build.target_os, | |
| 334 'is_cloud': build.IsCloud(), | |
| 335 'subrepo': _subrepo, | |
| 336 'path': path, | |
| 337 'gn_args': { | |
| 338 'extra_gn_args_str': build.extra_gn_args_str, | |
| 339 'enable_chrome_android_internal': build.enable_chrome_android_internal, | |
| 340 } | |
| 341 } | |
| 342 | |
| 343 | |
| 344 def _WriteMetadata(metadata): | |
| 345 with open(metadata['path'], 'w') as f: | |
| 346 json.dump(metadata, f) | |
| 347 | |
| 348 | |
| 349 def _MetadataExists(metadata): | |
| 350 old_metadata = {} | |
| 351 path = metadata['path'] | |
| 352 if os.path.exists(path): | |
| 353 with open(path, 'r') as f: | |
| 354 old_metadata = json.load(f) | |
| 355 ret = len(metadata) == len(old_metadata) | |
| 356 ret &= all(v == old_metadata[k] | |
| 357 for k, v in metadata.items() if k != 'gn_args') | |
| 358 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.
| |
| 359 ret &= metadata['gn_args'] == old_metadata['gn_args'] | |
| 360 return ret | |
| 361 return False | |
| 362 | |
| 363 | |
| 364 def _RunCmd(cmd, print_stdout=False, exit_on_failure=True): | |
| 222 """Convenience function for running commands. | 365 """Convenience function for running commands. |
| 223 | 366 |
| 224 Args: | 367 Args: |
| 225 cmd: the command to run. | 368 cmd: the command to run. |
| 226 print_stdout: if this is True, then the stdout of the process will be | 369 print_stdout: if this is True, then the stdout of the process will be |
| 227 printed, otherwise stdout will be returned. | 370 printed instead of returned. |
| 371 exit_on_failure: die if an error occurs when this is True. | |
| 228 | 372 |
| 229 Returns: | 373 Returns: |
| 230 Command stdout if |print_stdout| is False otherwise ''. | 374 Tuple of (process stdout, process returncode). |
| 231 """ | 375 """ |
| 232 cmd_str = ' '.join(c for c in cmd) | 376 cmd_str = ' '.join(c for c in cmd) |
| 233 _Print('Running: {}', cmd_str) | 377 _Print('Running: {}', cmd_str) |
| 234 if print_stdout: | 378 proc_stdout = sys.stdout if print_stdout else subprocess.PIPE |
| 235 proc_stdout = sys.stdout | |
| 236 else: | |
| 237 proc_stdout = subprocess.PIPE | |
| 238 | 379 |
| 239 proc = subprocess.Popen(cmd, stdout=proc_stdout, stderr=subprocess.PIPE) | 380 proc = subprocess.Popen(cmd, stdout=proc_stdout, stderr=subprocess.PIPE) |
| 240 stdout, stderr = proc.communicate() | 381 stdout, stderr = proc.communicate() |
| 241 | 382 |
| 242 if proc.returncode: | 383 if proc.returncode and exit_on_failure: |
| 243 _Die('command failed: {}\nstderr:\n{}', cmd_str, stderr) | 384 _Die('command failed: {}\nstderr:\n{}', cmd_str, stderr) |
| 244 | 385 |
| 245 return stdout.strip() if stdout else '' | 386 stdout = stdout.strip() if stdout else '' |
| 387 return stdout, proc.returncode | |
| 246 | 388 |
| 247 | 389 |
| 248 def _GitCmd(args): | 390 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
| |
| 249 return _RunCmd(['git', '-C', _SRC_ROOT] + args) | 391 return _RunCmd(['git', '-C', _subrepo] + args)[int(retcode)] |
| 250 | 392 |
| 251 | 393 |
| 252 def _GclientSyncCmd(rev): | 394 def _GclientSyncCmd(rev): |
| 253 cwd = os.getcwd() | 395 cwd = os.getcwd() |
| 254 os.chdir(_SRC_ROOT) | 396 os.chdir(_SRC_ROOT) |
| 255 _RunCmd(['gclient', 'sync', '-r', 'src@' + rev], print_stdout=True) | 397 _RunCmd(['gclient', 'sync', '-r', 'src@' + rev], print_stdout=True) |
| 256 os.chdir(cwd) | 398 os.chdir(cwd) |
| 257 | 399 |
| 258 | 400 |
| 259 def _ArchiveBuildResult(archive_dir, build): | |
| 260 """Save build artifacts necessary for diffing. | |
| 261 | |
| 262 Expects |build.output_directory| to be correct. | |
| 263 """ | |
| 264 _Print('Saving build results to: {}', archive_dir) | |
| 265 if not os.path.exists(archive_dir): | |
| 266 os.makedirs(archive_dir) | |
| 267 | |
| 268 def ArchiveFile(filename): | |
| 269 if not os.path.exists(filename): | |
| 270 _Die('missing expected file: {}', filename) | |
| 271 shutil.copy(filename, archive_dir) | |
| 272 | |
| 273 ArchiveFile(build.main_lib_path) | |
| 274 lib_name_noext = os.path.splitext(os.path.basename(build.main_lib_path))[0] | |
| 275 size_path = os.path.join(archive_dir, lib_name_noext + '.size') | |
| 276 supersize_path = os.path.join(_SRC_ROOT, 'tools/binary_size/supersize') | |
| 277 tool_prefix = _FindToolPrefix(build.output_directory) | |
| 278 supersize_cmd = [supersize_path, 'archive', size_path, '--elf-file', | |
| 279 build.main_lib_path, '--tool-prefix', tool_prefix, | |
| 280 '--output-directory', build.output_directory, | |
| 281 '--no-source-paths'] | |
| 282 if build.IsAndroid(): | |
| 283 supersize_cmd += ['--apk-file', build.abs_apk_path] | |
| 284 ArchiveFile(build.abs_apk_path) | |
| 285 | |
| 286 _RunCmd(supersize_cmd) | |
| 287 | |
| 288 | |
| 289 def _FindToolPrefix(output_directory): | 401 def _FindToolPrefix(output_directory): |
| 290 build_vars_path = os.path.join(output_directory, 'build_vars.txt') | 402 build_vars_path = os.path.join(output_directory, 'build_vars.txt') |
| 291 if os.path.exists(build_vars_path): | 403 if os.path.exists(build_vars_path): |
| 292 with open(build_vars_path) as f: | 404 with open(build_vars_path) as f: |
| 293 build_vars = dict(l.rstrip().split('=', 1) for l in f if '=' in l) | 405 build_vars = dict(l.rstrip().split('=', 1) for l in f if '=' in l) |
| 294 # Tool prefix is relative to output dir, rebase to source root. | 406 # Tool prefix is relative to output dir, rebase to source root. |
| 295 tool_prefix = build_vars['android_tool_prefix'] | 407 tool_prefix = build_vars['android_tool_prefix'] |
| 296 while os.path.sep in tool_prefix: | 408 while os.path.sep in tool_prefix: |
| 297 rebased_tool_prefix = os.path.join(_SRC_ROOT, tool_prefix) | 409 rebased_tool_prefix = os.path.join(_SRC_ROOT, tool_prefix) |
| 298 if os.path.exists(rebased_tool_prefix + 'readelf'): | 410 if os.path.exists(rebased_tool_prefix + 'readelf'): |
| 299 return rebased_tool_prefix | 411 return rebased_tool_prefix |
| 300 tool_prefix = tool_prefix[tool_prefix.find(os.path.sep) + 1:] | 412 tool_prefix = tool_prefix[tool_prefix.find(os.path.sep) + 1:] |
| 301 return '' | 413 return '' |
| 302 | 414 |
| 303 | 415 |
| 304 def _SyncAndBuild(revs, archive_dirs, build): | 416 def _SyncAndBuild(archive, build, use_subrepo): |
| 305 # Move to a detached state since gclient sync doesn't work with local commits | 417 if use_subrepo: |
| 306 # on a branch. | 418 _GitCmd(['checkout', archive.rev]) |
| 307 _GitCmd(['checkout', '--detach']) | 419 else: |
| 308 for rev, archive_dir in itertools.izip(revs, archive_dirs): | 420 # Move to a detached state since gclient sync doesn't work with local |
| 309 _GclientSyncCmd(rev) | 421 # commits on a branch. |
| 310 build.Run() | 422 _GitCmd(['checkout', '--detach']) |
| 311 _ArchiveBuildResult(archive_dir, build) | 423 _GclientSyncCmd(archive.rev) |
| 424 retcode = build.Run() | |
| 425 return retcode == 0 | |
| 312 | 426 |
| 313 | 427 |
| 314 def _NormalizeRev(rev): | 428 def _GenerateRevList(with_patch, without_patch, all_in_range): |
| 315 """Use actual revs instead of HEAD, HEAD^, etc.""" | 429 """Normalize and optionally generate a list of commits in the given range. |
| 316 return _GitCmd(['rev-parse', rev]) | 430 |
| 431 Returns a list of revisions ordered from newest to oldest. | |
| 432 """ | |
| 433 retcode = _GitCmd(['merge-base', '--is-ancestor', without_patch, with_patch]) | |
| 434 assert not retcode and with_patch != without_patch, ( | |
| 435 'Invalid revision arguments, rev_without_patch (%s) is newer than ' | |
| 436 'rev_with_patch (%s)' % (without_patch, with_patch)) | |
| 437 | |
| 438 rev_seq = '%s^..%s' % (without_patch, with_patch) | |
| 439 stdout = _GitCmd(['rev-list', rev_seq]) | |
| 440 all_revs = stdout.splitlines() | |
| 441 if all_in_range: | |
| 442 revs = all_revs | |
| 443 else: | |
| 444 revs = [all_revs[0], all_revs[-1]] | |
| 445 | |
| 446 assert len(revs) <= _ALLOWED_CONSECUTIVE_BUILDS, ( | |
| 447 'Too many commits in range %s..%s, allowed: %d, found %d' % ( | |
| 448 without_patch, with_patch, _ALLOWED_CONSECUTIVE_BUILDS, len(revs))) | |
| 449 return revs | |
| 317 | 450 |
| 318 | 451 |
| 319 def _EnsureDirectoryClean(): | 452 def _EnsureDirectoryClean(): |
| 320 _Print('Checking source directory') | 453 _Print('Checking source directory') |
| 321 stdout = _GitCmd(['status', '--porcelain']) | 454 stdout = _GitCmd(['status', '--porcelain']) |
| 322 # Ignore untracked files. | 455 # Ignore untracked files. |
| 323 if stdout and stdout[:2] != '??': | 456 if stdout and stdout[:2] != '??': |
| 324 _Die('please ensure working directory is clean.') | 457 _Die('please ensure working directory is clean.') |
| 325 | 458 |
| 326 | 459 |
| 327 def _SetInitialBranch(): | 460 def _SetInitialBranch(): |
| 328 global _initial_branch | 461 global _initial_branch |
| 329 _initial_branch = _GitCmd(['rev-parse', '--abbrev-ref', 'HEAD']) | 462 _initial_branch = _GitCmd(['rev-parse', '--abbrev-ref', 'HEAD']) |
| 330 | 463 |
| 331 | 464 |
| 332 def _RestoreInitialBranch(): | 465 def _RestoreInitialBranch(): |
| 333 if _initial_branch: | 466 if _initial_branch: |
| 334 _GitCmd(['checkout', _initial_branch]) | 467 _GitCmd(['checkout', _initial_branch]) |
| 335 | 468 |
| 336 | 469 |
| 470 def _SetSubrepo(subrepo): | |
| 471 global _subrepo | |
| 472 _subrepo = subrepo or _SRC_ROOT | |
| 473 | |
| 474 | |
| 337 def _Die(s, *args, **kwargs): | 475 def _Die(s, *args, **kwargs): |
| 338 _Print('Failure: ' + s, *args, **kwargs) | 476 _Print('Failure: ' + s, *args, **kwargs) |
| 339 _RestoreInitialBranch() | 477 _RestoreInitialBranch() |
| 340 sys.exit(1) | 478 sys.exit(1) |
| 341 | 479 |
| 342 | 480 |
| 343 def _DownloadBuildArtifacts(revs, archive_dirs, build, depot_tools_path=None): | 481 def _DownloadBuildArtifacts(archive, build, depot_tools_path=None): |
| 344 """Download artifacts from arm32 chromium perf builder.""" | 482 """Download artifacts from arm32 chromium perf builder.""" |
| 345 if depot_tools_path: | 483 if depot_tools_path: |
| 346 gsutil_path = os.path.join(depot_tools_path, 'gsutil.py') | 484 gsutil_path = os.path.join(depot_tools_path, 'gsutil.py') |
| 347 else: | 485 else: |
| 348 gsutil_path = distutils.spawn.find_executable('gsutil.py') | 486 gsutil_path = distutils.spawn.find_executable('gsutil.py') |
| 349 | 487 |
| 350 if not gsutil_path: | 488 if not gsutil_path: |
| 351 _Die('gsutil.py not found, please provide path to depot_tools via ' | 489 _Die('gsutil.py not found, please provide path to depot_tools via ' |
| 352 '--depot-tools-path or add it to your PATH') | 490 '--depot-tools-path or add it to your PATH') |
| 353 | 491 |
| 354 download_dir = tempfile.mkdtemp(dir=_SRC_ROOT) | 492 download_dir = tempfile.mkdtemp(dir=_SRC_ROOT) |
| 355 try: | 493 try: |
| 356 for rev, archive_dir in itertools.izip(revs, archive_dirs): | 494 _DownloadAndArchive(gsutil_path, archive, download_dir, build) |
| 357 _DownloadAndArchive(gsutil_path, rev, archive_dir, download_dir, build) | |
| 358 finally: | 495 finally: |
| 359 shutil.rmtree(download_dir) | 496 shutil.rmtree(download_dir) |
| 360 | 497 |
| 361 | 498 |
| 362 def _DownloadAndArchive(gsutil_path, rev, archive_dir, dl_dir, build): | 499 def _DownloadAndArchive(gsutil_path, archive, dl_dir, build): |
| 363 dl_file = 'full-build-linux_%s.zip' % rev | 500 dl_file = 'full-build-linux_%s.zip' % archive.rev |
| 364 dl_url = 'gs://chrome-perf/Android Builder/%s' % dl_file | 501 dl_url = 'gs://chrome-perf/Android Builder/%s' % dl_file |
| 365 dl_dst = os.path.join(dl_dir, dl_file) | 502 dl_dst = os.path.join(dl_dir, dl_file) |
| 366 _Print('Downloading build artifacts for {}', rev) | 503 _Print('Downloading build artifacts for {}', archive.rev) |
| 367 # gsutil writes stdout and stderr to stderr, so pipe stdout and stderr to | 504 # gsutil writes stdout and stderr to stderr, so pipe stdout and stderr to |
| 368 # sys.stdout. | 505 # sys.stdout. |
| 369 retcode = subprocess.call([gsutil_path, 'cp', dl_url, dl_dir], | 506 retcode = subprocess.call([gsutil_path, 'cp', dl_url, dl_dir], |
| 370 stdout=sys.stdout, stderr=subprocess.STDOUT) | 507 stdout=sys.stdout, stderr=subprocess.STDOUT) |
| 371 if retcode: | 508 if retcode: |
| 372 _Die('unexpected error while downloading {}. It may no longer exist on ' | 509 _Die('unexpected error while downloading {}. It may no longer exist on ' |
| 373 'the server or it may not have been uploaded yet (check {}). ' | 510 'the server or it may not have been uploaded yet (check {}). ' |
| 374 'Otherwise, you may not have the correct access permissions.', | 511 'Otherwise, you may not have the correct access permissions.', |
| 375 dl_url, _BUILDER_URL) | 512 dl_url, _BUILDER_URL) |
| 376 | 513 |
| 377 # Files needed for supersize and resource_sizes. Paths relative to out dir. | 514 # Files needed for supersize and resource_sizes. Paths relative to out dir. |
| 378 to_extract = [build.main_lib_name, build.map_file_name, 'args.gn', | 515 to_extract = [build.main_lib_name, build.map_file_name, 'args.gn', |
| 379 'build_vars.txt', build.apk_path] | 516 'build_vars.txt', build.apk_path] |
| 380 extract_dir = os.path.join(os.path.splitext(dl_dst)[0], 'unzipped') | 517 extract_dir = os.path.join(os.path.splitext(dl_dst)[0], 'unzipped') |
| 381 # Storage bucket stores entire output directory including out/Release prefix. | 518 # Storage bucket stores entire output directory including out/Release prefix. |
| 382 _Print('Extracting build artifacts') | 519 _Print('Extracting build artifacts') |
| 383 with zipfile.ZipFile(dl_dst, 'r') as z: | 520 with zipfile.ZipFile(dl_dst, 'r') as z: |
| 384 _ExtractFiles(to_extract, _CLOUD_OUT_DIR, extract_dir, z) | 521 _ExtractFiles(to_extract, _CLOUD_OUT_DIR, extract_dir, z) |
| 385 dl_out = os.path.join(extract_dir, _CLOUD_OUT_DIR) | 522 dl_out = os.path.join(extract_dir, _CLOUD_OUT_DIR) |
| 386 build.output_directory, output_directory = dl_out, build.output_directory | 523 build.output_directory, output_directory = dl_out, build.output_directory |
| 387 _ArchiveBuildResult(archive_dir, build) | 524 archive.ArchiveBuildResults() |
| 388 build.output_directory = output_directory | 525 build.output_directory = output_directory |
| 389 | 526 |
| 390 | 527 |
| 391 def _ExtractFiles(to_extract, prefix, dst, z): | 528 def _ExtractFiles(to_extract, prefix, dst, z): |
| 392 zip_infos = z.infolist() | 529 zip_infos = z.infolist() |
| 393 assert all(info.filename.startswith(prefix) for info in zip_infos), ( | 530 assert all(info.filename.startswith(prefix) for info in zip_infos), ( |
| 394 'Storage bucket folder structure doesn\'t start with %s' % prefix) | 531 'Storage bucket folder structure doesn\'t start with %s' % prefix) |
| 395 to_extract = [os.path.join(prefix, f) for f in to_extract] | 532 to_extract = [os.path.join(prefix, f) for f in to_extract] |
| 396 for f in to_extract: | 533 for f in to_extract: |
| 397 z.extract(f, path=dst) | 534 z.extract(f, path=dst) |
| (...skipping 15 matching lines...) Expand all Loading... | |
| 413 formatter_class=argparse.ArgumentDefaultsHelpFormatter) | 550 formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 414 parser.add_argument('--archive-dir', | 551 parser.add_argument('--archive-dir', |
| 415 default=_DEFAULT_ARCHIVE_DIR, | 552 default=_DEFAULT_ARCHIVE_DIR, |
| 416 help='Where results are stored.') | 553 help='Where results are stored.') |
| 417 parser.add_argument('--rev-with-patch', | 554 parser.add_argument('--rev-with-patch', |
| 418 default='HEAD', | 555 default='HEAD', |
| 419 help='Commit with patch.') | 556 help='Commit with patch.') |
| 420 parser.add_argument('--rev-without-patch', | 557 parser.add_argument('--rev-without-patch', |
| 421 help='Older patch to diff against. If not supplied, ' | 558 help='Older patch to diff against. If not supplied, ' |
| 422 'the previous commit to rev_with_patch will be used.') | 559 'the previous commit to rev_with_patch will be used.') |
| 560 parser.add_argument('--all', | |
| 561 action='store_true', | |
| 562 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
| |
| 563 'rev_without_patch and diff contiguous revisions.') | |
| 423 parser.add_argument('--include-slow-options', | 564 parser.add_argument('--include-slow-options', |
| 424 action='store_true', | 565 action='store_true', |
| 425 help='Run some extra steps that take longer to complete. ' | 566 help='Run some extra steps that take longer to complete. ' |
| 426 'This includes apk-patch-size estimation and ' | 567 'This includes apk-patch-size estimation and ' |
| 427 'static-initializer counting') | 568 'static-initializer counting') |
| 428 parser.add_argument('--cloud', | 569 parser.add_argument('--cloud', |
| 429 action='store_true', | 570 action='store_true', |
| 430 help='Download build artifacts from perf builders ' | 571 help='Download build artifacts from perf builders ' |
| 431 '(Android only, Googlers only).') | 572 '(Android only, Googlers only).') |
| 432 parser.add_argument('--depot-tools-path', | 573 parser.add_argument('--depot-tools-path', |
| 433 help='Custom path to depot tools. Needed for --cloud if ' | 574 help='Custom path to depot tools. Needed for --cloud if ' |
| 434 'depot tools isn\'t in your PATH') | 575 'depot tools isn\'t in your PATH') |
| 576 parser.add_argument('--subrepo', | |
| 577 help='Specify a subrepo directory to use. Gclient sync ' | |
| 578 'will be skipped if this option is used and all git ' | |
| 579 'commands will be executed from the subrepo directory. ' | |
| 580 'This option doesn\'t work with --cloud.') | |
| 435 | 581 |
| 436 build_group = parser.add_argument_group('ninja', 'Args to use with ninja/gn') | 582 build_group = parser.add_argument_group('ninja', 'Args to use with ninja/gn') |
| 437 build_group.add_argument('-j', | 583 build_group.add_argument('-j', |
| 438 dest='max_jobs', | 584 dest='max_jobs', |
| 439 help='Run N jobs in parallel.') | 585 help='Run N jobs in parallel.') |
| 440 build_group.add_argument('-l', | 586 build_group.add_argument('-l', |
| 441 dest='max_load_average', | 587 dest='max_load_average', |
| 442 help='Do not start new jobs if the load average is ' | 588 help='Do not start new jobs if the load average is ' |
| 443 'greater than N.') | 589 'greater than N.') |
| 444 build_group.add_argument('--no-goma', | 590 build_group.add_argument('--no-goma', |
| 445 action='store_false', | 591 action='store_false', |
| 446 dest='use_goma', | 592 dest='use_goma', |
| 447 default=True, | 593 default=True, |
| 448 help='Use goma when building with ninja.') | 594 help='Use goma when building with ninja.') |
| 449 build_group.add_argument('--target-os', | 595 build_group.add_argument('--target-os', |
| 450 default='android', | 596 default='android', |
| 451 choices=['android', 'linux'], | 597 choices=['android', 'linux'], |
| 452 help='target_os gn arg.') | 598 help='target_os gn arg.') |
| 453 build_group.add_argument('--output-directory', | 599 build_group.add_argument('--output-directory', |
| 454 default=_DEFAULT_OUT_DIR, | 600 default=_DEFAULT_OUT_DIR, |
| 455 help='ninja output directory.') | 601 help='ninja output directory.') |
| 456 build_group.add_argument('--enable_chrome_android_internal', | 602 build_group.add_argument('--enable-chrome-android-internal', |
| 457 action='store_true', | 603 action='store_true', |
| 458 help='Allow downstream targets to be built.') | 604 help='Allow downstream targets to be built.') |
| 459 build_group.add_argument('--target', | 605 build_group.add_argument('--target', |
| 460 default=_DEFAULT_TARGET, | 606 default=_DEFAULT_TARGET, |
| 461 help='GN APK target to build.') | 607 help='GN APK target to build.') |
| 462 args = parser.parse_args() | 608 args = parser.parse_args() |
| 463 build = _BuildHelper(args) | 609 build = _BuildHelper(args) |
| 464 if args.cloud and build.IsLinux(): | 610 _SetSubrepo(args.subrepo) |
| 465 parser.error('--cloud only works for android') | 611 use_subrepo = bool(args.subrepo) |
| 612 if build.IsCloud(): | |
| 613 if build.IsLinux(): | |
| 614 parser.error('--cloud only works for android') | |
| 615 if use_subrepo: | |
| 616 parser.error('--subrepo doesn\'t work with --cloud') | |
| 466 | 617 |
| 467 _EnsureDirectoryClean() | 618 _EnsureDirectoryClean() |
| 468 _SetInitialBranch() | 619 _SetInitialBranch() |
| 469 revs = [args.rev_with_patch, | 620 revs = _GenerateRevList(args.rev_with_patch, |
| 470 args.rev_without_patch or args.rev_with_patch + '^'] | 621 args.rev_without_patch or args.rev_with_patch + '^', |
| 471 revs = [_NormalizeRev(r) for r in revs] | 622 args.all) |
| 472 archive_dirs = [os.path.join(args.archive_dir, '%d-%s' % (len(revs) - i, rev)) | |
| 473 for i, rev in enumerate(revs)] | |
| 474 if args.cloud: | |
| 475 _DownloadBuildArtifacts(revs, archive_dirs, build, | |
| 476 depot_tools_path=args.depot_tools_path) | |
| 477 else: | |
| 478 _SetInitialBranch() | |
| 479 _SyncAndBuild(revs, archive_dirs, build) | |
| 480 _RestoreInitialBranch() | |
| 481 | |
| 482 output_file = os.path.join(args.archive_dir, | |
| 483 'diff_result_{}_{}.txt'.format(*revs)) | |
| 484 if os.path.exists(output_file): | |
| 485 os.remove(output_file) | |
| 486 diffs = [] | 623 diffs = [] |
| 487 if build.IsAndroid(): | 624 if build.IsAndroid(): |
| 488 diffs += [ | 625 diffs += [ |
| 489 ResourceSizesDiff(archive_dirs, build.apk_name, | 626 ResourceSizesDiff( |
| 490 slow_options=args.include_slow_options) | 627 build.apk_name, slow_options=args.include_slow_options) |
| 491 ] | 628 ] |
| 492 with open(output_file, 'a') as logfile: | 629 diff_mngr = _DiffArchiveManager(revs, args.archive_dir, diffs, build) |
| 493 for d in diffs: | 630 consecutive_failures = 0 |
| 494 d.AppendResults(logfile) | 631 for i, archive in enumerate(diff_mngr.IterArchives()): |
| 632 if archive.Exists(): | |
| 633 _Print('Found matching metadata for {}, skipping build step.', | |
| 634 archive.rev) | |
| 635 else: | |
| 636 if build.IsCloud(): | |
| 637 _DownloadBuildArtifacts(archive, build, | |
| 638 depot_tools_path=args.depot_tools_path) | |
| 639 else: | |
| 640 build_success = _SyncAndBuild(archive, build, use_subrepo) | |
| 641 if not build_success: | |
| 642 consecutive_failures += 1 | |
| 643 if consecutive_failures > _ALLOWED_CONSECUTIVE_FAILURES: | |
| 644 _Die('{} builds failed in a row, last failure was {}.', | |
| 645 consecutive_failures, archive.rev) | |
| 646 else: | |
| 647 archive.ArchiveBuildResults() | |
| 648 consecutive_failures = 0 | |
| 649 | |
| 650 if i != 0: | |
| 651 diff_mngr.MaybeDiff(i - 1, i) | |
| 652 | |
| 653 _RestoreInitialBranch() | |
| 495 | 654 |
| 496 if __name__ == '__main__': | 655 if __name__ == '__main__': |
| 497 sys.exit(main()) | 656 sys.exit(main()) |
| 498 | 657 |
| OLD | NEW |