Chromium Code Reviews| 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 |