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 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 Loading... | |
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 Loading... | |
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 Loading... | |
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 |
OLD | NEW |