| 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 |
| (...skipping 26 matching lines...) Expand all Loading... |
| 37 | 37 |
| 38 _global_restore_checkout_func = None | 38 _global_restore_checkout_func = None |
| 39 | 39 |
| 40 | 40 |
| 41 def _SetRestoreFunc(subrepo): | 41 def _SetRestoreFunc(subrepo): |
| 42 branch = _GitCmd(['rev-parse', '--abbrev-ref', 'HEAD'], subrepo) | 42 branch = _GitCmd(['rev-parse', '--abbrev-ref', 'HEAD'], subrepo) |
| 43 global _global_restore_checkout_func | 43 global _global_restore_checkout_func |
| 44 _global_restore_checkout_func = lambda: _GitCmd(['checkout', branch], subrepo) | 44 _global_restore_checkout_func = lambda: _GitCmd(['checkout', branch], subrepo) |
| 45 | 45 |
| 46 | 46 |
| 47 _DiffResult = collections.namedtuple( |
| 48 'DiffResult', ['name', 'value', 'units']) |
| 49 |
| 50 |
| 47 class BaseDiff(object): | 51 class BaseDiff(object): |
| 48 """Base class capturing binary size diffs.""" | 52 """Base class capturing binary size diffs.""" |
| 49 def __init__(self, name): | 53 def __init__(self, name): |
| 50 self.name = name | 54 self.name = name |
| 51 self.banner = '\n' + '*' * 30 + name + '*' * 30 | 55 self.banner = '\n' + '*' * 30 + name + '*' * 30 |
| 52 | 56 |
| 53 def AppendResults(self, logfile): | 57 def AppendResults(self, logfile): |
| 54 """Print and write diff results to an open |logfile|.""" | 58 """Print and write diff results to an open |logfile|.""" |
| 55 _PrintAndWriteToFile(logfile, self.banner) | 59 _PrintAndWriteToFile(logfile, self.banner) |
| 56 _PrintAndWriteToFile(logfile, 'Summary:') | 60 _PrintAndWriteToFile(logfile, 'Summary:') |
| 57 _PrintAndWriteToFile(logfile, self.Summary()) | 61 _PrintAndWriteToFile(logfile, self.Summary()) |
| 58 _PrintAndWriteToFile(logfile, '\nDetails:') | 62 _PrintAndWriteToFile(logfile, '\nDetails:') |
| 59 _PrintAndWriteToFile(logfile, self.DetailedResults()) | 63 _PrintAndWriteToFile(logfile, self.DetailedResults()) |
| 60 | 64 |
| 65 @property |
| 66 def summary_stat(self): |
| 67 return None |
| 68 |
| 61 def Summary(self): | 69 def Summary(self): |
| 62 """A short description that summarizes the source of binary size bloat.""" | 70 """A short description that summarizes the source of binary size bloat.""" |
| 63 raise NotImplementedError() | 71 raise NotImplementedError() |
| 64 | 72 |
| 65 def DetailedResults(self): | 73 def DetailedResults(self): |
| 66 """An iterable description of the cause of binary size bloat.""" | 74 """An iterable description of the cause of binary size bloat.""" |
| 67 raise NotImplementedError() | 75 raise NotImplementedError() |
| 68 | 76 |
| 69 def ProduceDiff(self, archive_dirs): | 77 def ProduceDiff(self, archive_dirs): |
| 70 """Prepare a binary size diff with ready to print results.""" | 78 """Prepare a binary size diff with ready to print results.""" |
| 71 raise NotImplementedError() | 79 raise NotImplementedError() |
| 72 | 80 |
| 73 def RunDiff(self, logfile, archive_dirs): | 81 def RunDiff(self, logfile, archive_dirs): |
| 74 self.ProduceDiff(archive_dirs) | 82 self.ProduceDiff(archive_dirs) |
| 75 self.AppendResults(logfile) | 83 self.AppendResults(logfile) |
| 76 | 84 |
| 77 | 85 |
| 78 class NativeDiff(BaseDiff): | 86 class NativeDiff(BaseDiff): |
| 79 _RE_SUMMARY = re.compile( | 87 _RE_SUMMARY = re.compile( |
| 80 r'.*(Section Sizes .*? object files added, \d+ removed).*', | 88 r'.*(Section Sizes .*? object files added, \d+ removed).*', |
| 81 flags=re.DOTALL) | 89 flags=re.DOTALL) |
| 90 _RE_SUMMARY_STAT = re.compile( |
| 91 r'Section Sizes \(Total=(?P<value>\d+) (?P<units>\w+)\)') |
| 92 _SUMMARY_STAT_NAME = 'Native Library Delta' |
| 82 | 93 |
| 83 def __init__(self, size_name, supersize_path): | 94 def __init__(self, size_name, supersize_path): |
| 84 self._size_name = size_name | 95 self._size_name = size_name |
| 85 self._supersize_path = supersize_path | 96 self._supersize_path = supersize_path |
| 86 self._diff = [] | 97 self._diff = [] |
| 87 super(NativeDiff, self).__init__('Native Diff') | 98 super(NativeDiff, self).__init__('Native Diff') |
| 88 | 99 |
| 100 @property |
| 101 def summary_stat(self): |
| 102 m = NativeDiff._RE_SUMMARY_STAT.search(self._diff) |
| 103 if m: |
| 104 return _DiffResult( |
| 105 NativeDiff._SUMMARY_STAT_NAME, m.group('value'), m.group('units')) |
| 106 return None |
| 107 |
| 89 def DetailedResults(self): | 108 def DetailedResults(self): |
| 90 return self._diff.splitlines() | 109 return self._diff.splitlines() |
| 91 | 110 |
| 92 def Summary(self): | 111 def Summary(self): |
| 93 return NativeDiff._RE_SUMMARY.match(self._diff).group(1) | 112 return NativeDiff._RE_SUMMARY.match(self._diff).group(1) |
| 94 | 113 |
| 95 def ProduceDiff(self, archive_dirs): | 114 def ProduceDiff(self, archive_dirs): |
| 96 size_files = [os.path.join(a, self._size_name) | 115 size_files = [os.path.join(a, self._size_name) |
| 97 for a in reversed(archive_dirs)] | 116 for a in reversed(archive_dirs)] |
| 98 cmd = [self._supersize_path, 'diff'] + size_files | 117 cmd = [self._supersize_path, 'diff'] + size_files |
| 99 self._diff = _RunCmd(cmd)[0].replace('{', '{{').replace('}', '}}') | 118 self._diff = _RunCmd(cmd)[0].replace('{', '{{').replace('}', '}}') |
| 100 | 119 |
| 101 | 120 |
| 102 _ResourceSizesDiffResult = collections.namedtuple( | |
| 103 'ResourceSizesDiffResult', ['section', 'value', 'units']) | |
| 104 | |
| 105 | |
| 106 class ResourceSizesDiff(BaseDiff): | 121 class ResourceSizesDiff(BaseDiff): |
| 107 _RESOURCE_SIZES_PATH = os.path.join( | 122 _RESOURCE_SIZES_PATH = os.path.join( |
| 108 _SRC_ROOT, 'build', 'android', 'resource_sizes.py') | 123 _SRC_ROOT, 'build', 'android', 'resource_sizes.py') |
| 109 | 124 |
| 110 def __init__(self, apk_name, slow_options=False): | 125 def __init__(self, apk_name, slow_options=False): |
| 111 self._apk_name = apk_name | 126 self._apk_name = apk_name |
| 112 self._slow_options = slow_options | 127 self._slow_options = slow_options |
| 113 self._diff = None # Set by |ProduceDiff()| | 128 self._diff = None # Set by |ProduceDiff()| |
| 114 super(ResourceSizesDiff, self).__init__('Resource Sizes Diff') | 129 super(ResourceSizesDiff, self).__init__('Resource Sizes Diff') |
| 115 | 130 |
| 131 @property |
| 132 def summary_stat(self): |
| 133 for s in self._diff: |
| 134 if 'normalized' in s.name: |
| 135 return s |
| 136 return None |
| 137 |
| 116 def DetailedResults(self): | 138 def DetailedResults(self): |
| 117 return ['{:>+10,} {} {}'.format(value, units, section) | 139 return ['{:>+10,} {} {}'.format(value, units, name) |
| 118 for section, value, units in self._diff] | 140 for name, value, units in self._diff] |
| 119 | 141 |
| 120 def Summary(self): | 142 def Summary(self): |
| 121 for s in self._diff: | 143 return 'Normalized APK size: {:+,} {}'.format( |
| 122 if 'normalized' in s.section: | 144 self.summary_stat.value, self.summary_stat.units) |
| 123 return 'Normalized APK size: {:+,} {}'.format(s.value, s.units) | |
| 124 return '' | |
| 125 | 145 |
| 126 def ProduceDiff(self, archive_dirs): | 146 def ProduceDiff(self, archive_dirs): |
| 127 chartjsons = self._RunResourceSizes(archive_dirs) | 147 chartjsons = self._RunResourceSizes(archive_dirs) |
| 128 diff = [] | 148 diff = [] |
| 129 with_patch = chartjsons[0]['charts'] | 149 with_patch = chartjsons[0]['charts'] |
| 130 without_patch = chartjsons[1]['charts'] | 150 without_patch = chartjsons[1]['charts'] |
| 131 for section, section_dict in with_patch.iteritems(): | 151 for section, section_dict in with_patch.iteritems(): |
| 132 for subsection, v in section_dict.iteritems(): | 152 for subsection, v in section_dict.iteritems(): |
| 133 # Ignore entries when resource_sizes.py chartjson format has changed. | 153 # Ignore entries when resource_sizes.py chartjson format has changed. |
| 134 if (section not in without_patch or | 154 if (section not in without_patch or |
| 135 subsection not in without_patch[section] or | 155 subsection not in without_patch[section] or |
| 136 v['units'] != without_patch[section][subsection]['units']): | 156 v['units'] != without_patch[section][subsection]['units']): |
| 137 _Print('Found differing dict structures for resource_sizes.py, ' | 157 _Print('Found differing dict structures for resource_sizes.py, ' |
| 138 'skipping {} {}', section, subsection) | 158 'skipping {} {}', section, subsection) |
| 139 else: | 159 else: |
| 140 diff.append( | 160 diff.append( |
| 141 _ResourceSizesDiffResult( | 161 _DiffResult( |
| 142 '%s %s' % (section, subsection), | 162 '%s %s' % (section, subsection), |
| 143 v['value'] - without_patch[section][subsection]['value'], | 163 v['value'] - without_patch[section][subsection]['value'], |
| 144 v['units'])) | 164 v['units'])) |
| 145 self._diff = sorted(diff, key=lambda x: abs(x.value), reverse=True) | 165 self._diff = sorted(diff, key=lambda x: abs(x.value), reverse=True) |
| 146 | 166 |
| 147 def _RunResourceSizes(self, archive_dirs): | 167 def _RunResourceSizes(self, archive_dirs): |
| 148 chartjsons = [] | 168 chartjsons = [] |
| 149 for archive_dir in archive_dirs: | 169 for archive_dir in archive_dirs: |
| 150 apk_path = os.path.join(archive_dir, self._apk_name) | 170 apk_path = os.path.join(archive_dir, self._apk_name) |
| 151 chartjson_file = os.path.join(archive_dir, 'results-chart.json') | 171 chartjson_file = os.path.join(archive_dir, 'results-chart.json') |
| (...skipping 148 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 300 | 320 |
| 301 class _DiffArchiveManager(object): | 321 class _DiffArchiveManager(object): |
| 302 """Class for maintaining BuildArchives and their related diff artifacts.""" | 322 """Class for maintaining BuildArchives and their related diff artifacts.""" |
| 303 def __init__(self, revs, archive_dir, diffs, build, subrepo): | 323 def __init__(self, revs, archive_dir, diffs, build, subrepo): |
| 304 self.archive_dir = archive_dir | 324 self.archive_dir = archive_dir |
| 305 self.build = build | 325 self.build = build |
| 306 self.build_archives = [_BuildArchive(rev, archive_dir, build, subrepo) | 326 self.build_archives = [_BuildArchive(rev, archive_dir, build, subrepo) |
| 307 for rev in revs] | 327 for rev in revs] |
| 308 self.diffs = diffs | 328 self.diffs = diffs |
| 309 self.subrepo = subrepo | 329 self.subrepo = subrepo |
| 330 self._summary_stats = [] |
| 310 | 331 |
| 311 def IterArchives(self): | 332 def IterArchives(self): |
| 312 return iter(self.build_archives) | 333 return iter(self.build_archives) |
| 313 | 334 |
| 314 def MaybeDiff(self, first_id, second_id): | 335 def MaybeDiff(self, first_id, second_id): |
| 315 """Perform diffs given two build archives.""" | 336 """Perform diffs given two build archives.""" |
| 316 archives = [ | 337 archives = [ |
| 317 self.build_archives[first_id], self.build_archives[second_id]] | 338 self.build_archives[first_id], self.build_archives[second_id]] |
| 318 diff_path = self._DiffFilePath(archives) | 339 diff_path = self._DiffFilePath(archives) |
| 319 if not self._CanDiff(archives): | 340 if not self._CanDiff(archives): |
| 320 _Print('Skipping diff for {} due to missing build archives.', diff_path) | 341 _Print('Skipping diff for {} due to missing build archives.', diff_path) |
| 321 return | 342 return |
| 322 | 343 |
| 323 metadata_path = self._DiffMetadataPath(archives) | 344 metadata_path = self._DiffMetadataPath(archives) |
| 324 metadata = _GenerateMetadata( | 345 metadata = _GenerateMetadata( |
| 325 archives, self.build, metadata_path, self.subrepo) | 346 archives, self.build, metadata_path, self.subrepo) |
| 326 if _MetadataExists(metadata): | 347 if _MetadataExists(metadata): |
| 327 _Print('Skipping diff for {} and {}. Matching diff already exists: {}', | 348 _Print('Skipping diff for {} and {}. Matching diff already exists: {}', |
| 328 archives[0].rev, archives[1].rev, diff_path) | 349 archives[0].rev, archives[1].rev, diff_path) |
| 329 else: | 350 else: |
| 330 if os.path.exists(diff_path): | 351 if os.path.exists(diff_path): |
| 331 os.remove(diff_path) | 352 os.remove(diff_path) |
| 332 archive_dirs = [archives[0].dir, archives[1].dir] | 353 archive_dirs = [archives[0].dir, archives[1].dir] |
| 333 with open(diff_path, 'a') as diff_file: | 354 with open(diff_path, 'a') as diff_file: |
| 334 for d in self.diffs: | 355 for d in self.diffs: |
| 335 d.RunDiff(diff_file, archive_dirs) | 356 d.RunDiff(diff_file, archive_dirs) |
| 336 _Print('See detailed diff results here: {}.', diff_path) | 357 _Print('\nSee detailed diff results here: {}.', diff_path) |
| 337 _WriteMetadata(metadata) | 358 _WriteMetadata(metadata) |
| 359 self._AddDiffSummaryStat(archives) |
| 360 |
| 361 def Summarize(self): |
| 362 if self._summary_stats: |
| 363 path = os.path.join(self.archive_dir, 'last_diff_summary.txt') |
| 364 with open(path, 'w') as f: |
| 365 stats = sorted( |
| 366 self._summary_stats, key=lambda x: x[0].value, reverse=True) |
| 367 _PrintAndWriteToFile(f, '\nDiff Summary') |
| 368 for s, before, after in stats: |
| 369 _PrintAndWriteToFile(f, '{:>+10} {} {} for range: {}..{}', |
| 370 s.value, s.units, s.name, before, after) |
| 371 |
| 372 def _AddDiffSummaryStat(self, archives): |
| 373 stat = None |
| 374 if self.build.IsAndroid(): |
| 375 summary_diff_type = ResourceSizesDiff |
| 376 else: |
| 377 summary_diff_type = NativeDiff |
| 378 for d in self.diffs: |
| 379 if isinstance(d, summary_diff_type): |
| 380 stat = d.summary_stat |
| 381 if stat: |
| 382 self._summary_stats.append((stat, archives[1].rev, archives[0].rev)) |
| 338 | 383 |
| 339 def _CanDiff(self, archives): | 384 def _CanDiff(self, archives): |
| 340 return all(a.Exists() for a in archives) | 385 return all(a.Exists() for a in archives) |
| 341 | 386 |
| 342 def _DiffFilePath(self, archives): | 387 def _DiffFilePath(self, archives): |
| 343 return os.path.join(self._DiffDir(archives), 'diff_results.txt') | 388 return os.path.join(self._DiffDir(archives), 'diff_results.txt') |
| 344 | 389 |
| 345 def _DiffMetadataPath(self, archives): | 390 def _DiffMetadataPath(self, archives): |
| 346 return os.path.join(self._DiffDir(archives), 'metadata.txt') | 391 return os.path.join(self._DiffDir(archives), 'metadata.txt') |
| 347 | 392 |
| 348 def _DiffDir(self, archives): | 393 def _DiffDir(self, archives): |
| 349 diff_path = os.path.join( | 394 archive_range = '%s..%s' % (archives[1].rev, archives[0].rev) |
| 350 self.archive_dir, 'diffs', '_'.join(a.rev for a in archives)) | 395 diff_path = os.path.join(self.archive_dir, 'diffs', archive_range) |
| 351 _EnsureDirsExist(diff_path) | 396 _EnsureDirsExist(diff_path) |
| 352 return diff_path | 397 return diff_path |
| 353 | 398 |
| 354 | 399 |
| 355 def _EnsureDirsExist(path): | 400 def _EnsureDirsExist(path): |
| 356 if not os.path.exists(path): | 401 if not os.path.exists(path): |
| 357 os.makedirs(path) | 402 os.makedirs(path) |
| 358 | 403 |
| 359 | 404 |
| 360 def _GenerateMetadata(archives, build, path, subrepo): | 405 def _GenerateMetadata(archives, build, path, subrepo): |
| (...skipping 201 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 562 'Storage bucket folder structure doesn\'t start with %s' % prefix) | 607 'Storage bucket folder structure doesn\'t start with %s' % prefix) |
| 563 to_extract = [os.path.join(prefix, f) for f in to_extract] | 608 to_extract = [os.path.join(prefix, f) for f in to_extract] |
| 564 for f in to_extract: | 609 for f in to_extract: |
| 565 z.extract(f, path=dst) | 610 z.extract(f, path=dst) |
| 566 | 611 |
| 567 | 612 |
| 568 def _Print(s, *args, **kwargs): | 613 def _Print(s, *args, **kwargs): |
| 569 print s.format(*args, **kwargs) | 614 print s.format(*args, **kwargs) |
| 570 | 615 |
| 571 | 616 |
| 572 def _PrintAndWriteToFile(logfile, s): | 617 def _PrintAndWriteToFile(logfile, s, *args, **kwargs): |
| 573 """Write and print |s| thottling output if |s| is a large list.""" | 618 """Write and print |s| thottling output if |s| is a large list.""" |
| 574 if isinstance(s, basestring): | 619 if isinstance(s, basestring): |
| 620 s = s.format(*args, **kwargs) |
| 575 _Print(s) | 621 _Print(s) |
| 576 logfile.write('%s\n' % s) | 622 logfile.write('%s\n' % s) |
| 577 else: | 623 else: |
| 578 for l in s[:_DIFF_DETAILS_LINES_THRESHOLD]: | 624 for l in s[:_DIFF_DETAILS_LINES_THRESHOLD]: |
| 579 _Print(l) | 625 _Print(l) |
| 580 if len(s) > _DIFF_DETAILS_LINES_THRESHOLD: | 626 if len(s) > _DIFF_DETAILS_LINES_THRESHOLD: |
| 581 _Print('\nOutput truncated, see {} for more.', logfile.name) | 627 _Print('\nOutput truncated, see {} for more.', logfile.name) |
| 582 logfile.write('\n'.join(s)) | 628 logfile.write('\n'.join(s)) |
| 583 | 629 |
| 584 | 630 |
| (...skipping 115 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 700 if consecutive_failures > _ALLOWED_CONSECUTIVE_FAILURES: | 746 if consecutive_failures > _ALLOWED_CONSECUTIVE_FAILURES: |
| 701 _Die('{} builds failed in a row, last failure was {}.', | 747 _Die('{} builds failed in a row, last failure was {}.', |
| 702 consecutive_failures, archive.rev) | 748 consecutive_failures, archive.rev) |
| 703 else: | 749 else: |
| 704 archive.ArchiveBuildResults(supersize_path) | 750 archive.ArchiveBuildResults(supersize_path) |
| 705 consecutive_failures = 0 | 751 consecutive_failures = 0 |
| 706 | 752 |
| 707 if i != 0: | 753 if i != 0: |
| 708 diff_mngr.MaybeDiff(i - 1, i) | 754 diff_mngr.MaybeDiff(i - 1, i) |
| 709 | 755 |
| 756 diff_mngr.Summarize() |
| 757 |
| 710 _global_restore_checkout_func() | 758 _global_restore_checkout_func() |
| 711 | 759 |
| 712 if __name__ == '__main__': | 760 if __name__ == '__main__': |
| 713 sys.exit(main()) | 761 sys.exit(main()) |
| 714 | 762 |
| OLD | NEW |