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

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

Powered by Google App Engine
This is Rietveld 408576698