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 |