| 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 """An interactive console for looking analyzing .size files.""" | 5 """An interactive console for looking analyzing .size files.""" |
| 6 | 6 |
| 7 import argparse | 7 import argparse |
| 8 import atexit | 8 import atexit |
| 9 import code | 9 import code |
| 10 import contextlib | 10 import contextlib |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 59 elif to_file: | 59 elif to_file: |
| 60 with open(to_file, 'w') as file_obj: | 60 with open(to_file, 'w') as file_obj: |
| 61 describe.WriteLines(lines, file_obj.write) | 61 describe.WriteLines(lines, file_obj.write) |
| 62 else: | 62 else: |
| 63 describe.WriteLines(lines, sys.stdout.write) | 63 describe.WriteLines(lines, sys.stdout.write) |
| 64 | 64 |
| 65 | 65 |
| 66 class _Session(object): | 66 class _Session(object): |
| 67 _readline_initialized = False | 67 _readline_initialized = False |
| 68 | 68 |
| 69 def __init__(self, size_infos, size_paths, lazy_paths): | 69 def __init__(self, size_infos, lazy_paths): |
| 70 self._printed_variables = [] | 70 self._printed_variables = [] |
| 71 self._variables = { | 71 self._variables = { |
| 72 'Print': self._PrintFunc, | 72 'Print': self._PrintFunc, |
| 73 'Diff': self._DiffFunc, | 73 'Diff': self._DiffFunc, |
| 74 'Disassemble': self._DisassembleFunc, | 74 'Disassemble': self._DisassembleFunc, |
| 75 'ExpandRegex': match_util.ExpandRegexIdentifierPlaceholder, | 75 'ExpandRegex': match_util.ExpandRegexIdentifierPlaceholder, |
| 76 'ShowExamples': self._ShowExamplesFunc, | 76 'ShowExamples': self._ShowExamplesFunc, |
| 77 'canned_queries': canned_queries.CannedQueries(size_infos), | 77 'canned_queries': canned_queries.CannedQueries(size_infos), |
| 78 'printed': self._printed_variables, | 78 'printed': self._printed_variables, |
| 79 } | 79 } |
| 80 self._lazy_paths = lazy_paths | 80 self._lazy_paths = lazy_paths |
| 81 self._size_infos = size_infos | 81 self._size_infos = size_infos |
| 82 self._size_paths = size_paths | |
| 83 self._disassemble_prefix_len = None | 82 self._disassemble_prefix_len = None |
| 84 | 83 |
| 85 if len(size_infos) == 1: | 84 if len(size_infos) == 1: |
| 86 self._variables['size_info'] = size_infos[0] | 85 self._variables['size_info'] = size_infos[0] |
| 87 else: | 86 else: |
| 88 for i, size_info in enumerate(size_infos): | 87 for i, size_info in enumerate(size_infos): |
| 89 self._variables['size_info%d' % (i + 1)] = size_info | 88 self._variables['size_info%d' % (i + 1)] = size_info |
| 90 | 89 |
| 91 def _DiffFunc(self, before=None, after=None, sort=True): | 90 def _DiffFunc(self, before=None, after=None, sort=True): |
| 92 """Diffs two SizeInfo objects. Returns a SizeInfoDiff. | 91 """Diffs two SizeInfo objects. Returns a DeltaSizeInfo. |
| 93 | 92 |
| 94 Args: | 93 Args: |
| 95 before: Defaults to first size_infos[0]. | 94 before: Defaults to first size_infos[0]. |
| 96 after: Defaults to second size_infos[1]. | 95 after: Defaults to second size_infos[1]. |
| 97 sort: When True (default), calls SymbolGroup.Sorted() after diffing. | 96 sort: When True (default), calls SymbolGroup.Sorted() after diffing. |
| 98 """ | 97 """ |
| 99 before = before if before is not None else self._size_infos[0] | 98 before = before if before is not None else self._size_infos[0] |
| 100 after = after if after is not None else self._size_infos[1] | 99 after = after if after is not None else self._size_infos[1] |
| 101 ret = diff.Diff(before, after) | 100 ret = diff.Diff(before, after) |
| 102 if sort: | 101 if sort: |
| 103 ret.symbols = ret.symbols.Sorted() | 102 ret.symbols = ret.symbols.Sorted() |
| 104 return ret | 103 return ret |
| 105 | 104 |
| 106 def _PrintFunc(self, obj=None, verbose=False, recursive=False, use_pager=None, | 105 def _PrintFunc(self, obj=None, verbose=False, recursive=False, use_pager=None, |
| 107 to_file=None): | 106 to_file=None): |
| 108 """Prints out the given Symbol / SymbolGroup / SymbolDiff / SizeInfo. | 107 """Prints out the given Symbol / SymbolGroup / SizeInfo. |
| 109 | 108 |
| 110 For convenience, |obj| will be appended to the global "printed" list. | 109 For convenience, |obj| will be appended to the global "printed" list. |
| 111 | 110 |
| 112 Args: | 111 Args: |
| 113 obj: The object to be printed. Defaults to size_infos[-1]. Also accepts an | 112 obj: The object to be printed. Defaults to size_infos[-1]. Also accepts an |
| 114 index into the |printed| array for showing previous results. | 113 index into the |printed| array for showing previous results. |
| 115 verbose: Show more detailed output. | 114 verbose: Show more detailed output. |
| 116 recursive: Print children of nested SymbolGroups. | 115 recursive: Print children of nested SymbolGroups. |
| 117 use_pager: Pipe output through `less`. Ignored when |obj| is a Symbol. | 116 use_pager: Pipe output through `less`. Ignored when |obj| is a Symbol. |
| 118 default is to automatically pipe when output is long. | 117 default is to automatically pipe when output is long. |
| 119 to_file: Rather than print to stdio, write to the given file. | 118 to_file: Rather than print to stdio, write to the given file. |
| 120 """ | 119 """ |
| 121 if isinstance(obj, int): | 120 if isinstance(obj, int): |
| 122 obj = self._printed_variables[obj] | 121 obj = self._printed_variables[obj] |
| 123 elif not self._printed_variables or self._printed_variables[-1] != obj: | 122 elif not self._printed_variables or self._printed_variables[-1] != obj: |
| 124 if not isinstance(obj, models.SymbolGroup) or len(obj) > 0: | 123 if not isinstance(obj, models.SymbolGroup) or len(obj) > 0: |
| 125 self._printed_variables.append(obj) | 124 self._printed_variables.append(obj) |
| 126 obj = obj if obj is not None else self._size_infos[-1] | 125 obj = obj if obj is not None else self._size_infos[-1] |
| 127 lines = describe.GenerateLines(obj, verbose=verbose, recursive=recursive) | 126 lines = describe.GenerateLines(obj, verbose=verbose, recursive=recursive) |
| 128 _WriteToStream(lines, use_pager=use_pager, to_file=to_file) | 127 _WriteToStream(lines, use_pager=use_pager, to_file=to_file) |
| 129 | 128 |
| 130 def _ElfPathAndToolPrefixForSymbol(self, symbol, elf_path, tool_prefix): | 129 def _ElfPathAndToolPrefixForSymbol(self, size_info, elf_path): |
| 131 size_info = None | 130 tool_prefix = self._lazy_paths.tool_prefix |
| 132 size_path = None | |
| 133 for size_info, size_path in zip(self._size_infos, self._size_paths): | |
| 134 if symbol in size_info.raw_symbols: | |
| 135 break | |
| 136 else: | |
| 137 # If symbols is from a diff(), use its address+name to find it. | |
| 138 for size_info, size_path in zip(self._size_infos, self._size_paths): | |
| 139 matched = size_info.raw_symbols.WhereAddressInRange(symbol.address) | |
| 140 # Use last matched symbol to skip over padding-only symbols. | |
| 141 if len(matched) > 0 and matched[-1].full_name == symbol.full_name: | |
| 142 symbol = matched[-1] | |
| 143 break | |
| 144 else: | |
| 145 assert False, 'Symbol does not belong to a size_info.' | |
| 146 | |
| 147 orig_tool_prefix = size_info.metadata.get(models.METADATA_TOOL_PREFIX) | 131 orig_tool_prefix = size_info.metadata.get(models.METADATA_TOOL_PREFIX) |
| 148 if orig_tool_prefix: | 132 if orig_tool_prefix: |
| 149 orig_tool_prefix = paths.FromSrcRootRelative(orig_tool_prefix) | 133 orig_tool_prefix = paths.FromSrcRootRelative(orig_tool_prefix) |
| 150 if os.path.exists(orig_tool_prefix + 'objdump'): | 134 if os.path.exists(orig_tool_prefix + 'objdump'): |
| 151 tool_prefix = orig_tool_prefix | 135 tool_prefix = orig_tool_prefix |
| 152 | 136 |
| 153 # TODO(agrieve): Would be even better to use objdump --info to check that | 137 # TODO(agrieve): Would be even better to use objdump --info to check that |
| 154 # the toolchain is for the correct architecture. | 138 # the toolchain is for the correct architecture. |
| 155 assert tool_prefix is not None, ( | 139 assert tool_prefix is not None, ( |
| 156 'Could not determine --tool-prefix. Possible fixes include setting ' | 140 'Could not determine --tool-prefix. Possible fixes include setting ' |
| 157 '--tool-prefix, or setting --output-directory') | 141 '--tool-prefix, or setting --output-directory') |
| 158 | 142 |
| 159 if elf_path is None: | 143 def build_id_matches(elf_path): |
| 160 filename = size_info.metadata.get(models.METADATA_ELF_FILENAME) | 144 found_build_id = archive.BuildIdFromElf(elf_path, tool_prefix) |
| 161 output_dir = self._lazy_paths.output_directory | 145 expected_build_id = size_info.metadata.get(models.METADATA_ELF_BUILD_ID) |
| 162 size_path = self._size_paths[self._size_infos.index(size_info)] | 146 return found_build_id == expected_build_id |
| 163 if output_dir: | 147 |
| 164 # Local build: File is located in output directory. | 148 filename = size_info.metadata.get(models.METADATA_ELF_FILENAME) |
| 165 path = os.path.normpath(os.path.join(output_dir, filename)) | 149 paths_to_try = [] |
| 166 if not output_dir or not os.path.exists(path): | 150 if elf_path: |
| 151 paths_to_try.append(elf_path) |
| 152 else: |
| 153 auto_lazy_paths = [ |
| 154 paths.LazyPaths(any_path_within_output_directory=s.size_path) |
| 155 for s in self._size_infos] |
| 156 for lazy_paths in auto_lazy_paths + [self._lazy_paths]: |
| 157 output_dir = lazy_paths.output_directory |
| 158 if output_dir: |
| 159 # Local build: File is located in output directory. |
| 160 paths_to_try.append( |
| 161 os.path.normpath(os.path.join(output_dir, filename))) |
| 167 # Downloaded build: File is located beside .size file. | 162 # Downloaded build: File is located beside .size file. |
| 168 path = os.path.normpath(os.path.join( | 163 paths_to_try.append(os.path.normpath(os.path.join( |
| 169 os.path.dirname(size_path), os.path.basename(filename))) | 164 os.path.dirname(size_info.size_path), os.path.basename(filename)))) |
| 170 | 165 |
| 171 assert os.path.exists(path), ( | 166 paths_to_try = [p for p in paths_to_try if os.path.exists(p)] |
| 172 'Could locate ELF file. If binary was built locally, ensure ' | |
| 173 '--output-directory is set. If output directory is unavailable, ' | |
| 174 'ensure {} is located beside {}, or pass its path explicitly using ' | |
| 175 'elf_path=').format(os.path.basename(filename), size_path) | |
| 176 | 167 |
| 177 found_build_id = archive.BuildIdFromElf(path, tool_prefix) | 168 for i, elf_path in enumerate(paths_to_try): |
| 178 expected_build_id = size_info.metadata.get(models.METADATA_ELF_BUILD_ID) | 169 if build_id_matches(elf_path): |
| 179 assert found_build_id == expected_build_id, ( | 170 return elf_path, tool_prefix |
| 180 'Build ID does not match for %s' % path) | 171 |
| 181 return path, tool_prefix | 172 # Show an error only once all paths are tried. |
| 173 if i + 1 == len(paths_to_try): |
| 174 assert False, 'Build ID does not match for %s' % elf_path |
| 175 |
| 176 assert False, ( |
| 177 'Could not locate ELF file. If binary was built locally, ensure ' |
| 178 '--output-directory is set. If output directory is unavailable, ' |
| 179 'ensure {} is located beside {}, or pass its path explicitly using ' |
| 180 'elf_path=').format(os.path.basename(filename), size_info.size_path) |
| 182 | 181 |
| 183 def _DetectDisassemblePrefixLen(self, args): | 182 def _DetectDisassemblePrefixLen(self, args): |
| 184 # Look for a line that looks like: | 183 # Look for a line that looks like: |
| 185 # /usr/{snip}/src/out/Release/../../net/quic/core/quic_time.h:100 | 184 # /usr/{snip}/src/out/Release/../../net/quic/core/quic_time.h:100 |
| 186 output = subprocess.check_output(args) | 185 output = subprocess.check_output(args) |
| 187 for line in output.splitlines(): | 186 for line in output.splitlines(): |
| 188 if line and line[0] == os.path.sep and line[-1].isdigit(): | 187 if line and line[0] == os.path.sep and line[-1].isdigit(): |
| 189 release_idx = line.find('Release') | 188 release_idx = line.find('Release') |
| 190 if release_idx == -1: | 189 if release_idx != -1: |
| 191 break | 190 return line.count(os.path.sep, 0, release_idx) |
| 192 return line.count(os.path.sep, 0, release_idx) | 191 dot_dot_idx = line.find('..') |
| 192 if dot_dot_idx != -1: |
| 193 return line.count(os.path.sep, 0, dot_dot_idx) - 1 |
| 194 out_idx = line.find(os.path.sep + 'out') |
| 195 if out_idx != -1: |
| 196 return line.count(os.path.sep, 0, out_idx) + 2 |
| 197 logging.warning('Could not guess source path from found path.') |
| 198 return None |
| 193 logging.warning('Found no source paths in objdump output.') | 199 logging.warning('Found no source paths in objdump output.') |
| 194 return None | 200 return None |
| 195 | 201 |
| 196 def _DisassembleFunc(self, symbol, elf_path=None, use_pager=None, | 202 def _DisassembleFunc(self, symbol, elf_path=None, use_pager=None, |
| 197 to_file=None): | 203 to_file=None): |
| 198 """Shows objdump disassembly for the given symbol. | 204 """Shows objdump disassembly for the given symbol. |
| 199 | 205 |
| 200 Args: | 206 Args: |
| 201 symbol: Must be a .text symbol and not a SymbolGroup. | 207 symbol: Must be a .text symbol and not a SymbolGroup. |
| 202 elf_path: Path to the executable containing the symbol. Required only | 208 elf_path: Path to the executable containing the symbol. Required only |
| 203 when auto-detection fails. | 209 when auto-detection fails. |
| 204 """ | 210 """ |
| 205 assert not symbol.IsGroup() | 211 assert not symbol.IsGroup() |
| 206 assert symbol.address and symbol.section_name == '.text' | 212 assert symbol.address and symbol.section_name == '.text' |
| 213 assert not symbol.IsDelta(), ('Cannot disasseble a Diff\'ed symbol. Try ' |
| 214 'passing .before_symbol or .after_symbol.') |
| 215 size_info = None |
| 216 for size_info in self._size_infos: |
| 217 if symbol in size_info.raw_symbols: |
| 218 break |
| 219 else: |
| 220 assert False, 'Symbol does not belong to a size_info.' |
| 207 | 221 |
| 208 tool_prefix = self._lazy_paths.tool_prefix | 222 elf_path, tool_prefix = self._ElfPathAndToolPrefixForSymbol( |
| 209 if not elf_path: | 223 size_info, elf_path) |
| 210 elf_path, tool_prefix = self._ElfPathAndToolPrefixForSymbol( | |
| 211 symbol, elf_path, tool_prefix) | |
| 212 | 224 |
| 213 args = [tool_prefix + 'objdump', '--disassemble', '--source', | 225 args = [tool_prefix + 'objdump', '--disassemble', '--source', |
| 214 '--line-numbers', '--demangle', | 226 '--line-numbers', '--demangle', |
| 215 '--start-address=0x%x' % symbol.address, | 227 '--start-address=0x%x' % symbol.address, |
| 216 '--stop-address=0x%x' % symbol.end_address, elf_path] | 228 '--stop-address=0x%x' % symbol.end_address, elf_path] |
| 217 if self._disassemble_prefix_len is None: | 229 if self._disassemble_prefix_len is None: |
| 218 prefix_len = self._DetectDisassemblePrefixLen(args) | 230 prefix_len = self._DetectDisassemblePrefixLen(args) |
| 219 if prefix_len is not None: | 231 if prefix_len is not None: |
| 220 self._disassemble_prefix_len = prefix_len | 232 self._disassemble_prefix_len = prefix_len |
| 221 | 233 |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 271 '# View per-component breakdowns, then drill into the last entry.', | 283 '# View per-component breakdowns, then drill into the last entry.', |
| 272 'c = canned_queries.CategorizeByChromeComponent()', | 284 'c = canned_queries.CategorizeByChromeComponent()', |
| 273 'Print(c)', | 285 'Print(c)', |
| 274 'Print(c[-1].GroupedByPath(depth=2).Sorted())', | 286 'Print(c[-1].GroupedByPath(depth=2).Sorted())', |
| 275 '', | 287 '', |
| 276 '# For even more inspiration, look at canned_queries.py', | 288 '# For even more inspiration, look at canned_queries.py', |
| 277 '# (and feel free to add your own!).', | 289 '# (and feel free to add your own!).', |
| 278 ]) | 290 ]) |
| 279 | 291 |
| 280 def _CreateBanner(self): | 292 def _CreateBanner(self): |
| 281 symbol_info_keys = sorted(m for m in dir(models.SizeInfo) if m[0] != '_') | 293 def keys(cls, super_keys=None): |
| 282 symbol_keys = sorted(m for m in dir(models.Symbol) if m[0] != '_') | 294 ret = sorted(m for m in dir(cls) if m[0] != '_') |
| 283 symbol_group_keys = [m for m in dir(models.SymbolGroup) if m[0] != '_'] | 295 if super_keys: |
| 284 symbol_diff_keys = sorted(m for m in dir(models.SymbolDiff) | 296 ret = sorted(m for m in ret if m not in super_keys) |
| 285 if m[0] != '_' and m not in symbol_group_keys) | 297 return ret |
| 286 symbol_group_keys = sorted(m for m in symbol_group_keys | 298 |
| 287 if m not in symbol_keys) | 299 symbol_info_keys = keys(models.SizeInfo) |
| 288 canned_queries_keys = sorted(m for m in dir(canned_queries.CannedQueries) | 300 symbol_keys = keys(models.Symbol) |
| 289 if m[0] != '_') | 301 symbol_group_keys = keys(models.SymbolGroup, symbol_keys) |
| 302 delta_size_info_keys = keys(models.DeltaSizeInfo) |
| 303 delta_symbol_keys = keys(models.DeltaSymbol, symbol_keys) |
| 304 delta_symbol_group_keys = keys(models.DeltaSymbolGroup, |
| 305 symbol_keys + symbol_group_keys) |
| 306 canned_queries_keys = keys(canned_queries.CannedQueries) |
| 307 |
| 290 functions = sorted(k for k in self._variables if k[0].isupper()) | 308 functions = sorted(k for k in self._variables if k[0].isupper()) |
| 291 variables = sorted(k for k in self._variables if k[0].islower()) | 309 lines = [ |
| 292 return '\n'.join([ | |
| 293 '*' * 80, | 310 '*' * 80, |
| 294 'Entering interactive Python shell. Quick reference:', | 311 'Entering interactive Python shell. Quick reference:', |
| 295 '', | 312 '', |
| 296 'SizeInfo: %s' % ', '.join(symbol_info_keys), | 313 'SizeInfo: %s' % ', '.join(symbol_info_keys), |
| 297 'Symbol: %s' % ', '.join(symbol_keys), | 314 'Symbol: %s' % ', '.join(symbol_keys), |
| 298 '', | 315 '', |
| 299 'SymbolGroup (extends Symbol): %s' % ', '.join(symbol_group_keys), | 316 'SymbolGroup (extends Symbol): %s' % ', '.join(symbol_group_keys), |
| 300 '', | 317 '', |
| 301 'SymbolDiff (extends SymbolGroup): %s' % ', '.join(symbol_diff_keys), | 318 'DeltaSizeInfo: %s' % ', '.join(delta_size_info_keys), |
| 319 'DeltaSymbol (extends Symbol): %s' % ', '.join(delta_symbol_keys), |
| 320 'DeltaSymbolGroup (extends SymbolGroup): %s' % ', '.join( |
| 321 delta_symbol_group_keys), |
| 302 '', | 322 '', |
| 303 'canned_queries: %s' % ', '.join(canned_queries_keys), | 323 'canned_queries: %s' % ', '.join(canned_queries_keys), |
| 304 '', | 324 '', |
| 305 'Functions: %s' % ', '.join('%s()' % f for f in functions), | 325 'Functions: %s' % ', '.join('%s()' % f for f in functions), |
| 306 'Variables: %s' % ', '.join(variables), | 326 'Variables: ', |
| 307 '*' * 80, | 327 ' printed: List of objects passed to Print().', |
| 308 ]) | 328 ] |
| 329 for key, value in self._variables.iteritems(): |
| 330 if key.startswith('size_info'): |
| 331 lines.append(' {}: Loaded from {}'.format(key, value.size_path)) |
| 332 lines.append('*' * 80) |
| 333 return '\n'.join(lines) |
| 334 |
| 309 | 335 |
| 310 @classmethod | 336 @classmethod |
| 311 def _InitReadline(cls): | 337 def _InitReadline(cls): |
| 312 if cls._readline_initialized: | 338 if cls._readline_initialized: |
| 313 return | 339 return |
| 314 cls._readline_initialized = True | 340 cls._readline_initialized = True |
| 315 # Without initializing readline, arrow keys don't even work! | 341 # Without initializing readline, arrow keys don't even work! |
| 316 readline.parse_and_bind('tab: complete') | 342 readline.parse_and_bind('tab: complete') |
| 317 history_file = os.path.join(os.path.expanduser('~'), | 343 history_file = os.path.join(os.path.expanduser('~'), |
| 318 '.binary_size_query_history') | 344 '.binary_size_query_history') |
| (...skipping 28 matching lines...) Expand all Loading... |
| 347 | 373 |
| 348 def Run(args, parser): | 374 def Run(args, parser): |
| 349 for path in args.inputs: | 375 for path in args.inputs: |
| 350 if not path.endswith('.size'): | 376 if not path.endswith('.size'): |
| 351 parser.error('All inputs must end with ".size"') | 377 parser.error('All inputs must end with ".size"') |
| 352 | 378 |
| 353 size_infos = [archive.LoadAndPostProcessSizeInfo(p) for p in args.inputs] | 379 size_infos = [archive.LoadAndPostProcessSizeInfo(p) for p in args.inputs] |
| 354 lazy_paths = paths.LazyPaths(tool_prefix=args.tool_prefix, | 380 lazy_paths = paths.LazyPaths(tool_prefix=args.tool_prefix, |
| 355 output_directory=args.output_directory, | 381 output_directory=args.output_directory, |
| 356 any_path_within_output_directory=args.inputs[0]) | 382 any_path_within_output_directory=args.inputs[0]) |
| 357 session = _Session(size_infos, args.inputs, lazy_paths) | 383 session = _Session(size_infos, lazy_paths) |
| 358 | 384 |
| 359 if args.query: | 385 if args.query: |
| 360 logging.info('Running query from command-line.') | 386 logging.info('Running query from command-line.') |
| 361 session.Eval(args.query) | 387 session.Eval(args.query) |
| 362 else: | 388 else: |
| 363 logging.info('Entering interactive console.') | 389 logging.info('Entering interactive console.') |
| 364 session.GoInteractive() | 390 session.GoInteractive() |
| OLD | NEW |