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

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

Issue 2837303002: diagnose_apk_bloat.py: add diff summary. (Closed)
Patch Set: Created 3 years, 7 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 | 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
(...skipping 26 matching lines...) Expand all
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
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
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
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
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698