| 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 """Generate a spatial analysis against an arbitrary library. | 6 """Generate a spatial analysis against an arbitrary library. |
| 7 | 7 |
| 8 To use, build the 'binary_size_tool' target. Then run this tool, passing | 8 To use, build the 'binary_size_tool' target. Then run this tool, passing |
| 9 in the location of the library to be analyzed along with any other options | 9 in the location of the library to be analyzed along with any other options |
| 10 you desire. | 10 you desire. |
| 11 """ | 11 """ |
| 12 | 12 |
| 13 import collections | 13 import collections |
| 14 import json | 14 import json |
| 15 import logging | 15 import logging |
| 16 import multiprocessing | 16 import multiprocessing |
| 17 import optparse | 17 import optparse |
| 18 import os | 18 import os |
| 19 import re | 19 import re |
| 20 import shutil | 20 import shutil |
| 21 import struct | 21 import struct |
| 22 import subprocess | 22 import subprocess |
| 23 import sys | 23 import sys |
| 24 import tempfile | 24 import tempfile |
| 25 import time | 25 import time |
| 26 | 26 |
| 27 import binary_size_utils | 27 import binary_size_utils |
| 28 | 28 |
| 29 # This path changee is not beautiful. Temporary (I hope) measure until | 29 # This path change is not beautiful. Temporary (I hope) measure until |
| 30 # the chromium project has figured out a proper way to organize the | 30 # the chromium project has figured out a proper way to organize the |
| 31 # library of python tools. http://crbug.com/375725 | 31 # library of python tools. http://crbug.com/375725 |
| 32 elf_symbolizer_path = os.path.abspath(os.path.join( | 32 elf_symbolizer_path = os.path.abspath(os.path.join( |
| 33 os.path.dirname(__file__), | 33 os.path.dirname(__file__), |
| 34 '..', | 34 '..', |
| 35 '..', | 35 '..', |
| 36 'build', | 36 'build', |
| 37 'android', | 37 'android', |
| 38 'pylib')) | 38 'pylib')) |
| 39 sys.path.append(elf_symbolizer_path) | 39 sys.path.append(elf_symbolizer_path) |
| (...skipping 173 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 213 SplitNoPathBucket(result) | 213 SplitNoPathBucket(result) |
| 214 | 214 |
| 215 largest_list_len = MakeChildrenDictsIntoLists(result) | 215 largest_list_len = MakeChildrenDictsIntoLists(result) |
| 216 | 216 |
| 217 if largest_list_len > BIG_BUCKET_LIMIT: | 217 if largest_list_len > BIG_BUCKET_LIMIT: |
| 218 logging.warning('There are sections with %d nodes. ' | 218 logging.warning('There are sections with %d nodes. ' |
| 219 'Results might be unusable.' % largest_list_len) | 219 'Results might be unusable.' % largest_list_len) |
| 220 return result | 220 return result |
| 221 | 221 |
| 222 | 222 |
| 223 # TODO(andrewhayden): Only used for legacy reports. Delete. | |
| 224 def TreeifySymbols(symbols): | |
| 225 """Convert symbols into a path-based tree, calculating size information | |
| 226 along the way. | |
| 227 | |
| 228 The result is a dictionary that contains two kinds of nodes: | |
| 229 1. Leaf nodes, representing source code locations (e.g., c++ files) | |
| 230 These nodes have the following dictionary entries: | |
| 231 sizes: a dictionary whose keys are categories (such as code, data, | |
| 232 vtable, etceteras) and whose values are the size, in bytes, of | |
| 233 those categories; | |
| 234 size: the total size, in bytes, of all the entries in the sizes dict | |
| 235 2. Non-leaf nodes, representing directories | |
| 236 These nodes have the following dictionary entries: | |
| 237 children: a dictionary whose keys are names (path entries; either | |
| 238 directory or file names) and whose values are other nodes; | |
| 239 size: the total size, in bytes, of all the leaf nodes that are | |
| 240 contained within the children dict (recursively expanded) | |
| 241 | |
| 242 The result object is itself a dictionary that represents the common ancestor | |
| 243 of all child nodes, e.g. a path to which all other nodes beneath it are | |
| 244 relative. The 'size' attribute of this dict yields the sum of the size of all | |
| 245 leaf nodes within the data structure. | |
| 246 """ | |
| 247 dirs = {'children': {}, 'size': 0} | |
| 248 for sym, symbol_type, size, path in symbols: | |
| 249 dirs['size'] += size | |
| 250 if path: | |
| 251 path = os.path.normpath(path) | |
| 252 if path.startswith('/'): | |
| 253 path = path[1:] | |
| 254 | |
| 255 parts = None | |
| 256 if path: | |
| 257 parts = path.split('/') | |
| 258 | |
| 259 if parts: | |
| 260 assert path | |
| 261 file_key = parts.pop() | |
| 262 tree = dirs | |
| 263 try: | |
| 264 # Traverse the tree to the parent of the file node, creating as needed | |
| 265 for part in parts: | |
| 266 assert part != '' | |
| 267 if part not in tree['children']: | |
| 268 tree['children'][part] = {'children': {}, 'size': 0} | |
| 269 tree = tree['children'][part] | |
| 270 tree['size'] += size | |
| 271 | |
| 272 # Get (creating if necessary) the node for the file | |
| 273 # This node doesn't have a 'children' attribute | |
| 274 if file_key not in tree['children']: | |
| 275 tree['children'][file_key] = {'sizes': collections.defaultdict(int), | |
| 276 'size': 0} | |
| 277 tree = tree['children'][file_key] | |
| 278 tree['size'] += size | |
| 279 | |
| 280 # Accumulate size into a bucket within the file | |
| 281 symbol_type = symbol_type.lower() | |
| 282 if 'vtable for ' in sym: | |
| 283 tree['sizes']['[vtable]'] += size | |
| 284 elif 'r' == symbol_type: | |
| 285 tree['sizes']['[rodata]'] += size | |
| 286 elif 'd' == symbol_type: | |
| 287 tree['sizes']['[data]'] += size | |
| 288 elif 'b' == symbol_type: | |
| 289 tree['sizes']['[bss]'] += size | |
| 290 elif 't' == symbol_type: | |
| 291 # 'text' in binary parlance means 'code'. | |
| 292 tree['sizes']['[code]'] += size | |
| 293 elif 'w' == symbol_type: | |
| 294 tree['sizes']['[weak]'] += size | |
| 295 else: | |
| 296 tree['sizes']['[other]'] += size | |
| 297 except: | |
| 298 print >> sys.stderr, sym, parts, file_key | |
| 299 raise | |
| 300 else: | |
| 301 key = 'symbols without paths' | |
| 302 if key not in dirs['children']: | |
| 303 dirs['children'][key] = {'sizes': collections.defaultdict(int), | |
| 304 'size': 0} | |
| 305 tree = dirs['children'][key] | |
| 306 subkey = 'misc' | |
| 307 if (sym.endswith('::__FUNCTION__') or | |
| 308 sym.endswith('::__PRETTY_FUNCTION__')): | |
| 309 subkey = '__FUNCTION__' | |
| 310 elif sym.startswith('CSWTCH.'): | |
| 311 subkey = 'CSWTCH' | |
| 312 elif '::' in sym: | |
| 313 subkey = sym[0:sym.find('::') + 2] | |
| 314 tree['sizes'][subkey] = tree['sizes'].get(subkey, 0) + size | |
| 315 tree['size'] += size | |
| 316 return dirs | |
| 317 | |
| 318 | |
| 319 # TODO(andrewhayden): Only used for legacy reports. Delete. | |
| 320 def JsonifyTree(tree, name): | |
| 321 """Convert TreeifySymbols output to a JSON treemap. | |
| 322 | |
| 323 The format is very similar, with the notable exceptions being | |
| 324 lists of children instead of maps and some different attribute names.""" | |
| 325 children = [] | |
| 326 css_class_map = { | |
| 327 '[vtable]': 'vtable', | |
| 328 '[rodata]': 'read-only_data', | |
| 329 '[data]': 'data', | |
| 330 '[bss]': 'bss', | |
| 331 '[code]': 'code', | |
| 332 '[weak]': 'weak_symbol' | |
| 333 } | |
| 334 if 'children' in tree: | |
| 335 # Non-leaf node. Recurse. | |
| 336 for child_name, child in tree['children'].iteritems(): | |
| 337 children.append(JsonifyTree(child, child_name)) | |
| 338 else: | |
| 339 # Leaf node; dump per-file stats as entries in the treemap | |
| 340 for kind, size in tree['sizes'].iteritems(): | |
| 341 child_json = {'name': kind + ' (' + FormatBytes(size) + ')', | |
| 342 'data': { '$area': size }} | |
| 343 css_class = css_class_map.get(kind) | |
| 344 if css_class is not None: | |
| 345 child_json['data']['$symbol'] = css_class | |
| 346 children.append(child_json) | |
| 347 # Sort children by size, largest to smallest. | |
| 348 children.sort(key=lambda child: -child['data']['$area']) | |
| 349 | |
| 350 # For leaf nodes, the 'size' attribute is the size of the leaf; | |
| 351 # Non-leaf nodes don't really have a size, but their 'size' attribute is | |
| 352 # the sum of the sizes of all their children. | |
| 353 return {'name': name + ' (' + FormatBytes(tree['size']) + ')', | |
| 354 'data': { '$area': tree['size'] }, | |
| 355 'children': children } | |
| 356 | |
| 357 def DumpCompactTree(symbols, symbol_path_origin_dir, outfile): | 223 def DumpCompactTree(symbols, symbol_path_origin_dir, outfile): |
| 358 tree_root = MakeCompactTree(symbols, symbol_path_origin_dir) | 224 tree_root = MakeCompactTree(symbols, symbol_path_origin_dir) |
| 359 with open(outfile, 'w') as out: | 225 with open(outfile, 'w') as out: |
| 360 out.write('var tree_data=') | 226 out.write('var tree_data=') |
| 361 # Use separators without whitespace to get a smaller file. | 227 # Use separators without whitespace to get a smaller file. |
| 362 json.dump(tree_root, out, separators=(',', ':')) | 228 json.dump(tree_root, out, separators=(',', ':')) |
| 363 print('Writing %d bytes json' % os.path.getsize(outfile)) | 229 print('Writing %d bytes json' % os.path.getsize(outfile)) |
| 364 | 230 |
| 365 | 231 |
| 366 # TODO(andrewhayden): Only used for legacy reports. Delete. | |
| 367 def DumpTreemap(symbols, outfile): | |
| 368 dirs = TreeifySymbols(symbols) | |
| 369 out = open(outfile, 'w') | |
| 370 try: | |
| 371 out.write('var kTree = ' + json.dumps(JsonifyTree(dirs, '/'))) | |
| 372 finally: | |
| 373 out.flush() | |
| 374 out.close() | |
| 375 | |
| 376 | |
| 377 # TODO(andrewhayden): Only used for legacy reports. Delete. | |
| 378 def DumpLargestSymbols(symbols, outfile, n): | |
| 379 # a list of (sym, symbol_type, size, path); sort by size. | |
| 380 symbols = sorted(symbols, key=lambda x: -x[2]) | |
| 381 dumped = 0 | |
| 382 out = open(outfile, 'w') | |
| 383 try: | |
| 384 out.write('var largestSymbols = [\n') | |
| 385 for sym, symbol_type, size, path in symbols: | |
| 386 if symbol_type in ('b', 'w'): | |
| 387 continue # skip bss and weak symbols | |
| 388 if path is None: | |
| 389 path = '' | |
| 390 entry = {'size': FormatBytes(size), | |
| 391 'symbol': sym, | |
| 392 'type': SymbolTypeToHuman(symbol_type), | |
| 393 'location': path } | |
| 394 out.write(json.dumps(entry)) | |
| 395 out.write(',\n') | |
| 396 dumped += 1 | |
| 397 if dumped >= n: | |
| 398 return | |
| 399 finally: | |
| 400 out.write('];\n') | |
| 401 out.flush() | |
| 402 out.close() | |
| 403 | |
| 404 | |
| 405 def MakeSourceMap(symbols): | 232 def MakeSourceMap(symbols): |
| 406 sources = {} | 233 sources = {} |
| 407 for _sym, _symbol_type, size, path in symbols: | 234 for _sym, _symbol_type, size, path in symbols: |
| 408 key = None | 235 key = None |
| 409 if path: | 236 if path: |
| 410 key = os.path.normpath(path) | 237 key = os.path.normpath(path) |
| 411 else: | 238 else: |
| 412 key = '[no path]' | 239 key = '[no path]' |
| 413 if key not in sources: | 240 if key not in sources: |
| 414 sources[key] = {'path': path, 'symbol_count': 0, 'size': 0} | 241 sources[key] = {'path': path, 'symbol_count': 0, 'size': 0} |
| 415 record = sources[key] | 242 record = sources[key] |
| 416 record['size'] += size | 243 record['size'] += size |
| 417 record['symbol_count'] += 1 | 244 record['symbol_count'] += 1 |
| 418 return sources | 245 return sources |
| 419 | 246 |
| 420 | 247 |
| 421 # TODO(andrewhayden): Only used for legacy reports. Delete. | |
| 422 def DumpLargestSources(symbols, outfile, n): | |
| 423 source_map = MakeSourceMap(symbols) | |
| 424 sources = sorted(source_map.values(), key=lambda x: -x['size']) | |
| 425 dumped = 0 | |
| 426 out = open(outfile, 'w') | |
| 427 try: | |
| 428 out.write('var largestSources = [\n') | |
| 429 for record in sources: | |
| 430 entry = {'size': FormatBytes(record['size']), | |
| 431 'symbol_count': str(record['symbol_count']), | |
| 432 'location': record['path']} | |
| 433 out.write(json.dumps(entry)) | |
| 434 out.write(',\n') | |
| 435 dumped += 1 | |
| 436 if dumped >= n: | |
| 437 return | |
| 438 finally: | |
| 439 out.write('];\n') | |
| 440 out.flush() | |
| 441 out.close() | |
| 442 | |
| 443 | |
| 444 # TODO(andrewhayden): Only used for legacy reports. Delete. | |
| 445 def DumpLargestVTables(symbols, outfile, n): | |
| 446 vtables = [] | |
| 447 for symbol, _type, size, path in symbols: | |
| 448 if 'vtable for ' in symbol: | |
| 449 vtables.append({'symbol': symbol, 'path': path, 'size': size}) | |
| 450 vtables = sorted(vtables, key=lambda x: -x['size']) | |
| 451 dumped = 0 | |
| 452 out = open(outfile, 'w') | |
| 453 try: | |
| 454 out.write('var largestVTables = [\n') | |
| 455 for record in vtables: | |
| 456 entry = {'size': FormatBytes(record['size']), | |
| 457 'symbol': record['symbol'], | |
| 458 'location': record['path']} | |
| 459 out.write(json.dumps(entry)) | |
| 460 out.write(',\n') | |
| 461 dumped += 1 | |
| 462 if dumped >= n: | |
| 463 return | |
| 464 finally: | |
| 465 out.write('];\n') | |
| 466 out.flush() | |
| 467 out.close() | |
| 468 | |
| 469 | |
| 470 # Regex for parsing "nm" output. A sample line looks like this: | 248 # Regex for parsing "nm" output. A sample line looks like this: |
| 471 # 0167b39c 00000018 t ACCESS_DESCRIPTION_free /path/file.c:95 | 249 # 0167b39c 00000018 t ACCESS_DESCRIPTION_free /path/file.c:95 |
| 472 # | 250 # |
| 473 # The fields are: address, size, type, name, source location | 251 # The fields are: address, size, type, name, source location |
| 474 # Regular expression explained ( see also: https://xkcd.com/208 ): | 252 # Regular expression explained ( see also: https://xkcd.com/208 ): |
| 475 # ([0-9a-f]{8,}+) The address | 253 # ([0-9a-f]{8,}+) The address |
| 476 # [\s]+ Whitespace separator | 254 # [\s]+ Whitespace separator |
| 477 # ([0-9a-f]{8,}+) The size. From here on out it's all optional. | 255 # ([0-9a-f]{8,}+) The size. From here on out it's all optional. |
| 478 # [\s]+ Whitespace separator | 256 # [\s]+ Whitespace separator |
| 479 # (\S?) The symbol type, which is any non-whitespace char | 257 # (\S?) The symbol type, which is any non-whitespace char |
| (...skipping 399 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 879 opts.disable_disambiguation is None, | 657 opts.disable_disambiguation is None, |
| 880 opts.source_path) | 658 opts.source_path) |
| 881 | 659 |
| 882 if opts.pak: | 660 if opts.pak: |
| 883 AddPakData(symbols, opts.pak) | 661 AddPakData(symbols, opts.pak) |
| 884 | 662 |
| 885 if not os.path.exists(opts.destdir): | 663 if not os.path.exists(opts.destdir): |
| 886 os.makedirs(opts.destdir, 0755) | 664 os.makedirs(opts.destdir, 0755) |
| 887 | 665 |
| 888 | 666 |
| 889 if opts.legacy: # legacy report | 667 if opts.library: |
| 890 DumpTreemap(symbols, os.path.join(opts.destdir, 'treemap-dump.js')) | 668 symbol_path_origin_dir = os.path.dirname(os.path.abspath(opts.library)) |
| 891 DumpLargestSymbols(symbols, | 669 else: |
| 892 os.path.join(opts.destdir, 'largest-symbols.js'), 100) | 670 # Just a guess. Hopefully all paths in the input file are absolute. |
| 893 DumpLargestSources(symbols, | 671 symbol_path_origin_dir = os.path.abspath(os.getcwd()) |
| 894 os.path.join(opts.destdir, 'largest-sources.js'), 100) | 672 data_js_file_name = os.path.join(opts.destdir, 'data.js') |
| 895 DumpLargestVTables(symbols, | 673 DumpCompactTree(symbols, symbol_path_origin_dir, data_js_file_name) |
| 896 os.path.join(opts.destdir, 'largest-vtables.js'), 100) | 674 d3_out = os.path.join(opts.destdir, 'd3') |
| 897 treemap_out = os.path.join(opts.destdir, 'webtreemap') | 675 if not os.path.exists(d3_out): |
| 898 if not os.path.exists(treemap_out): | 676 os.makedirs(d3_out, 0755) |
| 899 os.makedirs(treemap_out, 0755) | 677 d3_src = os.path.join(os.path.dirname(__file__), |
| 900 treemap_src = os.path.join('third_party', 'webtreemap', 'src') | 678 '..', |
| 901 shutil.copy(os.path.join(treemap_src, 'COPYING'), treemap_out) | 679 '..', |
| 902 shutil.copy(os.path.join(treemap_src, 'webtreemap.js'), treemap_out) | 680 'third_party', 'd3', 'src') |
| 903 shutil.copy(os.path.join(treemap_src, 'webtreemap.css'), treemap_out) | 681 template_src = os.path.join(os.path.dirname(__file__), |
| 904 shutil.copy(os.path.join('tools', 'binary_size', 'legacy_template', | 682 'template') |
| 905 'index.html'), opts.destdir) | 683 shutil.copy(os.path.join(d3_src, 'LICENSE'), d3_out) |
| 906 else: # modern report | 684 shutil.copy(os.path.join(d3_src, 'd3.js'), d3_out) |
| 907 if opts.library: | 685 shutil.copy(os.path.join(template_src, 'index.html'), opts.destdir) |
| 908 symbol_path_origin_dir = os.path.dirname(os.path.abspath(opts.library)) | 686 shutil.copy(os.path.join(template_src, 'D3SymbolTreeMap.js'), opts.destdir) |
| 909 else: | |
| 910 # Just a guess. Hopefully all paths in the input file are absolute. | |
| 911 symbol_path_origin_dir = os.path.abspath(os.getcwd()) | |
| 912 data_js_file_name = os.path.join(opts.destdir, 'data.js') | |
| 913 DumpCompactTree(symbols, symbol_path_origin_dir, data_js_file_name) | |
| 914 d3_out = os.path.join(opts.destdir, 'd3') | |
| 915 if not os.path.exists(d3_out): | |
| 916 os.makedirs(d3_out, 0755) | |
| 917 d3_src = os.path.join(os.path.dirname(__file__), | |
| 918 '..', | |
| 919 '..', | |
| 920 'third_party', 'd3', 'src') | |
| 921 template_src = os.path.join(os.path.dirname(__file__), | |
| 922 'template') | |
| 923 shutil.copy(os.path.join(d3_src, 'LICENSE'), d3_out) | |
| 924 shutil.copy(os.path.join(d3_src, 'd3.js'), d3_out) | |
| 925 shutil.copy(os.path.join(template_src, 'index.html'), opts.destdir) | |
| 926 shutil.copy(os.path.join(template_src, 'D3SymbolTreeMap.js'), opts.destdir) | |
| 927 | 687 |
| 928 print 'Report saved to ' + opts.destdir + '/index.html' | 688 print 'Report saved to ' + opts.destdir + '/index.html' |
| 929 | 689 |
| 930 | 690 |
| 931 if __name__ == '__main__': | 691 if __name__ == '__main__': |
| 932 sys.exit(main()) | 692 sys.exit(main()) |
| OLD | NEW |