Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(269)

Side by Side Diff: tools/binary_size/libsupersize/archive.py

Issue 2807343005: //tools/binary_size: Record packed .rel.dyn section size (Closed)
Patch Set: arm64 support and multiprocessing Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | tools/binary_size/libsupersize/console.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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')
OLDNEW
« no previous file with comments | « no previous file | tools/binary_size/libsupersize/console.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698