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

Side by Side Diff: tools/binary_size/diagnose_apk_bloat.py

Issue 2810813003: Add resource_sizes diffs to diagnose_apk_bloat.py. (Closed)
Patch Set: Change output file naming convention + append to it 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 | « tools/binary_size/README.md ('k') | 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 logging 12 import collections
13 import itertools
14 import json
13 import multiprocessing 15 import multiprocessing
14 import os 16 import os
15 import shutil 17 import shutil
16 import subprocess 18 import subprocess
17 import sys 19 import sys
18 20
19 import helpers 21 _SRC_ROOT = os.path.abspath(
22 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
23 _DEFAULT_OUT_DIR = os.path.join(_SRC_ROOT, 'out', 'diagnose-apk-bloat')
24 _DEFAULT_TARGET = 'monochrome_public_apk'
25 _DEFAULT_ARCHIVE_DIR = os.path.join(_SRC_ROOT, 'binary-size-bloat')
20 26
21 _DEFAULT_OUT_DIR = os.path.join(helpers.SRC_ROOT, 'out', 'diagnose-apk-bloat') 27 # Global variable for storing the initial branch before the script was launched
22 _DEFAULT_TARGET = 'monochrome_public_apk' 28 # so that it doesn't need to be passed everywhere in case we fail and exit.
23 _DEFAULT_ARCHIVE_DIR = os.path.join(helpers.SRC_ROOT, 'binary-size-bloat') 29 _initial_branch = None
30
31
32 class BaseDiff(object):
33 """Base class capturing binary size diffs."""
34 def __init__(self, name):
35 self.name = name
36 self.banner = '\n' + '*' * 30 + name + '*' * 30
37 self.RunDiff()
38
39 def PrintResults(self, output_file):
agrieve 2017/04/12 02:02:05 nit: Maybe just rename this to "AppendResults", an
40 with open(output_file, 'a') as logfile:
41 _PrintAndWriteToFile(logfile, self.banner)
42 _PrintAndWriteToFile(logfile, 'Summary:')
43 _PrintAndWriteToFile(logfile, self.Summary())
44 _PrintAndWriteToFile(logfile, '\nDetails:')
45 for l in self.DetailedResults():
46 _PrintAndWriteToFile(logfile, l)
47
48 def Summary(self):
49 """A short description that summarizes the source of binary size bloat."""
50 raise NotImplementedError()
51
52 def DetailedResults(self):
53 """An iterable description of the cause of binary size bloat."""
54 raise NotImplementedError()
55
56 def ProduceDiff(self):
57 """Prepare a binary size diff with ready to print results."""
58 raise NotImplementedError()
59
60 def RunDiff(self):
61 _Print('Creating {}', self.name)
62 self.ProduceDiff()
63
64
65 _ResourceSizesDiffResult = collections.namedtuple(
66 'ResourceSizesDiffResult', ['section', 'value', 'units'])
67
68
69 class ResourceSizesDiff(BaseDiff):
70 _RESOURCE_SIZES_PATH = os.path.join(
71 _SRC_ROOT, 'build', 'android', 'resource_sizes.py')
72
73 def __init__(self, archive_dirs, apk_name, slow_options=False):
74 self._archive_dirs = archive_dirs
75 self._apk_name = apk_name
76 self._slow_options = slow_options
77 self._diff = None # Set by |ProduceDiff()|
78 super(ResourceSizesDiff, self).__init__('Resource Sizes Diff')
79
80 def DetailedResults(self):
81 for section, value, units in self._diff:
82 yield '{:>+10,} {} {}'.format(value, units, section)
83
84 def Summary(self):
85 for s in self._diff:
86 if 'normalized' in s.section:
87 return 'Normalized APK size: {:+,} {}'.format(s.value, s.units)
88 return ''
89
90 def ProduceDiff(self):
91 chartjsons = self._RunResourceSizes()
92 diff = []
93 with_patch = chartjsons[0]['charts']
94 without_patch = chartjsons[1]['charts']
95 for section, section_dict in with_patch.iteritems():
96 for subsection, v in section_dict.iteritems():
97 # Ignore entries when resource_sizes.py chartjson format has changed.
98 if (section not in without_patch or
99 subsection not in without_patch[section] or
100 v['units'] != without_patch[section][subsection]['units']):
101 _Print('Found differing dict structures for resource_sizes.py, '
102 'skipping {} {}', section, subsection)
103 else:
104 diff.append(
105 _ResourceSizesDiffResult(
106 '%s %s' % (section, subsection),
107 v['value'] - without_patch[section][subsection]['value'],
108 v['units']))
109 self._diff = sorted(diff, key=lambda x: abs(x.value), reverse=True)
110
111 def _RunResourceSizes(self):
112 chartjsons = []
113 for archive_dir in self._archive_dirs:
114 apk_path = os.path.join(archive_dir, self._apk_name)
115 chartjson_file = os.path.join(archive_dir, 'results-chart.json')
116 cmd = [self._RESOURCE_SIZES_PATH, '--output-dir', archive_dir,
117 '--no-output-dir',
118 '--chartjson', apk_path]
119 if self._slow_options:
120 cmd += ['--estimate-patch-size']
121 else:
122 cmd += ['--no-static-initializer-check']
123 _RunCmd(cmd)
124 with open(chartjson_file) as f:
125 chartjsons.append(json.load(f))
126 return chartjsons
24 127
25 128
26 class _BuildHelper(object): 129 class _BuildHelper(object):
27 """Helper class for generating and building targets.""" 130 """Helper class for generating and building targets."""
28 131
29 def __init__(self, args): 132 def __init__(self, args):
30 self.enable_chrome_android_internal = args.enable_chrome_android_internal 133 self.enable_chrome_android_internal = args.enable_chrome_android_internal
31 self.max_jobs = args.max_jobs 134 self.max_jobs = args.max_jobs
32 self.max_load_average = args.max_load_average 135 self.max_load_average = args.max_load_average
33 self.output_directory = args.output_directory 136 self.output_directory = args.output_directory
(...skipping 21 matching lines...) Expand all
55 return ['gn', 'gen', self.output_directory, '--args=%s' % gn_args] 158 return ['gn', 'gen', self.output_directory, '--args=%s' % gn_args]
56 159
57 def _GenNinjaCmd(self): 160 def _GenNinjaCmd(self):
58 cmd = ['ninja', '-C', self.output_directory] 161 cmd = ['ninja', '-C', self.output_directory]
59 cmd += ['-j', self.max_jobs] if self.max_jobs else [] 162 cmd += ['-j', self.max_jobs] if self.max_jobs else []
60 cmd += ['-l', self.max_load_average] if self.max_load_average else [] 163 cmd += ['-l', self.max_load_average] if self.max_load_average else []
61 cmd += [self.target] 164 cmd += [self.target]
62 return cmd 165 return cmd
63 166
64 def Build(self): 167 def Build(self):
65 logging.info('Building %s. This may take a while (run with -vv for ' 168 _Print('Building: {}.', self.target)
66 'detailed ninja output).', self.target)
67 _RunCmd(self._GenGnCmd()) 169 _RunCmd(self._GenGnCmd())
68 _RunCmd(self._GenNinjaCmd(), print_stdout=True) 170 _RunCmd(self._GenNinjaCmd(), print_stdout=True)
69 171
70 172
71 def _GetLinkerMapPath(target_os, target): 173 def _GetMainLibPath(target_os, target):
72 # TODO(estevenson): Get this from GN instead of hardcoding. 174 # TODO(estevenson): Get this from GN instead of hardcoding.
73 if target_os == 'linux': 175 if target_os == 'linux':
74 return 'chrome.map.gz' 176 return 'chrome'
75 elif 'monochrome' in target: 177 elif 'monochrome' in target:
76 return 'lib.unstripped/libmonochrome.so.map.gz' 178 return 'lib.unstripped/libmonochrome.so'
77 else: 179 else:
78 return 'lib.unstripped/libchrome.so.map.gz' 180 return 'lib.unstripped/libchrome.so'
79 181
80 182
81 def _ApkPathFromTarget(target): 183 def _ApkNameFromTarget(target):
82 # Only works on apk targets that follow: my_great_apk naming convention. 184 # Only works on apk targets that follow: my_great_apk naming convention.
83 apk_name = ''.join(s.title() for s in target.split('_')[:-1]) + '.apk' 185 apk_name = ''.join(s.title() for s in target.split('_')[:-1]) + '.apk'
84 return os.path.join('apks', apk_name) 186 return apk_name.replace('Webview', 'WebView')
85 187
86 188
87 def _RunCmd(cmd, print_stdout=False): 189 def _RunCmd(cmd, print_stdout=False):
88 """Convenience function for running commands. 190 """Convenience function for running commands.
89 191
90 Args: 192 Args:
91 cmd: the command to run. 193 cmd: the command to run.
92 print_stdout: if this is True, then the stdout of the process will be 194 print_stdout: if this is True, then the stdout of the process will be
93 printed (to stdout if log level is DEBUG otherwise to /dev/null). 195 printed, otherwise stdout will be returned.
94 If false, stdout will be returned.
95 196
96 Returns: 197 Returns:
97 Command stdout if |print_stdout| is False otherwise ''. 198 Command stdout if |print_stdout| is False otherwise ''.
98 """ 199 """
99 cmd_str = ' '.join(c for c in cmd) 200 cmd_str = ' '.join(c for c in cmd)
100 logging.debug('Running: %s', cmd_str) 201 _Print('Running: {}', cmd_str)
101 if not print_stdout: 202 if print_stdout:
102 proc_stdout = subprocess.PIPE
103 elif logging.getLogger().isEnabledFor(logging.DEBUG):
104 proc_stdout = sys.stdout 203 proc_stdout = sys.stdout
105 else: 204 else:
106 proc_stdout = open(os.devnull, 'wb') 205 proc_stdout = subprocess.PIPE
107 206
108 proc = subprocess.Popen(cmd, stdout=proc_stdout, stderr=subprocess.PIPE) 207 proc = subprocess.Popen(cmd, stdout=proc_stdout, stderr=subprocess.PIPE)
109 stdout, stderr = proc.communicate() 208 stdout, stderr = proc.communicate()
110 209
111 if proc.returncode != 0: 210 if proc.returncode != 0:
112 logging.error('Command failed: %s\nstderr:\n%s' % (cmd_str, stderr)) 211 _Die('command failed: {}\nstderr:\n{}', cmd_str, stderr)
113 sys.exit(1)
114 212
115 return stdout.strip() if stdout else '' 213 return stdout.strip() if stdout else ''
116 214
117 215
118 def _GitCmd(args): 216 def _GitCmd(args):
119 return _RunCmd(['git', '-C', helpers.SRC_ROOT] + args) 217 return _RunCmd(['git', '-C', _SRC_ROOT] + args)
120 218
121 219
122 def _GclientSyncCmd(rev): 220 def _GclientSyncCmd(rev):
123 cwd = os.getcwd() 221 cwd = os.getcwd()
124 os.chdir(helpers.SRC_ROOT) 222 os.chdir(_SRC_ROOT)
125 logging.info('gclient sync to %s', rev)
126 _RunCmd(['gclient', 'sync', '-r', 'src@' + rev], print_stdout=True) 223 _RunCmd(['gclient', 'sync', '-r', 'src@' + rev], print_stdout=True)
127 os.chdir(cwd) 224 os.chdir(cwd)
128 225
129 226
130 def _ArchiveBuildResult(archive_dir, build_helper): 227 def _ArchiveBuildResult(archive_dir, build_helper):
131 """Save resulting APK and mapping file.""" 228 """Save build artifacts necessary for diffing."""
132 def ArchiveFile(file_path): 229 _Print('Saving build results to: {}', archive_dir)
133 file_path = os.path.join(build_helper.output_directory, file_path) 230 if not os.path.exists(archive_dir):
134 if os.path.exists(file_path): 231 os.makedirs(archive_dir)
135 if not os.path.exists(archive_dir):
136 os.makedirs(archive_dir)
137 shutil.copy(file_path, archive_dir)
138 else:
139 logging.error('Expected file: %s not found.' % file_path)
140 sys.exit(1)
141 232
142 logging.info('Saving build results to: %s', archive_dir) 233 def ArchiveFile(filename):
143 ArchiveFile(_GetLinkerMapPath(build_helper.target_os, build_helper.target)) 234 if not os.path.exists(filename):
235 _Die('missing expected file: {}', filename)
236 shutil.copy(filename, archive_dir)
237
238 lib_path = os.path.join(
239 build_helper.output_directory,
240 _GetMainLibPath(build_helper.target_os, build_helper.target))
241 ArchiveFile(lib_path)
242
243 size_path = os.path.join(
244 archive_dir, os.path.splitext(os.path.basename(lib_path))[0] + '.size')
245 supersize_path = os.path.join(_SRC_ROOT, 'tools/binary_size/supersize')
246 _RunCmd([supersize_path, 'archive', size_path, '--output-directory',
247 build_helper.output_directory, '--elf-file', lib_path])
248
144 if build_helper.target_os == 'android': 249 if build_helper.target_os == 'android':
145 ArchiveFile(_ApkPathFromTarget(build_helper.target)) 250 apk_path = os.path.join(build_helper.output_directory, 'apks',
251 _ApkNameFromTarget(build_helper.target))
252 ArchiveFile(apk_path)
146 253
147 254
148 def _SyncAndBuild(rev_with_patch, rev_without_patch, archive_dir, build_helper): 255 def _SyncAndBuild(revs, archive_dirs, build_helper):
149 rev_with_patch = _GitCmd(['rev-parse', rev_with_patch])
150 rev_without_patch = _GitCmd([
151 'rev-parse', rev_without_patch or rev_with_patch + '^'])
152
153 # Move to a detached state since gclient sync doesn't work with local commits 256 # Move to a detached state since gclient sync doesn't work with local commits
154 # on a branch. 257 # on a branch.
155 _GitCmd(['checkout', '--detach']) 258 _GitCmd(['checkout', '--detach'])
259 for rev, archive_dir in itertools.izip(revs, archive_dirs):
260 _GclientSyncCmd(rev)
261 build_helper.Build()
262 _ArchiveBuildResult(archive_dir, build_helper)
156 263
157 _GclientSyncCmd(rev_with_patch)
158 build_helper.Build()
159 _ArchiveBuildResult(
160 os.path.join(archive_dir, 'with_patch_%s' % rev_with_patch), build_helper)
161 264
162 _GclientSyncCmd(rev_without_patch) 265 def _NormalizeRev(rev):
163 build_helper.Build() 266 """Use actual revs instead of HEAD, HEAD^, etc."""
164 _ArchiveBuildResult( 267 return _GitCmd(['rev-parse', rev])
165 os.path.join(archive_dir, 'without_patch_%s' % rev_without_patch),
166 build_helper)
167 268
168 269
169 def _EnsureDirectoryClean(): 270 def _EnsureDirectoryClean():
170 logging.info('Checking source directory') 271 _Print('Checking source directory')
171 stdout = _GitCmd(['status', '--porcelain']) 272 stdout = _GitCmd(['status', '--porcelain'])
172 # Ignore untracked files. 273 # Ignore untracked files.
173 if stdout and stdout[:2] != '??': 274 if stdout and stdout[:2] != '??':
174 logging.error('Failure: please ensure working directory is clean.') 275 _Die('please ensure working directory is clean.')
175 sys.exit(1) 276
277
278 def _SetInitialBranch():
279 global _initial_branch
280 _initial_branch = _GitCmd(['rev-parse', '--abbrev-ref', 'HEAD'])
281
282
283 def _RestoreInitialBranch():
284 if _initial_branch:
285 _GitCmd(['checkout', _initial_branch])
286
287
288 def _Die(s, *args, **kwargs):
289 _Print('Failure: ' + s, *args, **kwargs)
290 _RestoreInitialBranch()
291 sys.exit(1)
292
293
294 def _Print(s, *args, **kwargs):
295 print s.format(*args, **kwargs)
296
297
298 def _PrintAndWriteToFile(logfile, s):
299 """Print |s| to |logfile| and stdout."""
300 _Print(s)
301 logfile.write('%s\n' % s)
176 302
177 303
178 def main(): 304 def main():
179 parser = argparse.ArgumentParser( 305 parser = argparse.ArgumentParser(
180 description='Find the cause of APK size bloat.', 306 description='Find the cause of APK size bloat.',
181 formatter_class=argparse.ArgumentDefaultsHelpFormatter) 307 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
182 parser.add_argument('--archive-dir', 308 parser.add_argument('--archive-dir',
183 default=_DEFAULT_ARCHIVE_DIR, 309 default=_DEFAULT_ARCHIVE_DIR,
184 help='Where results are stored.') 310 help='Where results are stored.')
185 parser.add_argument('--rev-with-patch', 311 parser.add_argument('--rev-with-patch',
186 default='HEAD', 312 default='HEAD',
187 help='Commit with patch.') 313 help='Commit with patch.')
188 parser.add_argument('--rev-without-patch', 314 parser.add_argument('--rev-without-patch',
189 help='Older patch to diff against. If not supplied, ' 315 help='Older patch to diff against. If not supplied, '
190 'the previous commit to rev_with_patch will be used.') 316 'the previous commit to rev_with_patch will be used.')
317 parser.add_argument('--include-slow-options',
318 action='store_true',
319 help='Run some extra steps that take longer to complete. '
320 'This includes apk-patch-size estimation and '
321 'static-initializer counting')
191 322
192 build_group = parser.add_argument_group('ninja', 'Args to use with ninja/gn') 323 build_group = parser.add_argument_group('ninja', 'Args to use with ninja/gn')
193 build_group.add_argument('-j', 324 build_group.add_argument('-j',
194 dest='max_jobs', 325 dest='max_jobs',
195 help='Run N jobs in parallel.') 326 help='Run N jobs in parallel.')
196 build_group.add_argument('-l', 327 build_group.add_argument('-l',
197 dest='max_load_average', 328 dest='max_load_average',
198 help='Do not start new jobs if the load average is ' 329 help='Do not start new jobs if the load average is '
199 'greater than N.') 330 'greater than N.')
200 build_group.add_argument('--no-goma', 331 build_group.add_argument('--no-goma',
201 action='store_false', 332 action='store_false',
202 dest='use_goma', 333 dest='use_goma',
203 default=True, 334 default=True,
204 help='Use goma when building with ninja.') 335 help='Use goma when building with ninja.')
205 build_group.add_argument('--target-os', 336 build_group.add_argument('--target-os',
206 default='android', 337 default='android',
207 choices=['android', 'linux'], 338 choices=['android', 'linux'],
208 help='target_os gn arg.') 339 help='target_os gn arg.')
209 build_group.add_argument('--output-directory', 340 build_group.add_argument('--output-directory',
210 default=_DEFAULT_OUT_DIR, 341 default=_DEFAULT_OUT_DIR,
211 help='ninja output directory.') 342 help='ninja output directory.')
212 build_group.add_argument('--enable_chrome_android_internal', 343 build_group.add_argument('--enable_chrome_android_internal',
213 action='store_true', 344 action='store_true',
214 help='Allow downstream targets to be built.') 345 help='Allow downstream targets to be built.')
215 build_group.add_argument('--target', 346 build_group.add_argument('--target',
216 default=_DEFAULT_TARGET, 347 default=_DEFAULT_TARGET,
217 help='GN APK target to build.') 348 help='GN APK target to build.')
218 args = helpers.AddCommonOptionsAndParseArgs(parser, sys.argv, pypy_warn=False) 349 args = parser.parse_args()
219 350
220 _EnsureDirectoryClean() 351 _EnsureDirectoryClean()
352 _SetInitialBranch()
353 revs = [args.rev_with_patch,
354 args.rev_without_patch or args.rev_with_patch + '^']
355 revs = [_NormalizeRev(r) for r in revs]
221 build_helper = _BuildHelper(args) 356 build_helper = _BuildHelper(args)
222 _SyncAndBuild(args.rev_with_patch, args.rev_without_patch, args.archive_dir, 357 archive_dirs = [os.path.join(args.archive_dir, '%d-%s' % (len(revs) - i, rev))
223 build_helper) 358 for i, rev in enumerate(revs)]
359 _SyncAndBuild(revs, archive_dirs, build_helper)
360 _RestoreInitialBranch()
224 361
362 output_file = os.path.join(args.archive_dir,
363 'diff_result_{}_{}.txt'.format(*revs))
364 if os.path.exists(output_file):
365 os.remove(output_file)
366 diffs = []
367 if build_helper.target_os == 'android':
368 diffs += [
369 ResourceSizesDiff(archive_dirs, _ApkNameFromTarget(args.target),
370 slow_options=args.include_slow_options)
371 ]
372 for d in diffs:
373 d.PrintResults(output_file)
225 374
226 if __name__ == '__main__': 375 if __name__ == '__main__':
227 sys.exit(main()) 376 sys.exit(main())
228 377
OLDNEW
« no previous file with comments | « tools/binary_size/README.md ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698