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

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

Issue 2954043002: diagnose_bloat.py: Grouped diffs. (Closed)
Patch Set: review comments Created 3 years, 5 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 # Sections where it makes sense to sum subsections into a section total.
117 _AGGREGATE_SECTIONS = (
118 'InstallBreakdown', 'Breakdown', 'MainLibInfo', 'Uncompressed')
116 119
117 def __init__(self, apk_name, slow_options=False): 120 def __init__(self, apk_name, slow_options=False):
118 self._apk_name = apk_name 121 self._apk_name = apk_name
119 self._slow_options = slow_options 122 self._slow_options = slow_options
120 self._diff = None # Set by |ProduceDiff()| 123 self._diff = None # Set by |ProduceDiff()|
121 super(ResourceSizesDiff, self).__init__('Resource Sizes Diff') 124 super(ResourceSizesDiff, self).__init__('Resource Sizes Diff')
122 125
123 @property 126 @property
124 def summary_stat(self): 127 def summary_stat(self):
125 for s in self._diff: 128 for section_name, results in self._diff.iteritems():
126 if 'normalized' in s.name: 129 for subsection_name, value, units in results:
127 return s 130 if 'normalized' in subsection_name:
131 full_name = '{} {}'.format(section_name, subsection_name)
132 return _DiffResult(full_name, value, units)
128 return None 133 return None
129 134
130 def DetailedResults(self): 135 def DetailedResults(self):
131 return ['{:>+10,} {} {}'.format(value, units, name) 136 return self._ResultLines()
132 for name, value, units in self._diff]
133 137
134 def Summary(self): 138 def Summary(self):
135 return 'Normalized APK size: {:+,} {}'.format( 139 return self._ResultLines(
136 self.summary_stat.value, self.summary_stat.units) 140 include_sections=ResourceSizesDiff._SUMMARY_SECTIONS)
137 141
138 def ProduceDiff(self, before_dir, after_dir): 142 def ProduceDiff(self, before_dir, after_dir):
139 before = self._RunResourceSizes(before_dir) 143 before = self._RunResourceSizes(before_dir)
140 after = self._RunResourceSizes(after_dir) 144 after = self._RunResourceSizes(after_dir)
141 diff = [] 145 self._diff = collections.defaultdict(list)
142 for section, section_dict in after.iteritems(): 146 for section, section_dict in after.iteritems():
143 for subsection, v in section_dict.iteritems(): 147 for subsection, v in section_dict.iteritems():
144 # Ignore entries when resource_sizes.py chartjson format has changed. 148 # Ignore entries when resource_sizes.py chartjson format has changed.
145 if (section not in before or 149 if (section not in before or
146 subsection not in before[section] or 150 subsection not in before[section] or
147 v['units'] != before[section][subsection]['units']): 151 v['units'] != before[section][subsection]['units']):
148 logging.warning( 152 logging.warning(
149 'Found differing dict structures for resource_sizes.py, ' 153 'Found differing dict structures for resource_sizes.py, '
150 'skipping %s %s', section, subsection) 154 'skipping %s %s', section, subsection)
151 else: 155 else:
152 diff.append( 156 self._diff[section].append(_DiffResult(
153 _DiffResult( 157 subsection,
154 '%s %s' % (section, subsection), 158 v['value'] - before[section][subsection]['value'],
155 v['value'] - before[section][subsection]['value'], 159 v['units']))
156 v['units'])) 160
157 self._diff = sorted(diff, key=lambda x: abs(x.value), reverse=True) 161 def _ResultLines(self, include_sections=None):
162 """Generates diff lines for the specified sections (defaults to all)."""
163 ret = []
164 for section_name, section_results in self._diff.iteritems():
165 section_no_target = re.sub(r'^.*_', '', section_name)
166 if not include_sections or section_no_target in include_sections:
167 subsection_lines = []
168 section_sum = 0
169 units = ''
170 for name, value, units in section_results:
171 # Omit subsections with no changes for summaries.
172 if value == 0 and include_sections:
173 continue
174 section_sum += value
175 subsection_lines.append('{:>+10,} {} {}'.format(value, units, name))
176 section_header = section_name
177 if section_no_target in ResourceSizesDiff._AGGREGATE_SECTIONS:
178 section_header += ' ({:+,} {})'.format(section_sum, units)
179 # Omit sections with empty subsections.
180 if subsection_lines:
181 ret.append(section_header)
182 ret.extend(subsection_lines)
183 if not ret:
184 ret = ['Empty ' + self.name]
185 return ret
158 186
159 def _RunResourceSizes(self, archive_dir): 187 def _RunResourceSizes(self, archive_dir):
160 apk_path = os.path.join(archive_dir, self._apk_name) 188 apk_path = os.path.join(archive_dir, self._apk_name)
161 chartjson_file = os.path.join(archive_dir, 'results-chart.json') 189 chartjson_file = os.path.join(archive_dir, 'results-chart.json')
162 cmd = [self._RESOURCE_SIZES_PATH, apk_path,'--output-dir', archive_dir, 190 cmd = [self._RESOURCE_SIZES_PATH, apk_path,'--output-dir', archive_dir,
163 '--no-output-dir', '--chartjson'] 191 '--no-output-dir', '--chartjson']
164 if self._slow_options: 192 if self._slow_options:
165 cmd += ['--estimate-patch-size', '--dump-static-initializers'] 193 cmd += ['--estimate-patch-size', '--dump-static-initializers']
166 _RunCmd(cmd) 194 _RunCmd(cmd)
167 with open(chartjson_file) as f: 195 with open(chartjson_file) as f:
(...skipping 481 matching lines...) Expand 10 before | Expand all | Expand 10 after
649 zipped_paths = z.namelist() 677 zipped_paths = z.namelist()
650 output_dir = os.path.commonprefix(zipped_paths) 678 output_dir = os.path.commonprefix(zipped_paths)
651 for f in to_extract: 679 for f in to_extract:
652 path = os.path.join(output_dir, f) 680 path = os.path.join(output_dir, f)
653 if path in zipped_paths: 681 if path in zipped_paths:
654 z.extract(path, path=dst) 682 z.extract(path, path=dst)
655 return os.path.join(dst, output_dir) 683 return os.path.join(dst, output_dir)
656 684
657 685
658 def _PrintAndWriteToFile(logfile, s, *args, **kwargs): 686 def _PrintAndWriteToFile(logfile, s, *args, **kwargs):
659 """Write and print |s| thottling output if |s| is a large list."""
660 if isinstance(s, basestring): 687 if isinstance(s, basestring):
661 s = s.format(*args, **kwargs) 688 data = s.format(*args, **kwargs) + '\n'
662 print s
663 logfile.write('%s\n' % s)
664 else: 689 else:
665 for l in s[:_DIFF_DETAILS_LINES_THRESHOLD]: 690 data = '\n'.join(s) + '\n'
666 print l 691 sys.stdout.write(data)
667 if len(s) > _DIFF_DETAILS_LINES_THRESHOLD: 692 logfile.write(data)
668 print '\nOutput truncated, see %s for more.' % logfile.name
669 logfile.write('\n'.join(s))
670 693
671 694
672 @contextmanager 695 @contextmanager
673 def _TmpCopyBinarySizeDir(): 696 def _TmpCopyBinarySizeDir():
674 """Recursively copy files to a temp dir and yield supersize path.""" 697 """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 698 # 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. 699 # since supersize uses this to find d3 in //third_party.
677 tmp_dir = tempfile.mkdtemp(dir=_SRC_ROOT) 700 tmp_dir = tempfile.mkdtemp(dir=_SRC_ROOT)
678 try: 701 try:
679 bs_dir = os.path.join(tmp_dir, 'binary_size') 702 bs_dir = os.path.join(tmp_dir, 'binary_size')
(...skipping 139 matching lines...) Expand 10 before | Expand all | Expand 10 after
819 842
820 if i != 0: 843 if i != 0:
821 diff_mngr.MaybeDiff(i - 1, i) 844 diff_mngr.MaybeDiff(i - 1, i)
822 845
823 diff_mngr.Summarize() 846 diff_mngr.Summarize()
824 847
825 848
826 if __name__ == '__main__': 849 if __name__ == '__main__':
827 sys.exit(main()) 850 sys.exit(main())
828 851
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