| #! /usr/bin/python2 | |
| # | |
| # Copyright 2016 the V8 project authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| # | |
| import argparse | |
| import collections | |
| import re | |
| import subprocess | |
| import sys | |
| __DESCRIPTION = """ | |
| Processes a perf.data sample file and reports the hottest Ignition bytecodes, | |
| or write an input file for flamegraph.pl. | |
| """ | |
| __HELP_EPILOGUE = """ | |
| examples: | |
| # Get a flamegraph for Ignition bytecode handlers on Octane benchmark, | |
| # without considering the time spent compiling JS code, entry trampoline | |
| # samples and other non-Ignition samples. | |
| # | |
| $ tools/run-perf.sh out/x64.release/d8 --noopt run.js | |
| $ tools/ignition/linux_perf_report.py --flamegraph -o out.collapsed | |
| $ flamegraph.pl --colors js out.collapsed > out.svg | |
| # Same as above, but show all samples, including time spent compiling JS code, | |
| # entry trampoline samples and other samples. | |
| $ # ... | |
| $ tools/ignition/linux_perf_report.py \\ | |
| --flamegraph --show-all -o out.collapsed | |
| $ # ... | |
| # Same as above, but show full function signatures in the flamegraph. | |
| $ # ... | |
| $ tools/ignition/linux_perf_report.py \\ | |
| --flamegraph --show-full-signatures -o out.collapsed | |
| $ # ... | |
| # See the hottest bytecodes on Octane benchmark, by number of samples. | |
| # | |
| $ tools/run-perf.sh out/x64.release/d8 --noopt octane/run.js | |
| $ tools/ignition/linux_perf_report.py | |
| """ | |
| COMPILER_SYMBOLS_RE = re.compile( | |
| r"v8::internal::(?:\(anonymous namespace\)::)?Compile|v8::internal::Parser") | |
| JIT_CODE_SYMBOLS_RE = re.compile( | |
| r"(LazyCompile|Compile|Eval|Script):(\*|~)") | |
| GC_SYMBOLS_RE = re.compile( | |
| r"v8::internal::Heap::CollectGarbage") | |
| def strip_function_parameters(symbol): | |
| if symbol[-1] != ')': return symbol | |
| pos = 1 | |
| parenthesis_count = 0 | |
| for c in reversed(symbol): | |
| if c == ')': | |
| parenthesis_count += 1 | |
| elif c == '(': | |
| parenthesis_count -= 1 | |
| if parenthesis_count == 0: | |
| break | |
| else: | |
| pos += 1 | |
| return symbol[:-pos] | |
| def collapsed_callchains_generator(perf_stream, hide_other=False, | |
| hide_compiler=False, hide_jit=False, | |
| hide_gc=False, show_full_signatures=False): | |
| current_chain = [] | |
| skip_until_end_of_chain = False | |
| compiler_symbol_in_chain = False | |
| for line in perf_stream: | |
| # Lines starting with a "#" are comments, skip them. | |
| if line[0] == "#": | |
| continue | |
| line = line.strip() | |
| # Empty line signals the end of the callchain. | |
| if not line: | |
| if (not skip_until_end_of_chain and current_chain | |
| and not hide_other): | |
| current_chain.append("[other]") | |
| yield current_chain | |
| # Reset parser status. | |
| current_chain = [] | |
| skip_until_end_of_chain = False | |
| compiler_symbol_in_chain = False | |
| continue | |
| if skip_until_end_of_chain: | |
| continue | |
| # Trim the leading address and the trailing +offset, if present. | |
| symbol = line.split(" ", 1)[1].split("+", 1)[0] | |
| if not show_full_signatures: | |
| symbol = strip_function_parameters(symbol) | |
| # Avoid chains of [unknown] | |
| if (symbol == "[unknown]" and current_chain and | |
| current_chain[-1] == "[unknown]"): | |
| continue | |
| current_chain.append(symbol) | |
| if symbol.startswith("BytecodeHandler:"): | |
| current_chain.append("[interpreter]") | |
| yield current_chain | |
| skip_until_end_of_chain = True | |
| elif JIT_CODE_SYMBOLS_RE.match(symbol): | |
| if not hide_jit: | |
| current_chain.append("[jit]") | |
| yield current_chain | |
| skip_until_end_of_chain = True | |
| elif GC_SYMBOLS_RE.match(symbol): | |
| if not hide_gc: | |
| current_chain.append("[gc]") | |
| yield current_chain | |
| skip_until_end_of_chain = True | |
| elif symbol == "Stub:CEntryStub" and compiler_symbol_in_chain: | |
| if not hide_compiler: | |
| current_chain.append("[compiler]") | |
| yield current_chain | |
| skip_until_end_of_chain = True | |
| elif COMPILER_SYMBOLS_RE.match(symbol): | |
| compiler_symbol_in_chain = True | |
| elif symbol == "Builtin:InterpreterEntryTrampoline": | |
| if len(current_chain) == 1: | |
| yield ["[entry trampoline]"] | |
| else: | |
| # If we see an InterpreterEntryTrampoline which is not at the top of the | |
| # chain and doesn't have a BytecodeHandler above it, then we have | |
| # skipped the top BytecodeHandler due to the top-level stub not building | |
| # a frame. File the chain in the [misattributed] bucket. | |
| current_chain[-1] = "[misattributed]" | |
| yield current_chain | |
| skip_until_end_of_chain = True | |
| def calculate_samples_count_per_callchain(callchains): | |
| chain_counters = collections.defaultdict(int) | |
| for callchain in callchains: | |
| key = ";".join(reversed(callchain)) | |
| chain_counters[key] += 1 | |
| return chain_counters.items() | |
| def calculate_samples_count_per_handler(callchains): | |
| def strip_handler_prefix_if_any(handler): | |
| return handler if handler[0] == "[" else handler.split(":", 1)[1] | |
| handler_counters = collections.defaultdict(int) | |
| for callchain in callchains: | |
| handler = strip_handler_prefix_if_any(callchain[-1]) | |
| handler_counters[handler] += 1 | |
| return handler_counters.items() | |
| def write_flamegraph_input_file(output_stream, callchains): | |
| for callchain, count in calculate_samples_count_per_callchain(callchains): | |
| output_stream.write("{}; {}\n".format(callchain, count)) | |
| def write_handlers_report(output_stream, callchains): | |
| handler_counters = calculate_samples_count_per_handler(callchains) | |
| samples_num = sum(counter for _, counter in handler_counters) | |
| # Sort by decreasing number of samples | |
| handler_counters.sort(key=lambda entry: entry[1], reverse=True) | |
| for bytecode_name, count in handler_counters: | |
| output_stream.write( | |
| "{}\t{}\t{:.3f}%\n".format(bytecode_name, count, | |
| 100. * count / samples_num)) | |
| def parse_command_line(): | |
| command_line_parser = argparse.ArgumentParser( | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| description=__DESCRIPTION, | |
| epilog=__HELP_EPILOGUE) | |
| command_line_parser.add_argument( | |
| "perf_filename", | |
| help="perf sample file to process (default: perf.data)", | |
| nargs="?", | |
| default="perf.data", | |
| metavar="<perf filename>" | |
| ) | |
| command_line_parser.add_argument( | |
| "--flamegraph", "-f", | |
| help="output an input file for flamegraph.pl, not a report", | |
| action="store_true", | |
| dest="output_flamegraph" | |
| ) | |
| command_line_parser.add_argument( | |
| "--hide-other", | |
| help="Hide other samples", | |
| action="store_true" | |
| ) | |
| command_line_parser.add_argument( | |
| "--hide-compiler", | |
| help="Hide samples during compilation", | |
| action="store_true" | |
| ) | |
| command_line_parser.add_argument( | |
| "--hide-jit", | |
| help="Hide samples from JIT code execution", | |
| action="store_true" | |
| ) | |
| command_line_parser.add_argument( | |
| "--hide-gc", | |
| help="Hide samples from garbage collection", | |
| action="store_true" | |
| ) | |
| command_line_parser.add_argument( | |
| "--show-full-signatures", "-s", | |
| help="show full signatures instead of function names", | |
| action="store_true" | |
| ) | |
| command_line_parser.add_argument( | |
| "--output", "-o", | |
| help="output file name (stdout if omitted)", | |
| type=argparse.FileType('wt'), | |
| default=sys.stdout, | |
| metavar="<output filename>", | |
| dest="output_stream" | |
| ) | |
| return command_line_parser.parse_args() | |
| def main(): | |
| program_options = parse_command_line() | |
| perf = subprocess.Popen(["perf", "script", "--fields", "ip,sym", | |
| "-i", program_options.perf_filename], | |
| stdout=subprocess.PIPE) | |
| callchains = collapsed_callchains_generator( | |
| perf.stdout, program_options.hide_other, program_options.hide_compiler, | |
| program_options.hide_jit, program_options.hide_gc, | |
| program_options.show_full_signatures) | |
| if program_options.output_flamegraph: | |
| write_flamegraph_input_file(program_options.output_stream, callchains) | |
| else: | |
| write_handlers_report(program_options.output_stream, callchains) | |
| if __name__ == "__main__": | |
| main() |