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