| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2014 The Chromium Authors. All rights reserved. | 2 # Copyright 2014 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 """Describe the size difference of two binaries. | 6 """Describe the size difference of two binaries. |
| 7 | 7 |
| 8 Generates a description of the size difference of two binaries based | 8 Generates a description of the size difference of two binaries based |
| 9 on the difference of the size of various symbols. | 9 on the difference of the size of various symbols. |
| 10 | 10 |
| (...skipping 19 matching lines...) Expand all Loading... |
| 30 --destdir /tmp/throwaway | 30 --destdir /tmp/throwaway |
| 31 --nm-out /tmp/nm2.dump | 31 --nm-out /tmp/nm2.dump |
| 32 | 32 |
| 33 # cleanup useless files | 33 # cleanup useless files |
| 34 rm -r /tmp/throwaway | 34 rm -r /tmp/throwaway |
| 35 | 35 |
| 36 # run this tool | 36 # run this tool |
| 37 explain_binary_size_delta.py --nm1 /tmp/nm1.dump --nm2 /tmp/nm2.dump | 37 explain_binary_size_delta.py --nm1 /tmp/nm1.dump --nm2 /tmp/nm2.dump |
| 38 """ | 38 """ |
| 39 | 39 |
| 40 import operator |
| 40 import optparse | 41 import optparse |
| 41 import os | 42 import os |
| 42 import sys | 43 import sys |
| 43 | 44 |
| 44 import binary_size_utils | 45 import binary_size_utils |
| 45 | 46 |
| 46 | 47 |
| 47 def Compare(symbols1, symbols2): | 48 def Compare(symbols1, symbols2): |
| 48 """Executes a comparison of the symbols in symbols1 and symbols2. | 49 """Executes a comparison of the symbols in symbols1 and symbols2. |
| 49 | 50 |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 99 unchanged.append((key[0], key[1], symbol_name, symbol_size, size2)) | 100 unchanged.append((key[0], key[1], symbol_name, symbol_size, size2)) |
| 100 | 101 |
| 101 # We have now analyzed all symbols that are in cache1 and removed all of | 102 # We have now analyzed all symbols that are in cache1 and removed all of |
| 102 # the encountered symbols from cache2. What's left in cache2 is the new | 103 # the encountered symbols from cache2. What's left in cache2 is the new |
| 103 # symbols. | 104 # symbols. |
| 104 for key, bucket2 in cache2.iteritems(): | 105 for key, bucket2 in cache2.iteritems(): |
| 105 for symbol_name, symbol_size in bucket2.items(): | 106 for symbol_name, symbol_size in bucket2.items(): |
| 106 added.append((key[0], key[1], symbol_name, None, symbol_size)) | 107 added.append((key[0], key[1], symbol_name, None, symbol_size)) |
| 107 return (added, removed, changed, unchanged) | 108 return (added, removed, changed, unchanged) |
| 108 | 109 |
| 110 def DeltaStr(number): |
| 111 """Returns the number as a string with a '+' prefix if it's > 0 and |
| 112 a '-' prefix if it's < 0.""" |
| 113 result = str(number) |
| 114 if number > 0: |
| 115 result = '+' + result |
| 116 return result |
| 117 |
| 118 |
| 119 class CrunchStatsData(object): |
| 120 """Stores a summary of data of a certain kind.""" |
| 121 def __init__(self, symbols): |
| 122 self.symbols = symbols |
| 123 self.sources = set() |
| 124 self.before_size = 0 |
| 125 self.after_size = 0 |
| 126 self.symbols_by_path = {} |
| 127 |
| 109 | 128 |
| 110 def CrunchStats(added, removed, changed, unchanged, showsources, showsymbols): | 129 def CrunchStats(added, removed, changed, unchanged, showsources, showsymbols): |
| 111 """Outputs to stdout a summary of changes based on the symbol lists.""" | 130 """Outputs to stdout a summary of changes based on the symbol lists.""" |
| 112 print 'Symbol statistics:' | 131 # Split changed into grown and shrunk because that is easier to |
| 113 sources_with_new_symbols = set() | 132 # discuss. |
| 114 new_symbols_size = 0 | 133 grown = [] |
| 115 new_symbols_by_path = {} | 134 shrunk = [] |
| 116 for file_path, symbol_type, symbol_name, size1, size2 in added: | 135 for item in changed: |
| 117 sources_with_new_symbols.add(file_path) | 136 file_path, symbol_type, symbol_name, size1, size2 = item |
| 118 new_symbols_size += size2 | 137 if size1 < size2: |
| 119 bucket = new_symbols_by_path.setdefault(file_path, []) | 138 grown.append(item) |
| 120 bucket.append((symbol_name, symbol_type, None, size2)) | 139 else: |
| 121 print(' %d added, totalling %d bytes across %d sources' % | 140 shrunk.append(item) |
| 122 (len(added), new_symbols_size, len(sources_with_new_symbols))) | |
| 123 | 141 |
| 124 sources_with_removed_symbols = set() | 142 new_symbols = CrunchStatsData(added) |
| 125 removed_symbols_size = 0 | 143 removed_symbols = CrunchStatsData(removed) |
| 126 removed_symbols_by_path = {} | 144 grown_symbols = CrunchStatsData(grown) |
| 127 for file_path, symbol_type, symbol_name, size1, size2 in removed: | 145 shrunk_symbols = CrunchStatsData(shrunk) |
| 128 sources_with_removed_symbols.add(file_path) | 146 sections = [new_symbols, removed_symbols, grown_symbols, shrunk_symbols] |
| 129 removed_symbols_size += size1 | 147 for section in sections: |
| 130 bucket = removed_symbols_by_path.setdefault(file_path, []) | 148 for file_path, symbol_type, symbol_name, size1, size2 in section.symbols: |
| 131 bucket.append((symbol_name, symbol_type, size1, None)) | 149 section.sources.add(file_path) |
| 132 print(' %d removed, totalling %d bytes removed across %d sources' % | 150 if size1 is not None: |
| 133 (len(removed), removed_symbols_size, len(sources_with_removed_symbols))) | 151 section.before_size += size1 |
| 152 if size2 is not None: |
| 153 section.after_size += size2 |
| 154 bucket = section.symbols_by_path.setdefault(file_path, []) |
| 155 bucket.append((symbol_name, symbol_type, size1, size2)) |
| 134 | 156 |
| 135 sources_with_changed_symbols = set() | 157 total_change = sum(s.after_size - s.before_size for s in sections) |
| 136 before_size = 0 | 158 summary = 'Total change: %s bytes' % DeltaStr(total_change) |
| 137 after_size = 0 | 159 print(summary) |
| 138 changed_symbols_by_path = {} | 160 print('=' * len(summary)) |
| 139 for file_path, symbol_type, symbol_name, size1, size2 in changed: | 161 for section in sections: |
| 140 sources_with_changed_symbols.add(file_path) | 162 if not section.symbols: |
| 141 before_size += size1 | 163 continue |
| 142 after_size += size2 | 164 if section.before_size == 0: |
| 143 bucket = changed_symbols_by_path.setdefault(file_path, []) | 165 description = ('added, totalling %s bytes' % DeltaStr(section.after_size)) |
| 144 bucket.append((symbol_name, symbol_type, size1, size2)) | 166 elif section.after_size == 0: |
| 145 print(' %d changed, resulting in a net change of %d bytes ' | 167 description = ('removed, totalling %s bytes' % |
| 146 '(%d bytes before, %d bytes after) across %d sources' % | 168 DeltaStr(-section.before_size)) |
| 147 (len(changed), (after_size - before_size), before_size, after_size, | 169 else: |
| 148 len(sources_with_changed_symbols))) | 170 if section.after_size > section.before_size: |
| 171 type_str = 'grown' |
| 172 else: |
| 173 type_str = 'shrunk' |
| 174 description = ('%s, for a net change of %s bytes ' |
| 175 '(%d bytes before, %d bytes after)' % |
| 176 (type_str, DeltaStr(section.after_size - section.before_size), |
| 177 section.before_size, section.after_size)) |
| 178 print(' %d %s across %d sources' % |
| 179 (len(section.symbols), description, len(section.sources))) |
| 149 | 180 |
| 150 maybe_unchanged_sources = set() | 181 maybe_unchanged_sources = set() |
| 151 unchanged_symbols_size = 0 | 182 unchanged_symbols_size = 0 |
| 152 for file_path, symbol_type, symbol_name, size1, size2 in unchanged: | 183 for file_path, symbol_type, symbol_name, size1, size2 in unchanged: |
| 153 maybe_unchanged_sources.add(file_path) | 184 maybe_unchanged_sources.add(file_path) |
| 154 unchanged_symbols_size += size1 # == size2 | 185 unchanged_symbols_size += size1 # == size2 |
| 155 print(' %d unchanged, totalling %d bytes' % | 186 print(' %d unchanged, totalling %d bytes' % |
| 156 (len(unchanged), unchanged_symbols_size)) | 187 (len(unchanged), unchanged_symbols_size)) |
| 157 | 188 |
| 158 # High level analysis, always output. | 189 # High level analysis, always output. |
| 159 unchanged_sources = (maybe_unchanged_sources - | 190 unchanged_sources = maybe_unchanged_sources |
| 160 sources_with_changed_symbols - | 191 for section in sections: |
| 161 sources_with_removed_symbols - | 192 unchanged_sources = unchanged_sources - section.sources |
| 162 sources_with_new_symbols) | 193 new_sources = (new_symbols.sources - |
| 163 new_sources = (sources_with_new_symbols - | |
| 164 maybe_unchanged_sources - | 194 maybe_unchanged_sources - |
| 165 sources_with_removed_symbols) | 195 removed_symbols.sources) |
| 166 removed_sources = (sources_with_removed_symbols - | 196 removed_sources = (removed_symbols.sources - |
| 167 maybe_unchanged_sources - | 197 maybe_unchanged_sources - |
| 168 sources_with_new_symbols) | 198 new_symbols.sources) |
| 169 partially_changed_sources = (sources_with_changed_symbols | | 199 partially_changed_sources = (grown_symbols.sources | |
| 170 sources_with_new_symbols | | 200 shrunk_symbols.sources | new_symbols.sources | |
| 171 sources_with_removed_symbols) - removed_sources - new_sources | 201 removed_symbols.sources) - removed_sources - new_sources |
| 172 allFiles = (sources_with_new_symbols | | 202 allFiles = set() |
| 173 sources_with_removed_symbols | | 203 for section in sections: |
| 174 sources_with_changed_symbols | | 204 allFiles = allFiles | section.sources |
| 175 maybe_unchanged_sources) | 205 allFiles = allFiles | maybe_unchanged_sources |
| 176 print 'Source stats:' | 206 print 'Source stats:' |
| 177 print(' %d sources encountered.' % len(allFiles)) | 207 print(' %d sources encountered.' % len(allFiles)) |
| 178 print(' %d completely new.' % len(new_sources)) | 208 print(' %d completely new.' % len(new_sources)) |
| 179 print(' %d removed completely.' % len(removed_sources)) | 209 print(' %d removed completely.' % len(removed_sources)) |
| 180 print(' %d partially changed.' % len(partially_changed_sources)) | 210 print(' %d partially changed.' % len(partially_changed_sources)) |
| 181 print(' %d completely unchanged.' % len(unchanged_sources)) | 211 print(' %d completely unchanged.' % len(unchanged_sources)) |
| 182 remainder = (allFiles - new_sources - removed_sources - | 212 remainder = (allFiles - new_sources - removed_sources - |
| 183 partially_changed_sources - unchanged_sources) | 213 partially_changed_sources - unchanged_sources) |
| 184 assert len(remainder) == 0 | 214 assert len(remainder) == 0 |
| 185 | 215 |
| 186 if not showsources: | 216 if not showsources: |
| 187 return # Per-source analysis, only if requested | 217 return # Per-source analysis, only if requested |
| 188 print 'Per-source Analysis:' | 218 print 'Per-source Analysis:' |
| 189 delta_by_path = {} | 219 delta_by_path = {} |
| 190 for path in new_symbols_by_path: | 220 for section in sections: |
| 191 entry = delta_by_path.get(path) | 221 for path in section.symbols_by_path: |
| 192 if not entry: | 222 entry = delta_by_path.get(path) |
| 193 entry = {'plus': 0, 'minus': 0} | 223 if not entry: |
| 194 delta_by_path[path] = entry | 224 entry = {'plus': 0, 'minus': 0} |
| 195 for symbol_name, symbol_type, size1, size2 in new_symbols_by_path[path]: | 225 delta_by_path[path] = entry |
| 196 entry['plus'] += size2 | 226 for symbol_name, symbol_type, size1, size2 in \ |
| 197 for path in removed_symbols_by_path: | 227 section.symbols_by_path[path]: |
| 198 entry = delta_by_path.get(path) | 228 if size1 is None: |
| 199 if not entry: | 229 delta = size2 |
| 200 entry = {'plus': 0, 'minus': 0} | 230 elif size2 is None: |
| 201 delta_by_path[path] = entry | 231 delta = -size1 |
| 202 for symbol_name, symbol_type, size1, size2 in removed_symbols_by_path[path]: | 232 else: |
| 203 entry['minus'] += size1 | 233 delta = size2 - size1 |
| 204 for path in changed_symbols_by_path: | |
| 205 entry = delta_by_path.get(path) | |
| 206 if not entry: | |
| 207 entry = {'plus': 0, 'minus': 0} | |
| 208 delta_by_path[path] = entry | |
| 209 for symbol_name, symbol_type, size1, size2 in changed_symbols_by_path[path]: | |
| 210 delta = size2 - size1 | |
| 211 if delta > 0: | |
| 212 entry['plus'] += delta | |
| 213 else: | |
| 214 entry['minus'] += (-1 * delta) | |
| 215 | 234 |
| 216 for path in sorted(delta_by_path): | 235 if delta > 0: |
| 217 print ' Source: ' + path | 236 entry['plus'] += delta |
| 218 size_data = delta_by_path[path] | 237 else: |
| 238 entry['minus'] += (-1 * delta) |
| 239 |
| 240 def delta_sort_key(item): |
| 241 _path, size_data = item |
| 242 growth = size_data['plus'] - size_data['minus'] |
| 243 return growth |
| 244 |
| 245 for path, size_data in sorted(delta_by_path.iteritems(), key=delta_sort_key, |
| 246 reverse=True): |
| 219 gain = size_data['plus'] | 247 gain = size_data['plus'] |
| 220 loss = size_data['minus'] | 248 loss = size_data['minus'] |
| 221 delta = size_data['plus'] - size_data['minus'] | 249 delta = size_data['plus'] - size_data['minus'] |
| 222 print (' Change: %d bytes (gained %d, lost %d)' % (delta, gain, loss)) | 250 header = ' %s - Source: %s - (gained %d, lost %d)' % (DeltaStr(delta), |
| 251 path, gain, loss) |
| 252 divider = '-' * len(header) |
| 253 print '' |
| 254 print divider |
| 255 print header |
| 256 print divider |
| 223 if showsymbols: | 257 if showsymbols: |
| 224 if path in new_symbols_by_path: | 258 if path in new_symbols.symbols_by_path: |
| 225 print ' New symbols:' | 259 print ' New symbols:' |
| 226 for symbol_name, symbol_type, size1, size2 in \ | 260 for symbol_name, symbol_type, size1, size2 in \ |
| 227 new_symbols_by_path[path]: | 261 sorted(new_symbols.symbols_by_path[path], |
| 228 print (' %s type=%s, size=%d bytes' % | 262 key=operator.itemgetter(3), |
| 229 (symbol_name, symbol_type, size2)) | 263 reverse=True): |
| 230 if path in removed_symbols_by_path: | 264 print (' %8s: %s type=%s, size=%d bytes' % |
| 231 print ' Removed symbols:' | 265 (DeltaStr(size2), symbol_name, symbol_type, size2)) |
| 266 if path in removed_symbols.symbols_by_path: |
| 267 print ' Removed symbols:' |
| 232 for symbol_name, symbol_type, size1, size2 in \ | 268 for symbol_name, symbol_type, size1, size2 in \ |
| 233 removed_symbols_by_path[path]: | 269 sorted(removed_symbols.symbols_by_path[path], |
| 234 print (' %s type=%s, size=%d bytes' % | 270 key=operator.itemgetter(2)): |
| 235 (symbol_name, symbol_type, size1)) | 271 print (' %8s: %s type=%s, size=%d bytes' % |
| 236 if path in changed_symbols_by_path: | 272 (DeltaStr(-size1), symbol_name, symbol_type, size1)) |
| 237 print ' Changed symbols:' | 273 for (changed_symbols_by_path, type_str) in [ |
| 238 def sortkey(item): | 274 (grown_symbols.symbols_by_path, "Grown"), |
| 239 symbol_name, _symbol_type, size1, size2 = item | 275 (shrunk_symbols.symbols_by_path, "Shrunk")]: |
| 240 return (size1 - size2, symbol_name) | 276 if path in changed_symbols_by_path: |
| 241 for symbol_name, symbol_type, size1, size2 in \ | 277 print ' %s symbols:' % type_str |
| 242 sorted(changed_symbols_by_path[path], key=sortkey): | 278 def changed_symbol_sortkey(item): |
| 243 print (' %s type=%s, delta=%d bytes (was %d bytes, now %d bytes)' | 279 symbol_name, _symbol_type, size1, size2 = item |
| 244 % (symbol_name, symbol_type, (size2 - size1), size1, size2)) | 280 return (size1 - size2, symbol_name) |
| 281 for symbol_name, symbol_type, size1, size2 in \ |
| 282 sorted(changed_symbols_by_path[path], key=changed_symbol_sortkey): |
| 283 print (' %8s: %s type=%s, (was %d bytes, now %d bytes)' |
| 284 % (DeltaStr(size2 - size1), symbol_name, |
| 285 symbol_type, size1, size2)) |
| 245 | 286 |
| 246 | 287 |
| 247 def main(): | 288 def main(): |
| 248 usage = """%prog [options] | 289 usage = """%prog [options] |
| 249 | 290 |
| 250 Analyzes the symbolic differences between two binary files | 291 Analyzes the symbolic differences between two binary files |
| 251 (typically, not necessarily, two different builds of the same | 292 (typically, not necessarily, two different builds of the same |
| 252 library) and produces a detailed description of symbols that have | 293 library) and produces a detailed description of symbols that have |
| 253 been added, removed, or whose size has changed. | 294 been added, removed, or whose size has changed. |
| 254 | 295 |
| (...skipping 24 matching lines...) Expand all Loading... |
| 279 with file(path, 'r') as nm_input: | 320 with file(path, 'r') as nm_input: |
| 280 if opts.verbose: | 321 if opts.verbose: |
| 281 print 'parsing ' + path + '...' | 322 print 'parsing ' + path + '...' |
| 282 symbols.append(list(binary_size_utils.ParseNm(nm_input))) | 323 symbols.append(list(binary_size_utils.ParseNm(nm_input))) |
| 283 (added, removed, changed, unchanged) = Compare(symbols[0], symbols[1]) | 324 (added, removed, changed, unchanged) = Compare(symbols[0], symbols[1]) |
| 284 CrunchStats(added, removed, changed, unchanged, | 325 CrunchStats(added, removed, changed, unchanged, |
| 285 opts.showsources | opts.showsymbols, opts.showsymbols) | 326 opts.showsources | opts.showsymbols, opts.showsymbols) |
| 286 | 327 |
| 287 if __name__ == '__main__': | 328 if __name__ == '__main__': |
| 288 sys.exit(main()) | 329 sys.exit(main()) |
| OLD | NEW |