#! /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()