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

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

Issue 2954043002: diagnose_bloat.py: Grouped diffs. (Closed)
Patch Set: Created 3 years, 6 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 binary size bloat. 6 """Tool for finding the cause of binary size bloat.
7 7
8 See //tools/binary_size/README.md for example usage. 8 See //tools/binary_size/README.md for example usage.
9 9
10 Note: this tool will perform gclient sync/git checkout on your local repo if 10 Note: this tool will perform gclient sync/git checkout on your local repo if
(...skipping 11 matching lines...) Expand all
22 import os 22 import os
23 import re 23 import re
24 import shutil 24 import shutil
25 import subprocess 25 import subprocess
26 import sys 26 import sys
27 import tempfile 27 import tempfile
28 import zipfile 28 import zipfile
29 29
30 _COMMIT_COUNT_WARN_THRESHOLD = 15 30 _COMMIT_COUNT_WARN_THRESHOLD = 15
31 _ALLOWED_CONSECUTIVE_FAILURES = 2 31 _ALLOWED_CONSECUTIVE_FAILURES = 2
32 _DIFF_DETAILS_LINES_THRESHOLD = 100
33 _SRC_ROOT = os.path.abspath( 32 _SRC_ROOT = os.path.abspath(
34 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) 33 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
35 _DEFAULT_ARCHIVE_DIR = os.path.join(_SRC_ROOT, 'out', 'binary-size-results') 34 _DEFAULT_ARCHIVE_DIR = os.path.join(_SRC_ROOT, 'out', 'binary-size-results')
36 _DEFAULT_OUT_DIR = os.path.join(_SRC_ROOT, 'out', 'binary-size-build') 35 _DEFAULT_OUT_DIR = os.path.join(_SRC_ROOT, 'out', 'binary-size-build')
37 _DEFAULT_ANDROID_TARGET = 'monochrome_public_apk' 36 _DEFAULT_ANDROID_TARGET = 'monochrome_public_apk'
38 _BINARY_SIZE_DIR = os.path.join(_SRC_ROOT, 'tools', 'binary_size') 37 _BINARY_SIZE_DIR = os.path.join(_SRC_ROOT, 'tools', 'binary_size')
39 38
40 39
41 _DiffResult = collections.namedtuple('DiffResult', ['name', 'value', 'units']) 40 _DiffResult = collections.namedtuple('DiffResult', ['name', 'value', 'units'])
42 41
43 42
44 class BaseDiff(object): 43 class BaseDiff(object):
45 """Base class capturing binary size diffs.""" 44 """Base class capturing binary size diffs."""
46 def __init__(self, name): 45 def __init__(self, name):
47 self.name = name 46 self.name = name
48 self.banner = '\n' + '*' * 30 + name + '*' * 30 47 self.banner = '\n' + '*' * 30 + name + '*' * 30
49 48
50 def AppendResults(self, logfile): 49 def AppendResults(self, logfile):
51 """Print and write diff results to an open |logfile|.""" 50 """Print and write diff results to an open |logfile|."""
52 _PrintAndWriteToFile(logfile, self.banner) 51 _PrintAndWriteToFile(logfile, self.banner)
53 _PrintAndWriteToFile(logfile, 'Summary:') 52 for s in self.Summary():
54 _PrintAndWriteToFile(logfile, self.Summary()) 53 print s
55 _PrintAndWriteToFile(logfile, '\nDetails:') 54 print
56 _PrintAndWriteToFile(logfile, self.DetailedResults()) 55 for s in self.DetailedResults():
56 logfile.write(s + '\n')
57 57
58 @property 58 @property
59 def summary_stat(self): 59 def summary_stat(self):
60 return None 60 return None
61 61
62 def Summary(self): 62 def Summary(self):
63 """A short description that summarizes the source of binary size bloat.""" 63 """A short description that summarizes the source of binary size bloat."""
64 raise NotImplementedError() 64 raise NotImplementedError()
65 65
66 def DetailedResults(self): 66 def DetailedResults(self):
67 """An iterable description of the cause of binary size bloat.""" 67 """An iterable description of the cause of binary size bloat."""
68 raise NotImplementedError() 68 raise NotImplementedError()
69 69
70 def ProduceDiff(self, before_dir, after_dir): 70 def ProduceDiff(self, before_dir, after_dir):
71 """Prepare a binary size diff with ready to print results.""" 71 """Prepare a binary size diff with ready to print results."""
72 raise NotImplementedError() 72 raise NotImplementedError()
73 73
74 def RunDiff(self, logfile, before_dir, after_dir): 74 def RunDiff(self, logfile, before_dir, after_dir):
75 logging.info('Creating: %s', self.name) 75 logging.info('Creating: %s', self.name)
76 self.ProduceDiff(before_dir, after_dir) 76 self.ProduceDiff(before_dir, after_dir)
77 self.AppendResults(logfile) 77 self.AppendResults(logfile)
78 78
79 79
80 class NativeDiff(BaseDiff): 80 class NativeDiff(BaseDiff):
81 _RE_SUMMARY = re.compile(r'Section Sizes .*?\n\n.*?(?=\n\n)', flags=re.DOTALL)
82 _RE_SUMMARY_STAT = re.compile( 81 _RE_SUMMARY_STAT = re.compile(
83 r'Section Sizes \(Total=(?P<value>\d+) (?P<units>\w+)\)') 82 r'Section Sizes \(Total=(?P<value>\d+) (?P<units>\w+)\)')
84 _SUMMARY_STAT_NAME = 'Native Library Delta' 83 _SUMMARY_STAT_NAME = 'Native Library Delta'
85 84
86 def __init__(self, size_name, supersize_path): 85 def __init__(self, size_name, supersize_path):
87 self._size_name = size_name 86 self._size_name = size_name
88 self._supersize_path = supersize_path 87 self._supersize_path = supersize_path
89 self._diff = [] 88 self._diff = []
90 super(NativeDiff, self).__init__('Native Diff') 89 super(NativeDiff, self).__init__('Native Diff')
91 90
92 @property 91 @property
93 def summary_stat(self): 92 def summary_stat(self):
94 m = NativeDiff._RE_SUMMARY_STAT.search(self._diff) 93 m = NativeDiff._RE_SUMMARY_STAT.search(self._diff)
95 if m: 94 if m:
96 return _DiffResult( 95 return _DiffResult(
97 NativeDiff._SUMMARY_STAT_NAME, m.group('value'), m.group('units')) 96 NativeDiff._SUMMARY_STAT_NAME, m.group('value'), m.group('units'))
98 return None 97 return None
99 98
100 def DetailedResults(self): 99 def DetailedResults(self):
101 return self._diff.splitlines() 100 return self._diff.splitlines()
102 101
103 def Summary(self): 102 def Summary(self):
104 return NativeDiff._RE_SUMMARY.search(self._diff).group() 103 return self.DetailedResults()[:100]
105 104
106 def ProduceDiff(self, before_dir, after_dir): 105 def ProduceDiff(self, before_dir, after_dir):
107 before_size = os.path.join(before_dir, self._size_name) 106 before_size = os.path.join(before_dir, self._size_name)
108 after_size = os.path.join(after_dir, self._size_name) 107 after_size = os.path.join(after_dir, self._size_name)
109 cmd = [self._supersize_path, 'diff', before_size, after_size] 108 cmd = [self._supersize_path, 'diff', before_size, after_size]
110 self._diff = _RunCmd(cmd)[0].replace('{', '{{').replace('}', '}}') 109 self._diff = _RunCmd(cmd)[0].replace('{', '{{').replace('}', '}}')
111 110
112 111
113 class ResourceSizesDiff(BaseDiff): 112 class ResourceSizesDiff(BaseDiff):
114 _RESOURCE_SIZES_PATH = os.path.join( 113 _RESOURCE_SIZES_PATH = os.path.join(
115 _SRC_ROOT, 'build', 'android', 'resource_sizes.py') 114 _SRC_ROOT, 'build', 'android', 'resource_sizes.py')
115 _SUMMARY_SECTIONS = ('Breakdown', 'Specifics')
116 _AGGREGATE_SECTIONS = (
117 'InstallBreakdown', 'Breakdown', 'MainLibInfo', 'Uncompressed')
116 118
117 def __init__(self, apk_name, slow_options=False): 119 def __init__(self, apk_name, slow_options=False):
118 self._apk_name = apk_name 120 self._apk_name = apk_name
119 self._slow_options = slow_options 121 self._slow_options = slow_options
120 self._diff = None # Set by |ProduceDiff()| 122 self._diff = None # Set by |ProduceDiff()|
121 super(ResourceSizesDiff, self).__init__('Resource Sizes Diff') 123 super(ResourceSizesDiff, self).__init__('Resource Sizes Diff')
122 124
123 @property 125 @property
124 def summary_stat(self): 126 def summary_stat(self):
125 for s in self._diff: 127 for section_name, results in self._diff.iteritems():
126 if 'normalized' in s.name: 128 for sub_section_name, value, units in results:
127 return s 129 if 'normalized' in sub_section_name:
130 full_name = '{} {}'.format(section_name, sub_section_name)
131 return _DiffResult(full_name, value, units)
128 return None 132 return None
129 133
130 def DetailedResults(self): 134 def DetailedResults(self):
131 return ['{:>+10,} {} {}'.format(value, units, name) 135 return self._ResultLines()
132 for name, value, units in self._diff]
133 136
134 def Summary(self): 137 def Summary(self):
135 return 'Normalized APK size: {:+,} {}'.format( 138 return self._ResultLines(
136 self.summary_stat.value, self.summary_stat.units) 139 include_sections=ResourceSizesDiff._SUMMARY_SECTIONS)
137 140
138 def ProduceDiff(self, before_dir, after_dir): 141 def ProduceDiff(self, before_dir, after_dir):
139 before = self._RunResourceSizes(before_dir) 142 before = self._RunResourceSizes(before_dir)
140 after = self._RunResourceSizes(after_dir) 143 after = self._RunResourceSizes(after_dir)
141 diff = [] 144 self._diff = collections.defaultdict(list)
142 for section, section_dict in after.iteritems(): 145 for section, section_dict in after.iteritems():
143 for subsection, v in section_dict.iteritems(): 146 for subsection, v in section_dict.iteritems():
144 # Ignore entries when resource_sizes.py chartjson format has changed. 147 # Ignore entries when resource_sizes.py chartjson format has changed.
145 if (section not in before or 148 if (section not in before or
146 subsection not in before[section] or 149 subsection not in before[section] or
147 v['units'] != before[section][subsection]['units']): 150 v['units'] != before[section][subsection]['units']):
148 logging.warning( 151 logging.warning(
149 'Found differing dict structures for resource_sizes.py, ' 152 'Found differing dict structures for resource_sizes.py, '
150 'skipping %s %s', section, subsection) 153 'skipping %s %s', section, subsection)
151 else: 154 else:
152 diff.append( 155 self._diff[section].append(_DiffResult(
153 _DiffResult( 156 subsection,
154 '%s %s' % (section, subsection), 157 v['value'] - before[section][subsection]['value'],
155 v['value'] - before[section][subsection]['value'], 158 v['units']))
156 v['units'])) 159
157 self._diff = sorted(diff, key=lambda x: abs(x.value), reverse=True) 160 def _ResultLines(self, include_sections=None):
161 ret = []
162 for section_name, section_results in self._diff.iteritems():
163 section_no_target = re.sub(r'^.*_', '', section_name)
164 if not include_sections or section_no_target in include_sections:
165 sub_section_lines = []
166 section_sum = 0
167 units = ''
168 for name, value, units in section_results:
169 if value == 0 and include_sections:
170 continue
171 section_sum += value
172 sub_section_lines.append('{:>+10,} {} {}'.format(value, units, name))
173 section_header = section_name
174 if section_no_target in ResourceSizesDiff._AGGREGATE_SECTIONS:
175 section_header += ' ({:+,} {})'.format(section_sum, units)
176 if sub_section_lines:
177 ret.append(section_header)
178 ret.extend(sub_section_lines)
179 if not ret:
180 ret = ['Empty ' + self.name]
181 return ret
158 182
159 def _RunResourceSizes(self, archive_dir): 183 def _RunResourceSizes(self, archive_dir):
160 apk_path = os.path.join(archive_dir, self._apk_name) 184 apk_path = os.path.join(archive_dir, self._apk_name)
161 chartjson_file = os.path.join(archive_dir, 'results-chart.json') 185 chartjson_file = os.path.join(archive_dir, 'results-chart.json')
162 cmd = [self._RESOURCE_SIZES_PATH, apk_path,'--output-dir', archive_dir, 186 cmd = [self._RESOURCE_SIZES_PATH, apk_path,'--output-dir', archive_dir,
163 '--no-output-dir', '--chartjson'] 187 '--no-output-dir', '--chartjson']
164 if self._slow_options: 188 if self._slow_options:
165 cmd += ['--estimate-patch-size', '--dump-static-initializers'] 189 cmd += ['--estimate-patch-size', '--dump-static-initializers']
166 _RunCmd(cmd) 190 _RunCmd(cmd)
167 with open(chartjson_file) as f: 191 with open(chartjson_file) as f:
(...skipping 487 matching lines...) Expand 10 before | Expand all | Expand 10 after
655 return os.path.join(dst, output_dir) 679 return os.path.join(dst, output_dir)
656 680
657 681
658 def _PrintAndWriteToFile(logfile, s, *args, **kwargs): 682 def _PrintAndWriteToFile(logfile, s, *args, **kwargs):
659 """Write and print |s| thottling output if |s| is a large list.""" 683 """Write and print |s| thottling output if |s| is a large list."""
660 if isinstance(s, basestring): 684 if isinstance(s, basestring):
661 s = s.format(*args, **kwargs) 685 s = s.format(*args, **kwargs)
662 print s 686 print s
663 logfile.write('%s\n' % s) 687 logfile.write('%s\n' % s)
664 else: 688 else:
665 for l in s[:_DIFF_DETAILS_LINES_THRESHOLD]: 689 for l in s:
666 print l 690 print l
667 if len(s) > _DIFF_DETAILS_LINES_THRESHOLD:
668 print '\nOutput truncated, see %s for more.' % logfile.name
669 logfile.write('\n'.join(s)) 691 logfile.write('\n'.join(s))
agrieve 2017/06/23 02:40:30 nit: might be simpler to do: if isinstance(s, bas
estevenson 2017/06/23 17:23:27 Done.
670 692
671 693
672 @contextmanager 694 @contextmanager
673 def _TmpCopyBinarySizeDir(): 695 def _TmpCopyBinarySizeDir():
674 """Recursively copy files to a temp dir and yield supersize path.""" 696 """Recursively copy files to a temp dir and yield supersize path."""
675 # Needs to be at same level of nesting as the real //tools/binary_size 697 # Needs to be at same level of nesting as the real //tools/binary_size
676 # since supersize uses this to find d3 in //third_party. 698 # since supersize uses this to find d3 in //third_party.
677 tmp_dir = tempfile.mkdtemp(dir=_SRC_ROOT) 699 tmp_dir = tempfile.mkdtemp(dir=_SRC_ROOT)
678 try: 700 try:
679 bs_dir = os.path.join(tmp_dir, 'binary_size') 701 bs_dir = os.path.join(tmp_dir, 'binary_size')
(...skipping 139 matching lines...) Expand 10 before | Expand all | Expand 10 after
819 841
820 if i != 0: 842 if i != 0:
821 diff_mngr.MaybeDiff(i - 1, i) 843 diff_mngr.MaybeDiff(i - 1, i)
822 844
823 diff_mngr.Summarize() 845 diff_mngr.Summarize()
824 846
825 847
826 if __name__ == '__main__': 848 if __name__ == '__main__':
827 sys.exit(main()) 849 sys.exit(main())
828 850
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