 Chromium Code Reviews
 Chromium Code Reviews Issue 258633003:
  Graphical version of the run_binary_size_analysis tool.    (Closed) 
  Base URL: https://chromium.googlesource.com/chromium/src.git@master
    
  
    Issue 258633003:
  Graphical version of the run_binary_size_analysis tool.    (Closed) 
  Base URL: https://chromium.googlesource.com/chromium/src.git@master| OLD | NEW | 
|---|---|
| 1 #!/usr/bin/python | 1 #!/usr/bin/python | 
| 
Primiano Tucci (use gerrit)
2014/05/20 15:22:57
see my previous comment, on env vs /usr/bin/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 fileinput | 14 import fileinput | 
| 15 import json | 15 import json | 
| 16 import multiprocessing | |
| 16 import optparse | 17 import optparse | 
| 17 import os | 18 import os | 
| 18 import pprint | 19 import pprint | 
| 19 import re | 20 import re | 
| 20 import shutil | 21 import shutil | 
| 21 import subprocess | 22 import subprocess | 
| 22 import sys | 23 import sys | 
| 23 import tempfile | 24 import tempfile | 
| 25 import time | |
| 26 | |
| 27 import binary_size_utils | |
| 28 | |
| 29 elf_symbolizer_path = os.path.abspath(os.path.join( | |
| 30 os.path.dirname(__file__), | |
| 31 '..', | |
| 32 '..', | |
| 33 'build', | |
| 34 'android', | |
| 35 'pylib')) | |
| 36 sys.path.append(elf_symbolizer_path) | |
| 37 import symbols.elf_symbolizer as elf_symbolizer | |
| 24 | 38 | 
| 25 | 39 | 
| 26 # TODO(andrewhayden): Only used for legacy reports. Delete. | 40 # TODO(andrewhayden): Only used for legacy reports. Delete. | 
| 27 def FormatBytes(bytes): | 41 def FormatBytes(bytes): | 
| 28 """Pretty-print a number of bytes.""" | 42 """Pretty-print a number of bytes.""" | 
| 29 if bytes > 1e6: | 43 if bytes > 1e6: | 
| 30 bytes = bytes / 1.0e6 | 44 bytes = bytes / 1.0e6 | 
| 31 return '%.1fm' % bytes | 45 return '%.1fm' % bytes | 
| 32 if bytes > 1e3: | 46 if bytes > 1e3: | 
| 33 bytes = bytes / 1.0e3 | 47 bytes = bytes / 1.0e3 | 
| 34 return '%.1fk' % bytes | 48 return '%.1fk' % bytes | 
| 35 return str(bytes) | 49 return str(bytes) | 
| 36 | 50 | 
| 37 | 51 | 
| 38 # TODO(andrewhayden): Only used for legacy reports. Delete. | 52 # TODO(andrewhayden): Only used for legacy reports. Delete. | 
| 39 def SymbolTypeToHuman(type): | 53 def SymbolTypeToHuman(type): | 
| 40 """Convert a symbol type as printed by nm into a human-readable name.""" | 54 """Convert a symbol type as printed by nm into a human-readable name.""" | 
| 41 return {'b': 'bss', | 55 return {'b': 'bss', | 
| 42 'd': 'data', | 56 'd': 'data', | 
| 43 'r': 'read-only data', | 57 'r': 'read-only data', | 
| 44 't': 'code', | 58 't': 'code', | 
| 45 'w': 'weak symbol', | 59 'w': 'weak symbol', | 
| 46 'v': 'weak symbol'}[type] | 60 'v': 'weak symbol'}[type] | 
| 47 | 61 | 
| 48 | 62 | 
| 49 def ParseNm(input): | |
| 50 """Parse nm output. | |
| 51 | |
| 52 Argument: an iterable over lines of nm output. | |
| 53 | |
| 54 Yields: (symbol name, symbol type, symbol size, source file path). | |
| 55 Path may be None if nm couldn't figure out the source file. | |
| 56 """ | |
| 57 | |
| 58 # Match lines with size, symbol, optional location, optional discriminator | |
| 59 sym_re = re.compile(r'^[0-9a-f]{8} ' # address (8 hex digits) | |
| 60 '([0-9a-f]{8}) ' # size (8 hex digits) | |
| 61 '(.) ' # symbol type, one character | |
| 62 '([^\t]+)' # symbol name, separated from next by tab | |
| 63 '(?:\t(.*):[\d\?]+)?.*$') # location | |
| 64 # Match lines with addr but no size. | |
| 65 addr_re = re.compile(r'^[0-9a-f]{8} (.) ([^\t]+)(?:\t.*)?$') | |
| 66 # Match lines that don't have an address at all -- typically external symbols. | |
| 67 noaddr_re = re.compile(r'^ {8} (.) (.*)$') | |
| 68 | |
| 69 for line in input: | |
| 70 line = line.rstrip() | |
| 71 match = sym_re.match(line) | |
| 72 if match: | |
| 73 size, type, sym = match.groups()[0:3] | |
| 74 size = int(size, 16) | |
| 75 if type.lower() == 'b': | |
| 76 continue # skip all BSS for now | |
| 77 path = match.group(4) | |
| 78 yield sym, type, size, path | |
| 79 continue | |
| 80 match = addr_re.match(line) | |
| 81 if match: | |
| 82 type, sym = match.groups()[0:2] | |
| 83 # No size == we don't care. | |
| 84 continue | |
| 85 match = noaddr_re.match(line) | |
| 86 if match: | |
| 87 type, sym = match.groups() | |
| 88 if type in ('U', 'w'): | |
| 89 # external or weak symbol | |
| 90 continue | |
| 91 | |
| 92 print >>sys.stderr, 'unparsed:', repr(line) | |
| 93 | |
| 94 | |
| 95 def _MkChild(node, name): | 63 def _MkChild(node, name): | 
| 
Primiano Tucci (use gerrit)
2014/05/20 15:22:57
_MkChild looks a "long"cut for
return node.setdefa
 
Daniel Bratell
2014/05/21 08:42:13
Hmm, yes. Border case. Using setdefault forces you
 | |
| 96 child = None | 64 child = node['children'].get(name) | 
| 97 for test in node['children']: | 65 if child is None: | 
| 98 if test['n'] == name: | 66 child = {'n': name, 'children': {}} | 
| 99 child = test | 67 node['children'][name] = child | 
| 100 break | |
| 101 if not child: | |
| 102 child = {'n': name, 'children': []} | |
| 103 node['children'].append(child) | |
| 104 return child | 68 return child | 
| 105 | 69 | 
| 106 | 70 | 
| 71 def MakeChildrenDictsIntoLists(node): | |
| 72 largest_list_len = 0 | |
| 73 if 'children' in node: | |
| 74 largest_list_len = len(node['children']) | |
| 75 child_list = [] | |
| 76 for child in node['children'].itervalues(): | |
| 77 child_largest_list_len = MakeChildrenDictsIntoLists(child) | |
| 78 if child_largest_list_len > largest_list_len: | |
| 79 largest_list_len = child_largest_list_len | |
| 80 child_list.append(child) | |
| 81 node['children'] = child_list | |
| 82 | |
| 83 return largest_list_len | |
| 84 | |
| 
Primiano Tucci (use gerrit)
2014/05/20 15:22:57
Nit: add extra newline
 | |
| 107 def MakeCompactTree(symbols): | 85 def MakeCompactTree(symbols): | 
| 108 result = {'n': '/', 'children': [], 'k': 'p', 'maxDepth': 0} | 86 result = {'n': '/', 'children': {}, 'k': 'p', 'maxDepth': 0} | 
| 87 seen_symbol_with_path = False | |
| 109 for symbol_name, symbol_type, symbol_size, file_path in symbols: | 88 for symbol_name, symbol_type, symbol_size, file_path in symbols: | 
| 110 | 89 | 
| 111 if 'vtable for ' in symbol_name: | 90 if 'vtable for ' in symbol_name: | 
| 112 symbol_type = '@' # hack to categorize these separately | 91 symbol_type = '@' # hack to categorize these separately | 
| 113 # Take path like '/foo/bar/baz', convert to ['foo', 'bar', 'baz'] | 92 # Take path like '/foo/bar/baz', convert to ['foo', 'bar', 'baz'] | 
| 114 if file_path: | 93 if file_path: | 
| 115 file_path = os.path.normpath(file_path) | 94 file_path = os.path.normpath(file_path) | 
| 95 seen_symbol_with_path = True | |
| 116 else: | 96 else: | 
| 117 file_path = '(No Path)' | 97 file_path = '(No Path)' | 
| 118 | 98 | 
| 119 if file_path.startswith('/'): | 99 if file_path.startswith('/'): | 
| 120 file_path = file_path[1:] | 100 file_path = file_path[1:] | 
| 121 path_parts = file_path.split('/') | 101 path_parts = file_path.split('/') | 
| 122 | 102 | 
| 123 # Find pre-existing node in tree, or update if it already exists | 103 # Find pre-existing node in tree, or update if it already exists | 
| 124 node = result | 104 node = result | 
| 125 depth = 0 | 105 depth = 0 | 
| 126 while len(path_parts) > 0: | 106 while len(path_parts) > 0: | 
| 127 path_part = path_parts.pop(0) | 107 path_part = path_parts.pop(0) | 
| 128 if len(path_part) == 0: | 108 if len(path_part) == 0: | 
| 129 continue | 109 continue | 
| 130 depth += 1 | 110 depth += 1 | 
| 131 node = _MkChild(node, path_part); | 111 node = _MkChild(node, path_part); | 
| 112 assert not 'k' in node or node['k'] == 'p' | |
| 132 node['k'] = 'p' # p for path | 113 node['k'] = 'p' # p for path | 
| 133 | 114 | 
| 134 # 'node' is now the file node. Find the symbol-type bucket. | 115 # 'node' is now the file node. Find the symbol-type bucket. | 
| 135 node['lastPathElement'] = True | 116 node['lastPathElement'] = True | 
| 136 node = _MkChild(node, symbol_type) | 117 node = _MkChild(node, symbol_type) | 
| 118 assert not 'k' in node or node['k'] == 'b' | |
| 137 node['t'] = symbol_type | 119 node['t'] = symbol_type | 
| 138 node['k'] = 'b' # b for bucket | 120 node['k'] = 'b' # b for bucket | 
| 139 depth += 1 | 121 depth += 1 | 
| 140 | 122 | 
| 141 # 'node' is now the symbol-type bucket. Make the child entry. | 123 # 'node' is now the symbol-type bucket. Make the child entry. | 
| 142 node = _MkChild(node, symbol_name) | 124 node = _MkChild(node, symbol_name) | 
| 143 if 'children' in node: # Only possible if we're adding duplicate entries!!! | 125 if 'children' in node: | 
| 126 if node['children']: | |
| 127 print('A container node used as symbol for %s.' % symbol_name) | |
| 128 # This is going to be used as a leaf so no use for child list. | |
| 144 del node['children'] | 129 del node['children'] | 
| 145 node['value'] = symbol_size | 130 node['value'] = symbol_size | 
| 146 node['t'] = symbol_type | 131 node['t'] = symbol_type | 
| 147 node['k'] = 's' # s for symbol | 132 node['k'] = 's' # s for symbol | 
| 148 depth += 1 | 133 depth += 1 | 
| 149 result['maxDepth'] = max(result['maxDepth'], depth); | 134 result['maxDepth'] = max(result['maxDepth'], depth); | 
| 150 | 135 | 
| 136 if not seen_symbol_with_path: | |
| 137 print('Symbols lack paths. Data will not be structured.') | |
| 
Primiano Tucci (use gerrit)
2014/05/20 15:22:57
shouldn't all these prints be logging.warning/erro
 | |
| 138 | |
| 139 largest_list_len = MakeChildrenDictsIntoLists(result) | |
| 140 | |
| 141 if largest_list_len > 1000: | |
| 142 print('There are sections with %d nodes. Results might be unusable.' % | |
| 143 largest_list_len) | |
| 151 return result | 144 return result | 
| 152 | 145 | 
| 153 | 146 | 
| 154 # TODO(andrewhayden): Only used for legacy reports. Delete. | 147 # TODO(andrewhayden): Only used for legacy reports. Delete. | 
| 155 def TreeifySymbols(symbols): | 148 def TreeifySymbols(symbols): | 
| 156 """Convert symbols into a path-based tree, calculating size information | 149 """Convert symbols into a path-based tree, calculating size information | 
| 157 along the way. | 150 along the way. | 
| 158 | 151 | 
| 159 The result is a dictionary that contains two kinds of nodes: | 152 The result is a dictionary that contains two kinds of nodes: | 
| 160 1. Leaf nodes, representing source code locations (e.g., c++ files) | 153 1. Leaf nodes, representing source code locations (e.g., c++ files) | 
| (...skipping 117 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 278 children.sort(key=lambda child: -child['data']['$area']) | 271 children.sort(key=lambda child: -child['data']['$area']) | 
| 279 | 272 | 
| 280 # For leaf nodes, the 'size' attribute is the size of the leaf; | 273 # For leaf nodes, the 'size' attribute is the size of the leaf; | 
| 281 # Non-leaf nodes don't really have a size, but their 'size' attribute is | 274 # Non-leaf nodes don't really have a size, but their 'size' attribute is | 
| 282 # the sum of the sizes of all their children. | 275 # the sum of the sizes of all their children. | 
| 283 return {'name': name + ' (' + FormatBytes(tree['size']) + ')', | 276 return {'name': name + ' (' + FormatBytes(tree['size']) + ')', | 
| 284 'data': { '$area': tree['size'] }, | 277 'data': { '$area': tree['size'] }, | 
| 285 'children': children } | 278 'children': children } | 
| 286 | 279 | 
| 287 def DumpCompactTree(symbols, outfile): | 280 def DumpCompactTree(symbols, outfile): | 
| 288 out = open(outfile, 'w') | 281 tree_root = MakeCompactTree(symbols) | 
| 289 try: | 282 json_string = json.dumps(tree_root) | 
| 290 out.write('var tree_data = ' + json.dumps(MakeCompactTree(symbols))) | 283 print('Writing %d bytes json' % len(json_string)) | 
| 291 finally: | 284 with open(outfile, 'w') as out: | 
| 292 out.flush() | 285 out.write('var tree_data = ' + json_string) | 
| 
Primiano Tucci (use gerrit)
2014/05/20 15:22:57
I've no idea how big this string is going to be.
I
 
Daniel Bratell
2014/05/21 08:42:13
It can be big so this is a nice suggestion. Won't
 | |
| 293 out.close() | |
| 294 | 286 | 
| 295 | 287 | 
| 296 # TODO(andrewhayden): Only used for legacy reports. Delete. | 288 # TODO(andrewhayden): Only used for legacy reports. Delete. | 
| 297 def DumpTreemap(symbols, outfile): | 289 def DumpTreemap(symbols, outfile): | 
| 298 dirs = TreeifySymbols(symbols) | 290 dirs = TreeifySymbols(symbols) | 
| 299 out = open(outfile, 'w') | 291 out = open(outfile, 'w') | 
| 300 try: | 292 try: | 
| 301 out.write('var kTree = ' + json.dumps(JsonifyTree(dirs, '/'))) | 293 out.write('var kTree = ' + json.dumps(JsonifyTree(dirs, '/'))) | 
| 302 finally: | 294 finally: | 
| 303 out.flush() | 295 out.flush() | 
| (...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 390 out.write(',\n') | 382 out.write(',\n') | 
| 391 dumped += 1 | 383 dumped += 1 | 
| 392 if dumped >= n: | 384 if dumped >= n: | 
| 393 return | 385 return | 
| 394 finally: | 386 finally: | 
| 395 out.write('];\n') | 387 out.write('];\n') | 
| 396 out.flush() | 388 out.flush() | 
| 397 out.close() | 389 out.close() | 
| 398 | 390 | 
| 399 | 391 | 
| 400 # TODO(andrewhayden): Switch to Primiano's python-based version. | 392 # Regex for parsing "nm" output. A sample line looks like this: | 
| 401 def RunParallelAddress2Line(outfile, library, arch, jobs, verbose): | 393 # 0167b39c 00000018 t ACCESS_DESCRIPTION_free /path/file.c:95 | 
| 402 """Run a parallel addr2line processing engine to dump and resolve symbols.""" | 394 # | 
| 403 out_dir = os.getenv('CHROMIUM_OUT_DIR', 'out') | 395 # The fields are: address, size, type, name, source location | 
| 404 build_type = os.getenv('BUILDTYPE', 'Release') | 396 # Regular expression explained ( see also: https://xkcd.com/208 ): | 
| 405 classpath = os.path.join(out_dir, build_type, 'lib.java', | 397 # ([0-9a-f]{8,}+) The address | 
| 406 'binary_size_java.jar') | 398 # [\s]+ Whitespace separator | 
| 407 cmd = ['java', | 399 # ([0-9a-f]{8,}+) The size. From here on out it's all optional. | 
| 408 '-classpath', classpath, | 400 # [\s]+ Whitespace separator | 
| 409 'org.chromium.tools.binary_size.ParallelAddress2Line', | 401 # (\S?) The symbol type, which is any non-whitespace char | 
| 410 '--disambiguate', | 402 # [\s*] Whitespace separator | 
| 411 '--outfile', outfile, | 403 # ([^\t]*) Symbol name, any non-tab character (spaces ok!) | 
| 412 '--library', library, | 404 # [\t]? Tab separator | 
| 413 '--threads', jobs] | 405 # (.*) The location (filename[:linennum|?][ (discriminator n)] | 
| 414 if verbose is True: | 406 sNmPattern = re.compile( | 
| 415 cmd.append('--verbose') | 407 r'([0-9a-f]{8,})[\s]+([0-9a-f]{8,})[\s]*(\S?)[\s*]([^\t]*)[\t]?(.*)') | 
| 416 prefix = os.path.join('third_party', 'android_tools', 'ndk', 'toolchains') | |
| 417 if arch == 'android-arm': | |
| 418 prefix = os.path.join(prefix, 'arm-linux-androideabi-4.8', 'prebuilt', | |
| 419 'linux-x86_64', 'bin', 'arm-linux-androideabi-') | |
| 420 cmd.extend(['--nm', prefix + 'nm', '--addr2line', prefix + 'addr2line']) | |
| 421 elif arch == 'android-mips': | |
| 422 prefix = os.path.join(prefix, 'mipsel-linux-android-4.8', 'prebuilt', | |
| 423 'linux-x86_64', 'bin', 'mipsel-linux-android-') | |
| 424 cmd.extend(['--nm', prefix + 'nm', '--addr2line', prefix + 'addr2line']) | |
| 425 elif arch == 'android-x86': | |
| 426 prefix = os.path.join(prefix, 'x86-4.8', 'prebuilt', | |
| 427 'linux-x86_64', 'bin', 'i686-linux-android-') | |
| 428 cmd.extend(['--nm', prefix + 'nm', '--addr2line', prefix + 'addr2line']) | |
| 429 # else, use whatever is in PATH (don't pass --nm or --addr2line) | |
| 430 | 408 | 
| 431 if verbose: | 409 class Progress(): | 
| 432 print cmd | 410 def __init__(self): | 
| 433 | 411 self.count = 0 | 
| 434 return_code = subprocess.call(cmd) | 412 self.skip_count = 0 | 
| 435 if return_code: | 413 self.collisions = 0 | 
| 436 raise RuntimeError('Failed to run ParallelAddress2Line: returned ' + | 414 self.time_last_output = time.time() | 
| 437 str(return_code)) | 415 self.count_last_output = 0 | 
| 438 | 416 | 
| 439 | 417 | 
| 440 def GetNmSymbols(infile, outfile, library, arch, jobs, verbose): | 418 def RunElfSymbolizer(outfile, library, addr2line_binary, nm_binary): | 
| 441 if infile is None: | 419 nm_output = run_nm(library, nm_binary) | 
| 420 nm_output_lines = nm_output.splitlines() | |
| 421 nm_output_lines_len = len(nm_output_lines) | |
| 422 address_symbol = {} | |
| 423 progress = Progress() | |
| 424 def map_address_symbol(symbol, addr): | |
| 425 progress.count += 1 | |
| 426 if addr in address_symbol: | |
| 427 # 'Collision between %s and %s.' % (str(symbol.name), | |
| 428 # str(address_symbol[addr].name)) | |
| 429 progress.collisions += 1 | |
| 430 else: | |
| 431 address_symbol[addr] = symbol | |
| 432 | |
| 433 progress_chunk = 100 | |
| 434 if progress.count % progress_chunk == 0: | |
| 435 time_now = time.time() | |
| 436 time_spent = time_now - progress.time_last_output | |
| 437 if time_spent > 1.0: | |
| 438 # Only output at most once per second. | |
| 439 progress.time_last_output = time_now | |
| 440 chunk_size = progress.count - progress.count_last_output | |
| 441 progress.count_last_output = progress.count | |
| 442 if time_spent > 0: | |
| 443 speed = chunk_size / time_spent | |
| 444 else: | |
| 445 speed = 0 | |
| 446 progress_percent = (100.0 * (progress.count + progress.skip_count) / | |
| 447 nm_output_lines_len) | |
| 448 print('%.1f%%: Looked up %d symbols (%d collisions) - %.1f lookups/s.' % | |
| 449 (progress_percent, progress.count, progress.collisions, speed)) | |
| 450 | |
| 451 symbolizer = elf_symbolizer.ELFSymbolizer(library, addr2line_binary, | |
| 452 map_address_symbol, | |
| 453 max_concurrent_jobs=2) | |
| 454 for line in nm_output_lines: | |
| 455 match = sNmPattern.match(line) | |
| 456 if match: | |
| 457 location = match.group(5) | |
| 458 if not location: | |
| 459 addr = int(match.group(1), 16) | |
| 460 size = int(match.group(2), 16) | |
| 461 if addr in address_symbol: # Already looked up, shortcut ELFSymbolizer. | |
| 462 map_address_symbol(address_symbol[addr], addr) | |
| 463 continue | |
| 464 elif size == 0: | |
| 465 # Save time by not looking up empty symbols (do they even exist?) | |
| 466 print('Empty symbol: ' + line) | |
| 467 else: | |
| 468 symbolizer.SymbolizeAsync(addr, addr) | |
| 469 continue | |
| 470 | |
| 471 progress.skip_count += 1 | |
| 472 | |
| 473 symbolizer.Join() | |
| 474 | |
| 475 with open(outfile, 'w') as out: | |
| 476 for line in nm_output_lines: | |
| 477 match = sNmPattern.match(line) | |
| 478 if match: | |
| 479 location = match.group(5) | |
| 480 if not location: | |
| 481 addr = int(match.group(1), 16) | |
| 482 symbol = address_symbol[addr] | |
| 483 path = '??' | |
| 484 if symbol.source_path is not None: | |
| 485 path = symbol.source_path | |
| 486 line_number = 0 | |
| 487 if symbol.source_line is not None: | |
| 488 line_number = symbol.source_line | |
| 489 out.write('%s\t%s:%d\n' % (line, path, line_number)) | |
| 490 continue | |
| 491 | |
| 492 out.write('%s\n' % line) | |
| 493 | |
| 494 print('%d symbols in the results.' % len(address_symbol)) | |
| 495 | |
| 496 | |
| 497 def run_nm(binary, nm_binary): | |
| 
Primiano Tucci (use gerrit)
2014/05/20 15:22:57
nit: RunNm
 | |
| 498 print('Starting nm') | |
| 499 cmd = [nm_binary, '-C', '--print-size', binary] | |
| 500 nm_process = subprocess.Popen(cmd, | |
| 501 stdout=subprocess.PIPE, | |
| 502 stderr=subprocess.PIPE) | |
| 503 (process_output, err_output) = nm_process.communicate() | |
| 504 | |
| 505 if nm_process.returncode != 0: | |
| 506 if err_output: | |
| 507 raise Exception, err_output | |
| 508 else: | |
| 509 raise Exception, process_output | |
| 510 | |
| 511 print('Finished nm') | |
| 512 return process_output | |
| 513 | |
| 514 | |
| 515 def GetNmSymbols(nm_infile, outfile, library, jobs, verbose, | |
| 516 addr2line_binary, nm_binary): | |
| 517 if nm_infile is None: | |
| 442 if outfile is None: | 518 if outfile is None: | 
| 443 infile = tempfile.NamedTemporaryFile(delete=False).name | 519 outfile = tempfile.NamedTemporaryFile(delete=False).name | 
| 444 else: | |
| 445 infile = outfile | |
| 446 | 520 | 
| 447 if verbose: | 521 if verbose: | 
| 448 print 'Running parallel addr2line, dumping symbols to ' + infile; | 522 print 'Running parallel addr2line, dumping symbols to ' + outfile; | 
| 449 RunParallelAddress2Line(outfile=infile, library=library, arch=arch, | 523 RunElfSymbolizer(outfile, library, addr2line_binary, nm_binary) | 
| 450 jobs=jobs, verbose=verbose) | 524 | 
| 525 nm_infile = outfile | |
| 526 | |
| 451 elif verbose: | 527 elif verbose: | 
| 452 print 'Using nm input from ' + infile | 528 print 'Using nm input from ' + nm_infile | 
| 453 with file(infile, 'r') as infile: | 529 with file(nm_infile, 'r') as infile: | 
| 454 return list(ParseNm(infile)) | 530 return list(binary_size_utils.ParseNm(infile)) | 
| 531 | |
| 532 | |
| 533 def _find_in_system_path(binary): | |
| 534 """Locate the full path to binary in the system path or return None | |
| 535 if not found.""" | |
| 536 system_path = os.environ["PATH"].split(os.pathsep) | |
| 537 for path in system_path: | |
| 538 binary_path = os.path.join(path, binary) | |
| 539 if os.path.isfile(binary_path): | |
| 540 return binary_path | |
| 541 return None | |
| 455 | 542 | 
| 456 | 543 | 
| 457 def main(): | 544 def main(): | 
| 458 usage="""%prog [options] | 545 usage="""%prog [options] | 
| 459 | 546 | 
| 460 Runs a spatial analysis on a given library, looking up the source locations | 547 Runs a spatial analysis on a given library, looking up the source locations | 
| 461 of its symbols and calculating how much space each directory, source file, | 548 of its symbols and calculating how much space each directory, source file, | 
| 462 and so on is taking. The result is a report that can be used to pinpoint | 549 and so on is taking. The result is a report that can be used to pinpoint | 
| 463 sources of large portions of the binary, etceteras. | 550 sources of large portions of the binary, etceteras. | 
| 464 | 551 | 
| (...skipping 14 matching lines...) Expand all Loading... | |
| 479 'present in the file; i.e., no addr2line symbol lookups ' | 566 'present in the file; i.e., no addr2line symbol lookups ' | 
| 480 'will be performed when this option is specified. ' | 567 'will be performed when this option is specified. ' | 
| 481 'Mutually exclusive with --library.') | 568 'Mutually exclusive with --library.') | 
| 482 parser.add_option('--destdir', metavar='PATH', | 569 parser.add_option('--destdir', metavar='PATH', | 
| 483 help='write output to the specified directory. An HTML ' | 570 help='write output to the specified directory. An HTML ' | 
| 484 'report is generated here along with supporting files; ' | 571 'report is generated here along with supporting files; ' | 
| 485 'any existing report will be overwritten.') | 572 'any existing report will be overwritten.') | 
| 486 parser.add_option('--library', metavar='PATH', | 573 parser.add_option('--library', metavar='PATH', | 
| 487 help='if specified, process symbols in the library at ' | 574 help='if specified, process symbols in the library at ' | 
| 488 'the specified path. Mutually exclusive with --nm-in.') | 575 'the specified path. Mutually exclusive with --nm-in.') | 
| 489 parser.add_option('--arch', | 576 parser.add_option('--nm-binary', | 
| 490 help='the architecture that the library is targeted to. ' | 577 help='use the specified nm binary to analyze library. ' | 
| 491 'Determines which nm/addr2line binaries are used. When ' | 578 'This is to be used when the nm in the path is not for ' | 
| 492 '\'host-native\' is chosen, the program will use whichever ' | 579 'the right architecture or of the right version.') | 
| 493 'nm/addr2line binaries are on the PATH. This is ' | 580 parser.add_option('--addr2line-binary', | 
| 494 'appropriate when you are analyzing a binary by and for ' | 581 help='use the specified addr2line binary to analyze ' | 
| 495 'your computer. ' | 582 'library. This is to be used when the addr2line in ' | 
| 496 'This argument is only valid when using --library. ' | 583 'the path is not for the right architecture or ' | 
| 497 'Default is \'host-native\'.', | 584 'of the right version.') | 
| 498 choices=['host-native', 'android-arm', | |
| 499 'android-mips', 'android-x86'],) | |
| 500 parser.add_option('--jobs', | 585 parser.add_option('--jobs', | 
| 501 help='number of jobs to use for the parallel ' | 586 help='number of jobs to use for the parallel ' | 
| 502 'addr2line processing pool; defaults to 1. More ' | 587 'addr2line processing pool; defaults to 1. More ' | 
| 503 'jobs greatly improve throughput but eat RAM like ' | 588 'jobs greatly improve throughput but eat RAM like ' | 
| 504 'popcorn, and take several gigabytes each. Start low ' | 589 'popcorn, and take several gigabytes each. Start low ' | 
| 505 'and ramp this number up until your machine begins to ' | 590 'and ramp this number up until your machine begins to ' | 
| 506 'struggle with RAM. ' | 591 'struggle with RAM. ' | 
| 507 'This argument is only valid when using --library.') | 592 'This argument is only valid when using --library.') | 
| 508 parser.add_option('-v', dest='verbose', action='store_true', | 593 parser.add_option('-v', dest='verbose', action='store_true', | 
| 509 help='be verbose, printing lots of status information.') | 594 help='be verbose, printing lots of status information.') | 
| 510 parser.add_option('--nm-out', metavar='PATH', | 595 parser.add_option('--nm-out', metavar='PATH', | 
| 511 help='keep the nm output file, and store it at the ' | 596 help='keep the nm output file, and store it at the ' | 
| 512 'specified path. This is useful if you want to see the ' | 597 'specified path. This is useful if you want to see the ' | 
| 513 'fully processed nm output after the symbols have been ' | 598 'fully processed nm output after the symbols have been ' | 
| 514 'mapped to source locations. By default, a tempfile is ' | 599 'mapped to source locations. By default, a tempfile is ' | 
| 515 'used and is deleted when the program terminates.' | 600 'used and is deleted when the program terminates.' | 
| 516 'This argument is only valid when using --library.') | 601 'This argument is only valid when using --library.') | 
| 517 parser.add_option('--legacy', action='store_true', | 602 parser.add_option('--legacy', action='store_true', | 
| 518 help='emit legacy binary size report instead of modern') | 603 help='emit legacy binary size report instead of modern') | 
| 519 opts, args = parser.parse_args() | 604 opts, args = parser.parse_args() | 
| 520 | 605 | 
| 521 if ((not opts.library) and (not opts.nm_in)) or (opts.library and opts.nm_in): | 606 if ((not opts.library) and (not opts.nm_in)) or (opts.library and opts.nm_in): | 
| 522 parser.error('exactly one of --library or --nm-in is required') | 607 parser.error('exactly one of --library or --nm-in is required') | 
| 523 if (opts.nm_in): | 608 if (opts.nm_in): | 
| 524 if opts.jobs: | 609 if opts.jobs: | 
| 525 print >> sys.stderr, ('WARNING: --jobs has no effect ' | 610 print >> sys.stderr, ('WARNING: --jobs has no effect ' | 
| 526 'when used with --nm-in') | 611 'when used with --nm-in') | 
| 527 if opts.arch: | |
| 528 print >> sys.stderr, ('WARNING: --arch has no effect ' | |
| 529 'when used with --nm-in') | |
| 530 if not opts.destdir: | 612 if not opts.destdir: | 
| 531 parser.error('--destdir is required argument') | 613 parser.error('--destdir is required argument') | 
| 532 if not opts.jobs: | 614 if not opts.jobs: | 
| 533 opts.jobs = '1' | 615 opts.jobs = str(multiprocessing.cpu_count()) | 
| 534 if not opts.arch: | |
| 535 opts.arch = 'host-native' | |
| 536 | 616 | 
| 537 symbols = GetNmSymbols(opts.nm_in, opts.nm_out, opts.library, opts.arch, | 617 if opts.addr2line_binary: | 
| 538 opts.jobs, opts.verbose is True) | 618 assert os.path.isfile(opts.addr2line_binary) | 
| 619 addr2line_binary = opts.addr2line_binary | |
| 620 else: | |
| 621 addr2line_binary = _find_in_system_path('addr2line') | |
| 622 assert addr2line_binary, 'Unable to find addr2line in the path. '\ | |
| 623 'Use --addr2line-binary to specify location.' | |
| 624 | |
| 625 if opts.nm_binary: | |
| 626 assert os.path.isfile(opts.nm_binary) | |
| 627 nm_binary = opts.nm_binary | |
| 628 else: | |
| 629 nm_binary = _find_in_system_path('nm') | |
| 630 assert nm_binary, 'Unable to find nm in the path. Use --nm-binary '\ | |
| 631 'to specify location.' | |
| 632 | |
| 633 print('nm: %s' % nm_binary) | |
| 634 print('addr2line: %s' % addr2line_binary) | |
| 635 | |
| 636 symbols = GetNmSymbols(opts.nm_in, opts.nm_out, opts.library, | |
| 637 opts.jobs, opts.verbose is True, | |
| 638 addr2line_binary, nm_binary) | |
| 539 if not os.path.exists(opts.destdir): | 639 if not os.path.exists(opts.destdir): | 
| 540 os.makedirs(opts.destdir, 0755) | 640 os.makedirs(opts.destdir, 0755) | 
| 541 | 641 | 
| 542 | 642 | 
| 543 if opts.legacy: # legacy report | 643 if opts.legacy: # legacy report | 
| 544 DumpTreemap(symbols, os.path.join(opts.destdir, 'treemap-dump.js')) | 644 DumpTreemap(symbols, os.path.join(opts.destdir, 'treemap-dump.js')) | 
| 545 DumpLargestSymbols(symbols, | 645 DumpLargestSymbols(symbols, | 
| 546 os.path.join(opts.destdir, 'largest-symbols.js'), 100) | 646 os.path.join(opts.destdir, 'largest-symbols.js'), 100) | 
| 547 DumpLargestSources(symbols, | 647 DumpLargestSources(symbols, | 
| 548 os.path.join(opts.destdir, 'largest-sources.js'), 100) | 648 os.path.join(opts.destdir, 'largest-sources.js'), 100) | 
| 549 DumpLargestVTables(symbols, | 649 DumpLargestVTables(symbols, | 
| 550 os.path.join(opts.destdir, 'largest-vtables.js'), 100) | 650 os.path.join(opts.destdir, 'largest-vtables.js'), 100) | 
| 551 treemap_out = os.path.join(opts.destdir, 'webtreemap') | 651 treemap_out = os.path.join(opts.destdir, 'webtreemap') | 
| 552 if not os.path.exists(treemap_out): | 652 if not os.path.exists(treemap_out): | 
| 553 os.makedirs(treemap_out, 0755) | 653 os.makedirs(treemap_out, 0755) | 
| 554 treemap_src = os.path.join('third_party', 'webtreemap', 'src') | 654 treemap_src = os.path.join('third_party', 'webtreemap', 'src') | 
| 555 shutil.copy(os.path.join(treemap_src, 'COPYING'), treemap_out) | 655 shutil.copy(os.path.join(treemap_src, 'COPYING'), treemap_out) | 
| 556 shutil.copy(os.path.join(treemap_src, 'webtreemap.js'), treemap_out) | 656 shutil.copy(os.path.join(treemap_src, 'webtreemap.js'), treemap_out) | 
| 557 shutil.copy(os.path.join(treemap_src, 'webtreemap.css'), treemap_out) | 657 shutil.copy(os.path.join(treemap_src, 'webtreemap.css'), treemap_out) | 
| 558 shutil.copy(os.path.join('tools', 'binary_size', 'legacy_template', | 658 shutil.copy(os.path.join('tools', 'binary_size', 'legacy_template', | 
| 559 'index.html'), opts.destdir) | 659 'index.html'), opts.destdir) | 
| 560 else: # modern report | 660 else: # modern report | 
| 561 DumpCompactTree(symbols, os.path.join(opts.destdir, 'data.js')) | 661 DumpCompactTree(symbols, os.path.join(opts.destdir, 'data.js')) | 
| 562 d3_out = os.path.join(opts.destdir, 'd3') | 662 d3_out = os.path.join(opts.destdir, 'd3') | 
| 563 if not os.path.exists(d3_out): | 663 if not os.path.exists(d3_out): | 
| 564 os.makedirs(d3_out, 0755) | 664 os.makedirs(d3_out, 0755) | 
| 565 d3_src = os.path.join('third_party', 'd3', 'src') | 665 d3_src = os.path.join(os.path.dirname(__file__), | 
| 566 template_src = os.path.join('tools', 'binary_size', | 666 '..', | 
| 667 '..', | |
| 668 'third_party', 'd3', 'src') | |
| 669 template_src = os.path.join(os.path.dirname(__file__), | |
| 567 'template') | 670 'template') | 
| 568 shutil.copy(os.path.join(d3_src, 'LICENSE'), d3_out) | 671 shutil.copy(os.path.join(d3_src, 'LICENSE'), d3_out) | 
| 569 shutil.copy(os.path.join(d3_src, 'd3.js'), d3_out) | 672 shutil.copy(os.path.join(d3_src, 'd3.js'), d3_out) | 
| 673 print('Copying index.html') | |
| 570 shutil.copy(os.path.join(template_src, 'index.html'), opts.destdir) | 674 shutil.copy(os.path.join(template_src, 'index.html'), opts.destdir) | 
| 571 shutil.copy(os.path.join(template_src, 'D3SymbolTreeMap.js'), opts.destdir) | 675 shutil.copy(os.path.join(template_src, 'D3SymbolTreeMap.js'), opts.destdir) | 
| 572 | 676 | 
| 573 if opts.verbose: | 677 if opts.verbose: | 
| 574 print 'Report saved to ' + opts.destdir + '/index.html' | 678 print 'Report saved to ' + opts.destdir + '/index.html' | 
| 575 | 679 | 
| 576 | 680 | 
| 577 if __name__ == '__main__': | 681 if __name__ == '__main__': | 
| 578 sys.exit(main()) | 682 sys.exit(main()) | 
| OLD | NEW |