OLD | NEW |
1 # Copyright 2017 The Chromium Authors. All rights reserved. | 1 # Copyright 2017 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Main Python API for analyzing binary size.""" | 5 """Main Python API for analyzing binary size.""" |
6 | 6 |
7 import argparse | 7 import argparse |
8 import calendar | 8 import calendar |
9 import collections | 9 import collections |
10 import datetime | 10 import datetime |
11 import gzip | 11 import gzip |
12 import logging | 12 import logging |
13 import os | 13 import os |
| 14 import multiprocessing |
| 15 import posixpath |
14 import re | 16 import re |
15 import subprocess | 17 import subprocess |
16 import sys | 18 import sys |
| 19 import tempfile |
| 20 import zipfile |
17 | 21 |
18 import describe | 22 import describe |
19 import file_format | 23 import file_format |
20 import function_signature | 24 import function_signature |
21 import helpers | 25 import helpers |
22 import linker_map_parser | 26 import linker_map_parser |
23 import models | 27 import models |
24 import ninja_parser | 28 import ninja_parser |
25 import paths | 29 import paths |
26 | 30 |
(...skipping 171 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
198 'Symbol has negative size (likely not sorted propertly): ' | 202 'Symbol has negative size (likely not sorted propertly): ' |
199 '%r\nprev symbol: %r' % (symbol, prev_symbol)) | 203 '%r\nprev symbol: %r' % (symbol, prev_symbol)) |
200 | 204 |
201 | 205 |
202 def _ClusterSymbols(symbols): | 206 def _ClusterSymbols(symbols): |
203 """Returns a new list of symbols with some symbols moved into groups. | 207 """Returns a new list of symbols with some symbols moved into groups. |
204 | 208 |
205 Groups include: | 209 Groups include: |
206 * Symbols that have [clone] in their name (created by compiler optimization). | 210 * Symbols that have [clone] in their name (created by compiler optimization). |
207 * Star symbols (such as "** merge strings", and "** symbol gap") | 211 * Star symbols (such as "** merge strings", and "** symbol gap") |
| 212 |
| 213 To view created groups: |
| 214 Print(size_info.symbols.Filter(lambda s: s.IsGroup()), recursive=True) |
208 """ | 215 """ |
209 # http://unix.stackexchange.com/questions/223013/function-symbol-gets-part-suf
fix-after-compilation | 216 # http://unix.stackexchange.com/questions/223013/function-symbol-gets-part-suf
fix-after-compilation |
210 # Example name suffixes: | 217 # Example name suffixes: |
211 # [clone .part.322] | 218 # [clone .part.322] # GCC |
212 # [clone .isra.322] | 219 # [clone .isra.322] # GCC |
213 # [clone .constprop.1064] | 220 # [clone .constprop.1064] # GCC |
| 221 # [clone .11064] # clang |
214 | 222 |
215 # Step 1: Create name map, find clones, collect star syms into replacements. | 223 # Step 1: Create name map, find clones, collect star syms into replacements. |
216 logging.debug('Creating name -> symbol map') | 224 logging.debug('Creating name -> symbol map') |
217 clone_indices = [] | 225 clone_indices = [] |
218 indices_by_full_name = {} | 226 indices_by_full_name = {} |
219 # (name, full_name) -> [(index, sym),...] | 227 # (name, full_name) -> [(index, sym),...] |
220 replacements_by_name = collections.defaultdict(list) | 228 replacements_by_name = collections.defaultdict(list) |
221 for i, symbol in enumerate(symbols): | 229 for i, symbol in enumerate(symbols): |
222 if symbol.name.startswith('**'): | 230 if symbol.name.startswith('**'): |
223 # "symbol gap 3" -> "symbol gaps" | 231 # "symbol gap 3" -> "symbol gaps" |
(...skipping 120 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
344 size_info = models.SizeInfo(section_sizes, raw_symbols) | 352 size_info = models.SizeInfo(section_sizes, raw_symbols) |
345 | 353 |
346 # Name normalization not strictly required, but makes for smaller files. | 354 # Name normalization not strictly required, but makes for smaller files. |
347 if raw_only: | 355 if raw_only: |
348 logging.info('Normalizing symbol names') | 356 logging.info('Normalizing symbol names') |
349 _NormalizeNames(size_info.raw_symbols) | 357 _NormalizeNames(size_info.raw_symbols) |
350 else: | 358 else: |
351 _PostProcessSizeInfo(size_info) | 359 _PostProcessSizeInfo(size_info) |
352 | 360 |
353 if logging.getLogger().isEnabledFor(logging.DEBUG): | 361 if logging.getLogger().isEnabledFor(logging.DEBUG): |
| 362 # Padding is reported in size coverage logs. |
| 363 if raw_only: |
| 364 _CalculatePadding(size_info.raw_symbols) |
354 for line in describe.DescribeSizeInfoCoverage(size_info): | 365 for line in describe.DescribeSizeInfoCoverage(size_info): |
355 logging.info(line) | 366 logging.info(line) |
356 logging.info('Recorded info for %d symbols', len(size_info.raw_symbols)) | 367 logging.info('Recorded info for %d symbols', len(size_info.raw_symbols)) |
357 return size_info | 368 return size_info |
358 | 369 |
359 | 370 |
360 def _DetectGitRevision(directory): | 371 def _DetectGitRevision(directory): |
361 try: | 372 try: |
362 git_rev = subprocess.check_output( | 373 git_rev = subprocess.check_output( |
363 ['git', '-C', directory, 'rev-parse', 'HEAD']) | 374 ['git', '-C', directory, 'rev-parse', 'HEAD']) |
(...skipping 15 matching lines...) Expand all Loading... |
379 args = [tool_prefix + 'readelf', '-S', '--wide', elf_path] | 390 args = [tool_prefix + 'readelf', '-S', '--wide', elf_path] |
380 stdout = subprocess.check_output(args) | 391 stdout = subprocess.check_output(args) |
381 section_sizes = {} | 392 section_sizes = {} |
382 # Matches [ 2] .hash HASH 00000000006681f0 0001f0 003154 04 A 3 0 8 | 393 # Matches [ 2] .hash HASH 00000000006681f0 0001f0 003154 04 A 3 0 8 |
383 for match in re.finditer(r'\[[\s\d]+\] (\..*)$', stdout, re.MULTILINE): | 394 for match in re.finditer(r'\[[\s\d]+\] (\..*)$', stdout, re.MULTILINE): |
384 items = match.group(1).split() | 395 items = match.group(1).split() |
385 section_sizes[items[0]] = int(items[4], 16) | 396 section_sizes[items[0]] = int(items[4], 16) |
386 return section_sizes | 397 return section_sizes |
387 | 398 |
388 | 399 |
| 400 def _ArchFromElf(elf_path, tool_prefix): |
| 401 args = [tool_prefix + 'readelf', '-h', elf_path] |
| 402 stdout = subprocess.check_output(args) |
| 403 return re.search('Machine:\s*(\S+)', stdout).group(1) |
| 404 |
| 405 |
389 def _ParseGnArgs(args_path): | 406 def _ParseGnArgs(args_path): |
390 """Returns a list of normalized "key=value" strings.""" | 407 """Returns a list of normalized "key=value" strings.""" |
391 args = {} | 408 args = {} |
392 with open(args_path) as f: | 409 with open(args_path) as f: |
393 for l in f: | 410 for l in f: |
394 # Strips #s even if within string literal. Not a problem in practice. | 411 # Strips #s even if within string literal. Not a problem in practice. |
395 parts = l.split('#')[0].split('=') | 412 parts = l.split('#')[0].split('=') |
396 if len(parts) != 2: | 413 if len(parts) != 2: |
397 continue | 414 continue |
398 args[parts[0].strip()] = parts[1].strip() | 415 args[parts[0].strip()] = parts[1].strip() |
399 return ["%s=%s" % x for x in sorted(args.iteritems())] | 416 return ["%s=%s" % x for x in sorted(args.iteritems())] |
400 | 417 |
401 | 418 |
| 419 def _ElfInfoFromApk(apk_path, apk_so_path, tool_prefix): |
| 420 """Returns a tuple of (build_id, section_sizes).""" |
| 421 with zipfile.ZipFile(apk_path) as apk, \ |
| 422 tempfile.NamedTemporaryFile() as f: |
| 423 f.write(apk.read(apk_so_path)) |
| 424 f.flush() |
| 425 build_id = BuildIdFromElf(f.name, tool_prefix) |
| 426 section_sizes = _SectionSizesFromElf(f.name, tool_prefix) |
| 427 return build_id, section_sizes |
| 428 |
| 429 |
402 def AddArguments(parser): | 430 def AddArguments(parser): |
403 parser.add_argument('size_file', help='Path to output .size file.') | 431 parser.add_argument('size_file', help='Path to output .size file.') |
404 parser.add_argument('--elf-file', required=True, | 432 parser.add_argument('--apk-file', |
| 433 help='.apk file to measure. When set, --elf-file will be ' |
| 434 'derived (if unset). Providing the .apk allows ' |
| 435 'for the size of packed relocations to be recorded') |
| 436 parser.add_argument('--elf-file', |
405 help='Path to input ELF file. Currently used for ' | 437 help='Path to input ELF file. Currently used for ' |
406 'capturing metadata. Pass "" to skip ' | 438 'capturing metadata.') |
407 'metadata collection.') | |
408 parser.add_argument('--map-file', | 439 parser.add_argument('--map-file', |
409 help='Path to input .map(.gz) file. Defaults to ' | 440 help='Path to input .map(.gz) file. Defaults to ' |
410 '{{elf_file}}.map(.gz)?') | 441 '{{elf_file}}.map(.gz)?. If given without ' |
| 442 '--elf-file, no size metadata will be recorded.') |
411 parser.add_argument('--no-source-paths', action='store_true', | 443 parser.add_argument('--no-source-paths', action='store_true', |
412 help='Do not use .ninja files to map ' | 444 help='Do not use .ninja files to map ' |
413 'object_path -> source_path') | 445 'object_path -> source_path') |
414 parser.add_argument('--tool-prefix', default='', | 446 parser.add_argument('--tool-prefix', default='', |
415 help='Path prefix for c++filt.') | 447 help='Path prefix for c++filt.') |
416 parser.add_argument('--output-directory', | 448 parser.add_argument('--output-directory', |
417 help='Path to the root build directory.') | 449 help='Path to the root build directory.') |
418 | 450 |
419 | 451 |
420 def Run(args, parser): | 452 def Run(args, parser): |
421 if not args.size_file.endswith('.size'): | 453 if not args.size_file.endswith('.size'): |
422 parser.error('size_file must end with .size') | 454 parser.error('size_file must end with .size') |
423 | 455 |
424 if args.map_file: | 456 elf_path = args.elf_file |
425 if (not args.map_file.endswith('.map') | 457 map_path = args.map_file |
426 and not args.map_file.endswith('.map.gz')): | 458 apk_path = args.apk_file |
| 459 any_input = apk_path or elf_path or map_path |
| 460 if not any_input: |
| 461 parser.error('Most pass at least one of --apk-file, --elf-file, --map-file') |
| 462 lazy_paths = paths.LazyPaths(args=args, input_file=any_input) |
| 463 |
| 464 if apk_path: |
| 465 with zipfile.ZipFile(apk_path) as z: |
| 466 lib_infos = [f for f in z.infolist() |
| 467 if f.filename.endswith('.so') and f.file_size > 0] |
| 468 assert lib_infos, 'APK has no .so files.' |
| 469 # TODO(agrieve): Add support for multiple .so files, and take into account |
| 470 # secondary architectures. |
| 471 apk_so_path = max(lib_infos, key=lambda x:x.file_size).filename |
| 472 logging.debug('Sub-apk path=%s', apk_so_path) |
| 473 if not elf_path: |
| 474 elf_path = os.path.join( |
| 475 lazy_paths.output_directory, 'lib.unstripped', |
| 476 os.path.basename(apk_so_path.replace('crazy.', ''))) |
| 477 logging.debug('Detected --elf-file=%s', elf_path) |
| 478 |
| 479 if map_path: |
| 480 if not map_path.endswith('.map') and not map_path.endswith('.map.gz'): |
427 parser.error('Expected --map-file to end with .map or .map.gz') | 481 parser.error('Expected --map-file to end with .map or .map.gz') |
428 map_file_path = args.map_file | |
429 else: | 482 else: |
430 map_file_path = args.elf_file + '.map' | 483 map_path = elf_path + '.map' |
431 if not os.path.exists(map_file_path): | 484 if not os.path.exists(map_path): |
432 map_file_path += '.gz' | 485 map_path += '.gz' |
433 if not os.path.exists(map_file_path): | 486 if not os.path.exists(map_path): |
434 parser.error('Could not find .map(.gz)? file. Use --map-file.') | 487 parser.error('Could not find .map(.gz)? file. Use --map-file.') |
435 | 488 |
436 lazy_paths = paths.LazyPaths(args=args, input_file=args.elf_file) | |
437 metadata = None | 489 metadata = None |
438 if args.elf_file: | 490 if elf_path: |
439 logging.debug('Constructing metadata') | 491 logging.debug('Constructing metadata') |
440 git_rev = _DetectGitRevision(os.path.dirname(args.elf_file)) | 492 git_rev = _DetectGitRevision(os.path.dirname(elf_path)) |
441 build_id = BuildIdFromElf(args.elf_file, lazy_paths.tool_prefix) | 493 architecture = _ArchFromElf(elf_path, lazy_paths.tool_prefix) |
| 494 build_id = BuildIdFromElf(elf_path, lazy_paths.tool_prefix) |
442 timestamp_obj = datetime.datetime.utcfromtimestamp(os.path.getmtime( | 495 timestamp_obj = datetime.datetime.utcfromtimestamp(os.path.getmtime( |
443 args.elf_file)) | 496 elf_path)) |
444 timestamp = calendar.timegm(timestamp_obj.timetuple()) | 497 timestamp = calendar.timegm(timestamp_obj.timetuple()) |
445 gn_args = _ParseGnArgs(os.path.join(lazy_paths.output_directory, 'args.gn')) | 498 gn_args = _ParseGnArgs(os.path.join(lazy_paths.output_directory, 'args.gn')) |
446 | 499 |
447 def relative_to_out(path): | 500 def relative_to_out(path): |
448 return os.path.relpath(path, lazy_paths.VerifyOutputDirectory()) | 501 return os.path.relpath(path, lazy_paths.VerifyOutputDirectory()) |
449 | 502 |
450 metadata = { | 503 metadata = { |
451 models.METADATA_GIT_REVISION: git_rev, | 504 models.METADATA_GIT_REVISION: git_rev, |
452 models.METADATA_MAP_FILENAME: relative_to_out(map_file_path), | 505 models.METADATA_MAP_FILENAME: relative_to_out(map_path), |
453 models.METADATA_ELF_FILENAME: relative_to_out(args.elf_file), | 506 models.METADATA_ELF_ARCHITECTURE: architecture, |
| 507 models.METADATA_ELF_FILENAME: relative_to_out(elf_path), |
454 models.METADATA_ELF_MTIME: timestamp, | 508 models.METADATA_ELF_MTIME: timestamp, |
455 models.METADATA_ELF_BUILD_ID: build_id, | 509 models.METADATA_ELF_BUILD_ID: build_id, |
456 models.METADATA_GN_ARGS: gn_args, | 510 models.METADATA_GN_ARGS: gn_args, |
457 } | 511 } |
458 | 512 |
459 size_info = CreateSizeInfo(map_file_path, lazy_paths, | 513 if apk_path: |
460 no_source_paths=args.no_source_paths, | 514 metadata[models.METADATA_APK_FILENAME] = relative_to_out(apk_path) |
461 raw_only=True) | 515 # Extraction takes around 1 second, so do it in parallel. |
| 516 pool_of_one = multiprocessing.Pool(1) |
| 517 apk_elf_result = pool_of_one.apply_async( |
| 518 _ElfInfoFromApk, (apk_path, apk_so_path, lazy_paths.tool_prefix)) |
| 519 pool_of_one.close() |
| 520 |
| 521 size_info = CreateSizeInfo( |
| 522 map_path, lazy_paths, no_source_paths=args.no_source_paths, raw_only=True) |
462 | 523 |
463 if metadata: | 524 if metadata: |
464 size_info.metadata = metadata | 525 size_info.metadata = metadata |
465 logging.debug('Validating section sizes') | 526 logging.debug('Validating section sizes') |
466 elf_section_sizes = _SectionSizesFromElf(args.elf_file, | 527 elf_section_sizes = _SectionSizesFromElf(elf_path, lazy_paths.tool_prefix) |
467 lazy_paths.tool_prefix) | |
468 for k, v in elf_section_sizes.iteritems(): | 528 for k, v in elf_section_sizes.iteritems(): |
469 assert v == size_info.section_sizes.get(k), ( | 529 assert v == size_info.section_sizes.get(k), ( |
470 'ELF file and .map file do not match.') | 530 'ELF file and .map file do not match.') |
471 | 531 |
| 532 if apk_path: |
| 533 logging.debug('Extracting section sizes from .so within .apk') |
| 534 unstripped_section_sizes = size_info.section_sizes |
| 535 apk_build_id, size_info.section_sizes = apk_elf_result.get() |
| 536 assert apk_build_id == build_id, ( |
| 537 'BuildID for %s within %s did not match the one at %s' % |
| 538 (apk_so_path, apk_path, elf_path)) |
| 539 |
| 540 packed_section_name = None |
| 541 if architecture == 'ARM': |
| 542 packed_section_name = '.rel.dyn' |
| 543 elif architecture == 'AArch64': |
| 544 packed_section_name = '.rela.dyn' |
| 545 |
| 546 if packed_section_name: |
| 547 logging.debug('Recording size of unpacked relocations') |
| 548 if packed_section_name not in size_info.section_sizes: |
| 549 logging.warning('Packed section not present: %s', packed_section_name) |
| 550 else: |
| 551 size_info.section_sizes['%s (unpacked)' % packed_section_name] = ( |
| 552 unstripped_section_sizes.get(packed_section_name)) |
| 553 |
472 logging.info('Recording metadata: \n %s', | 554 logging.info('Recording metadata: \n %s', |
473 '\n '.join(describe.DescribeMetadata(size_info.metadata))) | 555 '\n '.join(describe.DescribeMetadata(size_info.metadata))) |
474 logging.info('Saving result to %s', args.size_file) | 556 logging.info('Saving result to %s', args.size_file) |
475 file_format.SaveSizeInfo(size_info, args.size_file) | 557 file_format.SaveSizeInfo(size_info, args.size_file) |
476 logging.info('Done') | 558 logging.info('Done') |
OLD | NEW |