2016-04-18 11:46:22 +00:00
|
|
|
#! /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")
|
2016-11-08 10:37:21 +00:00
|
|
|
JIT_CODE_SYMBOLS_RE = re.compile(
|
|
|
|
r"(LazyCompile|Compile|Eval|Script):(\*|~)")
|
2016-11-17 13:51:45 +00:00
|
|
|
GC_SYMBOLS_RE = re.compile(
|
|
|
|
r"v8::internal::Heap::CollectGarbage")
|
2016-04-18 11:46:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
|
|
|
|
2016-11-08 10:37:21 +00:00
|
|
|
def collapsed_callchains_generator(perf_stream, hide_other=False,
|
|
|
|
hide_compiler=False, hide_jit=False,
|
2016-11-17 13:51:45 +00:00
|
|
|
hide_gc=False, show_full_signatures=False):
|
2016-04-18 11:46:22 +00:00
|
|
|
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:
|
2016-11-08 10:37:21 +00:00
|
|
|
if (not skip_until_end_of_chain and current_chain
|
|
|
|
and not hide_other):
|
2016-04-18 11:46:22 +00:00
|
|
|
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
|
|
|
|
|
2016-07-08 16:24:04 +00:00
|
|
|
# Trim the leading address and the trailing +offset, if present.
|
|
|
|
symbol = line.split(" ", 1)[1].split("+", 1)[0]
|
2016-04-18 11:46:22 +00:00
|
|
|
if not show_full_signatures:
|
|
|
|
symbol = strip_function_parameters(symbol)
|
2016-11-08 10:37:21 +00:00
|
|
|
|
|
|
|
# Avoid chains of [unknown]
|
|
|
|
if (symbol == "[unknown]" and current_chain and
|
|
|
|
current_chain[-1] == "[unknown]"):
|
|
|
|
continue
|
|
|
|
|
2016-04-18 11:46:22 +00:00
|
|
|
current_chain.append(symbol)
|
|
|
|
|
|
|
|
if symbol.startswith("BytecodeHandler:"):
|
2016-11-08 10:37:21 +00:00
|
|
|
current_chain.append("[interpreter]")
|
2016-04-18 11:46:22 +00:00
|
|
|
yield current_chain
|
|
|
|
skip_until_end_of_chain = True
|
2016-11-08 10:37:21 +00:00
|
|
|
elif JIT_CODE_SYMBOLS_RE.match(symbol):
|
|
|
|
if not hide_jit:
|
|
|
|
current_chain.append("[jit]")
|
|
|
|
yield current_chain
|
|
|
|
skip_until_end_of_chain = True
|
2016-11-17 13:51:45 +00:00
|
|
|
elif GC_SYMBOLS_RE.match(symbol):
|
|
|
|
if not hide_gc:
|
|
|
|
current_chain.append("[gc]")
|
|
|
|
yield current_chain
|
|
|
|
skip_until_end_of_chain = True
|
2016-04-18 11:46:22 +00:00
|
|
|
elif symbol == "Stub:CEntryStub" and compiler_symbol_in_chain:
|
2016-11-08 10:37:21 +00:00
|
|
|
if not hide_compiler:
|
|
|
|
current_chain.append("[compiler]")
|
2016-04-18 11:46:22 +00:00
|
|
|
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]"]
|
2016-04-18 15:05:21 +00:00
|
|
|
else:
|
2016-04-18 11:46:22 +00:00
|
|
|
# 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(
|
2016-11-08 10:37:21 +00:00
|
|
|
"--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",
|
2016-04-18 11:46:22 +00:00
|
|
|
action="store_true"
|
|
|
|
)
|
2016-11-17 13:51:45 +00:00
|
|
|
command_line_parser.add_argument(
|
|
|
|
"--hide-gc",
|
|
|
|
help="Hide samples from garbage collection",
|
|
|
|
action="store_true"
|
|
|
|
)
|
2016-04-18 11:46:22 +00:00
|
|
|
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()
|
|
|
|
|
2016-07-08 16:24:04 +00:00
|
|
|
perf = subprocess.Popen(["perf", "script", "--fields", "ip,sym",
|
2016-04-18 11:46:22 +00:00
|
|
|
"-i", program_options.perf_filename],
|
|
|
|
stdout=subprocess.PIPE)
|
|
|
|
|
|
|
|
callchains = collapsed_callchains_generator(
|
2016-11-08 10:37:21 +00:00
|
|
|
perf.stdout, program_options.hide_other, program_options.hide_compiler,
|
2016-11-17 13:51:45 +00:00
|
|
|
program_options.hide_jit, program_options.hide_gc,
|
|
|
|
program_options.show_full_signatures)
|
2016-04-18 11:46:22 +00:00
|
|
|
|
|
|
|
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()
|