Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2017 The Chromium Authors. All rights reserved. | 2 # Copyright 2017 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Tool for finding the cause of APK bloat. | 6 """Tool for finding the cause of APK bloat. |
| 7 | 7 |
| 8 Run diagnose_apk_bloat.py -h for detailed usage help. | 8 Run diagnose_apk_bloat.py -h for detailed usage help. |
| 9 """ | 9 """ |
| 10 | 10 |
| 11 import argparse | 11 import argparse |
| 12 import 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 Loading... | |
| 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 |
| OLD | NEW |