Index: tools/ignition/linux_perf_report.py |
diff --git a/tools/ignition/linux_perf_report.py b/tools/ignition/linux_perf_report.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..d6fd55e38a0719a6019c06b9341fb98ec1bbf17f |
--- /dev/null |
+++ b/tools/ignition/linux_perf_report.py |
@@ -0,0 +1,222 @@ |
+#! /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 \\ |
+ --ignition --noturbo --nocrankshaft 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 \\ |
+ --ignition --noturbo --nocrankshaft octane/run.js |
+ $ tools/ignition/linux_perf_report.py |
+""" |
+ |
+ |
+COMPILER_SYMBOLS_RE = re.compile( |
+ r"v8::internal::(?:\(anonymous namespace\)::)?Compile|v8::internal::Parser") |
+ |
+ |
+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, show_all=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 show_all: |
+ 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 |
+ |
+ symbol = line.split(" ", 1)[1] |
+ if not show_full_signatures: |
+ symbol = strip_function_parameters(symbol) |
+ current_chain.append(symbol) |
+ |
+ if symbol.startswith("BytecodeHandler:"): |
+ yield current_chain |
+ skip_until_end_of_chain = True |
+ elif symbol == "Stub:CEntryStub" and compiler_symbol_in_chain: |
+ if show_all: |
+ current_chain[-1] = "[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]"] |
+ elif show_all: |
+ # 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( |
+ "--show-all", "-a", |
+ help="show samples outside Ignition bytecode handlers", |
+ 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", "-f", "ip,sym", |
+ "-i", program_options.perf_filename], |
+ stdout=subprocess.PIPE) |
+ |
+ callchains = collapsed_callchains_generator( |
+ perf.stdout, program_options.show_all, |
+ 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() |