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

Side by Side 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 unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
OLDNEW
« 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