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

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: Put ProduceDiff back into BaseDiff 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):
40 with open(output_file, 'w+') 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 _RunCmd(['tools/binary_size/supersize', 'archive', '--output-directory',
agrieve 2017/04/12 00:20:50 I think you need to os.path.join this with _SRC_RO
estevenson 2017/04/12 01:39:23 Done.
246 build_helper.output_directory, '--elf-file', lib_path, size_path])
247
144 if build_helper.target_os == 'android': 248 if build_helper.target_os == 'android':
145 ArchiveFile(_ApkPathFromTarget(build_helper.target)) 249 apk_path = os.path.join(build_helper.output_directory, 'apks',
250 _ApkNameFromTarget(build_helper.target))
251 ArchiveFile(apk_path)
146 252
147 253
148 def _SyncAndBuild(rev_with_patch, rev_without_patch, archive_dir, build_helper): 254 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 255 # Move to a detached state since gclient sync doesn't work with local commits
154 # on a branch. 256 # on a branch.
155 _GitCmd(['checkout', '--detach']) 257 _GitCmd(['checkout', '--detach'])
258 for rev, archive_dir in itertools.izip(revs, archive_dirs):
259 _GclientSyncCmd(rev)
260 build_helper.Build()
261 _ArchiveBuildResult(archive_dir, build_helper)
156 262
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 263
162 _GclientSyncCmd(rev_without_patch) 264 def _NormalizeRev(rev):
163 build_helper.Build() 265 """Use actual revs instead of HEAD, HEAD^, etc."""
164 _ArchiveBuildResult( 266 return _GitCmd(['rev-parse', rev])
165 os.path.join(archive_dir, 'without_patch_%s' % rev_without_patch),
166 build_helper)
167 267
168 268
169 def _EnsureDirectoryClean(): 269 def _EnsureDirectoryClean():
170 logging.info('Checking source directory') 270 _Print('Checking source directory')
171 stdout = _GitCmd(['status', '--porcelain']) 271 stdout = _GitCmd(['status', '--porcelain'])
172 # Ignore untracked files. 272 # Ignore untracked files.
173 if stdout and stdout[:2] != '??': 273 if stdout and stdout[:2] != '??':
174 logging.error('Failure: please ensure working directory is clean.') 274 _Die('please ensure working directory is clean.')
175 sys.exit(1) 275
276
277 def _SetInitialBranch():
278 global _initial_branch
279 _initial_branch = _GitCmd(['rev-parse', '--abbrev-ref', 'HEAD'])
280
281
282 def _RestoreInitialBranch():
283 if _initial_branch:
284 _GitCmd(['checkout', _initial_branch])
285
286
287 def _Die(s, *args, **kwargs):
288 _Print('Failure: ' + s, *args, **kwargs)
289 _RestoreInitialBranch()
290 sys.exit(1)
291
292
293 def _Print(s, *args, **kwargs):
294 print s.format(*args, **kwargs)
295
296
297 def _PrintAndWriteToFile(logfile, s):
298 """Print |s| to |logfile| and stdout."""
299 _Print(s)
300 logfile.write('%s\n' % s)
176 301
177 302
178 def main(): 303 def main():
179 parser = argparse.ArgumentParser( 304 parser = argparse.ArgumentParser(
180 description='Find the cause of APK size bloat.', 305 description='Find the cause of APK size bloat.',
181 formatter_class=argparse.ArgumentDefaultsHelpFormatter) 306 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
182 parser.add_argument('--archive-dir', 307 parser.add_argument('--archive-dir',
183 default=_DEFAULT_ARCHIVE_DIR, 308 default=_DEFAULT_ARCHIVE_DIR,
184 help='Where results are stored.') 309 help='Where results are stored.')
185 parser.add_argument('--rev-with-patch', 310 parser.add_argument('--rev-with-patch',
186 default='HEAD', 311 default='HEAD',
187 help='Commit with patch.') 312 help='Commit with patch.')
188 parser.add_argument('--rev-without-patch', 313 parser.add_argument('--rev-without-patch',
189 help='Older patch to diff against. If not supplied, ' 314 help='Older patch to diff against. If not supplied, '
190 'the previous commit to rev_with_patch will be used.') 315 'the previous commit to rev_with_patch will be used.')
316 parser.add_argument('--include-slow-options',
317 action='store_true',
318 help='Run some extra steps that take longer to complete. '
319 'This includes apk-patch-size estimation and '
320 'static-initializer counting')
191 321
192 build_group = parser.add_argument_group('ninja', 'Args to use with ninja/gn') 322 build_group = parser.add_argument_group('ninja', 'Args to use with ninja/gn')
193 build_group.add_argument('-j', 323 build_group.add_argument('-j',
194 dest='max_jobs', 324 dest='max_jobs',
195 help='Run N jobs in parallel.') 325 help='Run N jobs in parallel.')
196 build_group.add_argument('-l', 326 build_group.add_argument('-l',
197 dest='max_load_average', 327 dest='max_load_average',
198 help='Do not start new jobs if the load average is ' 328 help='Do not start new jobs if the load average is '
199 'greater than N.') 329 'greater than N.')
200 build_group.add_argument('--no-goma', 330 build_group.add_argument('--no-goma',
201 action='store_false', 331 action='store_false',
202 dest='use_goma', 332 dest='use_goma',
203 default=True, 333 default=True,
204 help='Use goma when building with ninja.') 334 help='Use goma when building with ninja.')
205 build_group.add_argument('--target-os', 335 build_group.add_argument('--target-os',
206 default='android', 336 default='android',
207 choices=['android', 'linux'], 337 choices=['android', 'linux'],
208 help='target_os gn arg.') 338 help='target_os gn arg.')
209 build_group.add_argument('--output-directory', 339 build_group.add_argument('--output-directory',
210 default=_DEFAULT_OUT_DIR, 340 default=_DEFAULT_OUT_DIR,
211 help='ninja output directory.') 341 help='ninja output directory.')
212 build_group.add_argument('--enable_chrome_android_internal', 342 build_group.add_argument('--enable_chrome_android_internal',
213 action='store_true', 343 action='store_true',
214 help='Allow downstream targets to be built.') 344 help='Allow downstream targets to be built.')
215 build_group.add_argument('--target', 345 build_group.add_argument('--target',
216 default=_DEFAULT_TARGET, 346 default=_DEFAULT_TARGET,
217 help='GN APK target to build.') 347 help='GN APK target to build.')
218 args = helpers.AddCommonOptionsAndParseArgs(parser, sys.argv, pypy_warn=False) 348 args = parser.parse_args()
219 349
220 _EnsureDirectoryClean() 350 _EnsureDirectoryClean()
351 _SetInitialBranch()
352 revs = [args.rev_with_patch,
353 args.rev_without_patch or args.rev_with_patch + '^']
354 revs = [_NormalizeRev(r) for r in revs]
221 build_helper = _BuildHelper(args) 355 build_helper = _BuildHelper(args)
222 _SyncAndBuild(args.rev_with_patch, args.rev_without_patch, args.archive_dir, 356 archive_dirs = [os.path.join(args.archive_dir, '%d-%s' % (len(revs) - i, rev))
223 build_helper) 357 for i, rev in enumerate(revs)]
358 _SyncAndBuild(revs, archive_dirs, build_helper)
359 _RestoreInitialBranch()
224 360
361 output_file = os.path.join(args.archive_dir, 'diff_result.txt')
362 if os.path.exists(output_file):
agrieve 2017/04/12 00:20:50 nit: probably unnecessary to explicitly delete thi
estevenson 2017/04/12 01:39:23 Oops, what I meant to do here was delete it if it
363 os.remove(output_file)
364 diffs = []
365 if build_helper.target_os == 'android':
366 diffs += [
367 ResourceSizesDiff(archive_dirs, _ApkNameFromTarget(args.target),
368 slow_options=args.include_slow_options)
369 ]
370 for d in diffs:
371 d.PrintResults(output_file)
225 372
226 if __name__ == '__main__': 373 if __name__ == '__main__':
227 sys.exit(main()) 374 sys.exit(main())
228 375
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