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 |