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 posixpath | |
14 import re | 15 import re |
15 import subprocess | 16 import subprocess |
16 import sys | 17 import sys |
18 import tempfile | |
19 import zipfile | |
17 | 20 |
18 import describe | 21 import describe |
19 import file_format | 22 import file_format |
20 import function_signature | 23 import function_signature |
21 import helpers | 24 import helpers |
22 import linker_map_parser | 25 import linker_map_parser |
23 import models | 26 import models |
24 import ninja_parser | 27 import ninja_parser |
25 import paths | 28 import paths |
26 | 29 |
(...skipping 171 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
198 'Symbol has negative size (likely not sorted propertly): ' | 201 'Symbol has negative size (likely not sorted propertly): ' |
199 '%r\nprev symbol: %r' % (symbol, prev_symbol)) | 202 '%r\nprev symbol: %r' % (symbol, prev_symbol)) |
200 | 203 |
201 | 204 |
202 def _ClusterSymbols(symbols): | 205 def _ClusterSymbols(symbols): |
203 """Returns a new list of symbols with some symbols moved into groups. | 206 """Returns a new list of symbols with some symbols moved into groups. |
204 | 207 |
205 Groups include: | 208 Groups include: |
206 * Symbols that have [clone] in their name (created by compiler optimization). | 209 * Symbols that have [clone] in their name (created by compiler optimization). |
207 * Star symbols (such as "** merge strings", and "** symbol gap") | 210 * Star symbols (such as "** merge strings", and "** symbol gap") |
211 | |
212 To view created groups: | |
213 Print(size_info.symbols.Filter(lambda s: s.IsGroup()), recursive=True) | |
208 """ | 214 """ |
209 # http://unix.stackexchange.com/questions/223013/function-symbol-gets-part-suf fix-after-compilation | 215 # http://unix.stackexchange.com/questions/223013/function-symbol-gets-part-suf fix-after-compilation |
210 # Example name suffixes: | 216 # Example name suffixes: |
211 # [clone .part.322] | 217 # [clone .part.322] # GCC |
212 # [clone .isra.322] | 218 # [clone .isra.322] # GCC |
213 # [clone .constprop.1064] | 219 # [clone .constprop.1064] # GCC |
220 # [clone .11064] # clang | |
214 | 221 |
215 # Step 1: Create name map, find clones, collect star syms into replacements. | 222 # Step 1: Create name map, find clones, collect star syms into replacements. |
216 logging.debug('Creating name -> symbol map') | 223 logging.debug('Creating name -> symbol map') |
217 clone_indices = [] | 224 clone_indices = [] |
218 indices_by_full_name = {} | 225 indices_by_full_name = {} |
219 # (name, full_name) -> [(index, sym),...] | 226 # (name, full_name) -> [(index, sym),...] |
220 replacements_by_name = collections.defaultdict(list) | 227 replacements_by_name = collections.defaultdict(list) |
221 for i, symbol in enumerate(symbols): | 228 for i, symbol in enumerate(symbols): |
222 if symbol.name.startswith('**'): | 229 if symbol.name.startswith('**'): |
223 # "symbol gap 3" -> "symbol gaps" | 230 # "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) | 351 size_info = models.SizeInfo(section_sizes, raw_symbols) |
345 | 352 |
346 # Name normalization not strictly required, but makes for smaller files. | 353 # Name normalization not strictly required, but makes for smaller files. |
347 if raw_only: | 354 if raw_only: |
348 logging.info('Normalizing symbol names') | 355 logging.info('Normalizing symbol names') |
349 _NormalizeNames(size_info.raw_symbols) | 356 _NormalizeNames(size_info.raw_symbols) |
350 else: | 357 else: |
351 _PostProcessSizeInfo(size_info) | 358 _PostProcessSizeInfo(size_info) |
352 | 359 |
353 if logging.getLogger().isEnabledFor(logging.DEBUG): | 360 if logging.getLogger().isEnabledFor(logging.DEBUG): |
361 # Padding is reported in size coverage logs. | |
362 if raw_only: | |
363 _CalculatePadding(size_info.raw_symbols) | |
354 for line in describe.DescribeSizeInfoCoverage(size_info): | 364 for line in describe.DescribeSizeInfoCoverage(size_info): |
355 logging.info(line) | 365 logging.info(line) |
356 logging.info('Recorded info for %d symbols', len(size_info.raw_symbols)) | 366 logging.info('Recorded info for %d symbols', len(size_info.raw_symbols)) |
357 return size_info | 367 return size_info |
358 | 368 |
359 | 369 |
360 def _DetectGitRevision(directory): | 370 def _DetectGitRevision(directory): |
361 try: | 371 try: |
362 git_rev = subprocess.check_output( | 372 git_rev = subprocess.check_output( |
363 ['git', '-C', directory, 'rev-parse', 'HEAD']) | 373 ['git', '-C', directory, 'rev-parse', 'HEAD']) |
(...skipping 28 matching lines...) Expand all Loading... | |
392 with open(args_path) as f: | 402 with open(args_path) as f: |
393 for l in f: | 403 for l in f: |
394 # Strips #s even if within string literal. Not a problem in practice. | 404 # Strips #s even if within string literal. Not a problem in practice. |
395 parts = l.split('#')[0].split('=') | 405 parts = l.split('#')[0].split('=') |
396 if len(parts) != 2: | 406 if len(parts) != 2: |
397 continue | 407 continue |
398 args[parts[0].strip()] = parts[1].strip() | 408 args[parts[0].strip()] = parts[1].strip() |
399 return ["%s=%s" % x for x in sorted(args.iteritems())] | 409 return ["%s=%s" % x for x in sorted(args.iteritems())] |
400 | 410 |
401 | 411 |
412 def _SectionSizesFromApk(apk_file, elf_file, build_id, lazy_paths): | |
413 with zipfile.ZipFile(apk_file) as apk, \ | |
414 tempfile.NamedTemporaryFile() as f: | |
415 target = os.path.basename(elf_file) | |
416 target_info = next((f for f in apk.infolist() | |
417 if posixpath.basename(f.filename) == target), None) | |
418 assert target_info, ( | |
419 'Could not find apk entry for %s in %s' % (target, apk_file)) | |
420 f.write(apk.read(target_info)) | |
421 f.flush() | |
422 apk_build_id = BuildIdFromElf(f.name, lazy_paths.tool_prefix) | |
423 assert apk_build_id == build_id, ( | |
424 'BuildID for %s within %s did not match the one at %s' % | |
425 (target_info.filename, apk_file, elf_file)) | |
426 return _SectionSizesFromElf(f.name, lazy_paths.tool_prefix) | |
427 | |
428 | |
402 def AddArguments(parser): | 429 def AddArguments(parser): |
403 parser.add_argument('size_file', help='Path to output .size file.') | 430 parser.add_argument('size_file', help='Path to output .size file.') |
404 parser.add_argument('--elf-file', required=True, | 431 parser.add_argument('--apk-file', |
432 help='.apk file to measure. When set, --elf-file will be ' | |
433 'derived (if unset). Providing the .apk allows ' | |
434 'for the size of packed relocations to be recorded') | |
435 parser.add_argument('--elf-file', | |
405 help='Path to input ELF file. Currently used for ' | 436 help='Path to input ELF file. Currently used for ' |
406 'capturing metadata. Pass "" to skip ' | 437 'capturing metadata.') |
407 'metadata collection.') | |
408 parser.add_argument('--map-file', | 438 parser.add_argument('--map-file', |
409 help='Path to input .map(.gz) file. Defaults to ' | 439 help='Path to input .map(.gz) file. Defaults to ' |
410 '{{elf_file}}.map(.gz)?') | 440 '{{elf_file}}.map(.gz)?. If given without ' |
441 '--elf-file, no size metadata will be recorded.') | |
411 parser.add_argument('--no-source-paths', action='store_true', | 442 parser.add_argument('--no-source-paths', action='store_true', |
412 help='Do not use .ninja files to map ' | 443 help='Do not use .ninja files to map ' |
413 'object_path -> source_path') | 444 'object_path -> source_path') |
414 parser.add_argument('--tool-prefix', default='', | 445 parser.add_argument('--tool-prefix', default='', |
415 help='Path prefix for c++filt.') | 446 help='Path prefix for c++filt.') |
416 parser.add_argument('--output-directory', | 447 parser.add_argument('--output-directory', |
417 help='Path to the root build directory.') | 448 help='Path to the root build directory.') |
418 | 449 |
419 | 450 |
420 def Run(args, parser): | 451 def Run(args, parser): |
421 if not args.size_file.endswith('.size'): | 452 if not args.size_file.endswith('.size'): |
422 parser.error('size_file must end with .size') | 453 parser.error('size_file must end with .size') |
423 | 454 |
455 any_input = args.apk_file or args.elf_file or args.map_file | |
456 if not any_input: | |
457 parser.error('Most pass at least one of --apk-file, --elf-file, --map-file') | |
458 lazy_paths = paths.LazyPaths(args=args, input_file=any_input) | |
459 | |
460 elf_file = args.elf_file | |
461 if args.apk_file and not elf_file: | |
462 with zipfile.ZipFile(args.apk_file) as z: | |
463 lib_infos = [f for f in z.infolist() | |
464 if f.filename.endswith('.so') and f.file_size > 0] | |
465 assert lib_infos, 'APK has not .so files to measure.' | |
estevenson
2017/04/12 16:54:42
nit: awkward wording.
agrieve
2017/04/12 19:37:46
Done.
| |
466 # TODO(agrieve): Add support for multiple .so files, and take into account | |
467 # secondary architectures. | |
estevenson
2017/04/12 16:54:42
_SectionSizesFromApk() would fail for 64 bit monoc
agrieve
2017/04/12 19:37:45
Didn't hit any of those other errors, but good cat
| |
468 apk_so_path = max(lib_infos, key=lambda x:x.file_size).filename | |
469 elf_file = os.path.join(lazy_paths.output_directory, 'lib.unstripped', | |
470 os.path.basename(apk_so_path)) | |
471 logging.debug('Detected --elf-file=%s', elf_file) | |
472 | |
424 if args.map_file: | 473 if args.map_file: |
425 if (not args.map_file.endswith('.map') | 474 if (not args.map_file.endswith('.map') |
426 and not args.map_file.endswith('.map.gz')): | 475 and not args.map_file.endswith('.map.gz')): |
427 parser.error('Expected --map-file to end with .map or .map.gz') | 476 parser.error('Expected --map-file to end with .map or .map.gz') |
428 map_file_path = args.map_file | 477 map_file_path = args.map_file |
429 else: | 478 else: |
430 map_file_path = args.elf_file + '.map' | 479 map_file_path = elf_file + '.map' |
431 if not os.path.exists(map_file_path): | 480 if not os.path.exists(map_file_path): |
432 map_file_path += '.gz' | 481 map_file_path += '.gz' |
433 if not os.path.exists(map_file_path): | 482 if not os.path.exists(map_file_path): |
434 parser.error('Could not find .map(.gz)? file. Use --map-file.') | 483 parser.error('Could not find .map(.gz)? file. Use --map-file.') |
435 | 484 |
436 lazy_paths = paths.LazyPaths(args=args, input_file=args.elf_file) | |
437 metadata = None | 485 metadata = None |
438 if args.elf_file: | 486 if elf_file: |
439 logging.debug('Constructing metadata') | 487 logging.debug('Constructing metadata') |
440 git_rev = _DetectGitRevision(os.path.dirname(args.elf_file)) | 488 git_rev = _DetectGitRevision(os.path.dirname(elf_file)) |
441 build_id = BuildIdFromElf(args.elf_file, lazy_paths.tool_prefix) | 489 build_id = BuildIdFromElf(elf_file, lazy_paths.tool_prefix) |
442 timestamp_obj = datetime.datetime.utcfromtimestamp(os.path.getmtime( | 490 timestamp_obj = datetime.datetime.utcfromtimestamp(os.path.getmtime( |
443 args.elf_file)) | 491 elf_file)) |
444 timestamp = calendar.timegm(timestamp_obj.timetuple()) | 492 timestamp = calendar.timegm(timestamp_obj.timetuple()) |
445 gn_args = _ParseGnArgs(os.path.join(lazy_paths.output_directory, 'args.gn')) | 493 gn_args = _ParseGnArgs(os.path.join(lazy_paths.output_directory, 'args.gn')) |
446 | 494 |
447 def relative_to_out(path): | 495 def relative_to_out(path): |
448 return os.path.relpath(path, lazy_paths.VerifyOutputDirectory()) | 496 return os.path.relpath(path, lazy_paths.VerifyOutputDirectory()) |
449 | 497 |
450 metadata = { | 498 metadata = { |
451 models.METADATA_GIT_REVISION: git_rev, | 499 models.METADATA_GIT_REVISION: git_rev, |
452 models.METADATA_MAP_FILENAME: relative_to_out(map_file_path), | 500 models.METADATA_MAP_FILENAME: relative_to_out(map_file_path), |
453 models.METADATA_ELF_FILENAME: relative_to_out(args.elf_file), | 501 models.METADATA_ELF_FILENAME: relative_to_out(elf_file), |
454 models.METADATA_ELF_MTIME: timestamp, | 502 models.METADATA_ELF_MTIME: timestamp, |
455 models.METADATA_ELF_BUILD_ID: build_id, | 503 models.METADATA_ELF_BUILD_ID: build_id, |
456 models.METADATA_GN_ARGS: gn_args, | 504 models.METADATA_GN_ARGS: gn_args, |
457 } | 505 } |
458 | 506 |
459 size_info = CreateSizeInfo(map_file_path, lazy_paths, | 507 size_info = CreateSizeInfo(map_file_path, lazy_paths, |
460 no_source_paths=args.no_source_paths, | 508 no_source_paths=args.no_source_paths, |
461 raw_only=True) | 509 raw_only=True) |
462 | 510 |
463 if metadata: | 511 if metadata: |
464 size_info.metadata = metadata | 512 size_info.metadata = metadata |
465 logging.debug('Validating section sizes') | 513 logging.debug('Validating section sizes') |
466 elf_section_sizes = _SectionSizesFromElf(args.elf_file, | 514 elf_section_sizes = _SectionSizesFromElf(elf_file, lazy_paths.tool_prefix) |
467 lazy_paths.tool_prefix) | |
468 for k, v in elf_section_sizes.iteritems(): | 515 for k, v in elf_section_sizes.iteritems(): |
469 assert v == size_info.section_sizes.get(k), ( | 516 assert v == size_info.section_sizes.get(k), ( |
470 'ELF file and .map file do not match.') | 517 'ELF file and .map file do not match.') |
471 | 518 |
519 if args.apk_file: | |
520 logging.debug('Extracting section sizes from .so within .apk') | |
521 unpacked_rel_dyn = size_info.section_sizes.get('.rel.dyn', 0) | |
522 if not unpacked_rel_dyn: | |
523 logging.warning('Section .rel.dyn did not exist') | |
524 # TODO(agrieve): Extracting the .so is slow. Do it on a background thread. | |
525 size_info.section_sizes = _SectionSizesFromApk( | |
526 args.apk_file, elf_file, build_id, lazy_paths) | |
527 size_info.section_sizes['.rel.dyn (unpacked)'] = unpacked_rel_dyn | |
528 | |
472 logging.info('Recording metadata: \n %s', | 529 logging.info('Recording metadata: \n %s', |
473 '\n '.join(describe.DescribeMetadata(size_info.metadata))) | 530 '\n '.join(describe.DescribeMetadata(size_info.metadata))) |
474 logging.info('Saving result to %s', args.size_file) | 531 logging.info('Saving result to %s', args.size_file) |
475 file_format.SaveSizeInfo(size_info, args.size_file) | 532 file_format.SaveSizeInfo(size_info, args.size_file) |
476 logging.info('Done') | 533 logging.info('Done') |
OLD | NEW |