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