Home | History | Annotate | Download | only in ignition
      1 #! /usr/bin/python2
      2 #
      3 # Copyright 2016 the V8 project authors. All rights reserved.
      4 # Use of this source code is governed by a BSD-style license that can be
      5 # found in the LICENSE file.
      6 #
      7 
      8 import argparse
      9 import collections
     10 import re
     11 import subprocess
     12 import sys
     13 
     14 
     15 __DESCRIPTION = """
     16 Processes a perf.data sample file and reports the hottest Ignition bytecodes,
     17 or write an input file for flamegraph.pl.
     18 """
     19 
     20 
     21 __HELP_EPILOGUE = """
     22 examples:
     23   # Get a flamegraph for Ignition bytecode handlers on Octane benchmark,
     24   # without considering the time spent compiling JS code, entry trampoline
     25   # samples and other non-Ignition samples.
     26   #
     27   $ tools/run-perf.sh out/x64.release/d8 --noopt run.js
     28   $ tools/ignition/linux_perf_report.py --flamegraph -o out.collapsed
     29   $ flamegraph.pl --colors js out.collapsed > out.svg
     30 
     31   # Same as above, but show all samples, including time spent compiling JS code,
     32   # entry trampoline samples and other samples.
     33   $ # ...
     34   $ tools/ignition/linux_perf_report.py \\
     35       --flamegraph --show-all -o out.collapsed
     36   $ # ...
     37 
     38   # Same as above, but show full function signatures in the flamegraph.
     39   $ # ...
     40   $ tools/ignition/linux_perf_report.py \\
     41       --flamegraph --show-full-signatures -o out.collapsed
     42   $ # ...
     43 
     44   # See the hottest bytecodes on Octane benchmark, by number of samples.
     45   #
     46   $ tools/run-perf.sh out/x64.release/d8 --noopt octane/run.js
     47   $ tools/ignition/linux_perf_report.py
     48 """
     49 
     50 
     51 COMPILER_SYMBOLS_RE = re.compile(
     52   r"v8::internal::(?:\(anonymous namespace\)::)?Compile|v8::internal::Parser")
     53 JIT_CODE_SYMBOLS_RE = re.compile(
     54   r"(LazyCompile|Compile|Eval|Script):(\*|~)")
     55 GC_SYMBOLS_RE = re.compile(
     56   r"v8::internal::Heap::CollectGarbage")
     57 
     58 
     59 def strip_function_parameters(symbol):
     60   if symbol[-1] != ')': return symbol
     61   pos = 1
     62   parenthesis_count = 0
     63   for c in reversed(symbol):
     64     if c == ')':
     65       parenthesis_count += 1
     66     elif c == '(':
     67       parenthesis_count -= 1
     68     if parenthesis_count == 0:
     69       break
     70     else:
     71       pos += 1
     72   return symbol[:-pos]
     73 
     74 
     75 def collapsed_callchains_generator(perf_stream, hide_other=False,
     76                                    hide_compiler=False, hide_jit=False,
     77                                    hide_gc=False, show_full_signatures=False):
     78   current_chain = []
     79   skip_until_end_of_chain = False
     80   compiler_symbol_in_chain = False
     81 
     82   for line in perf_stream:
     83     # Lines starting with a "#" are comments, skip them.
     84     if line[0] == "#":
     85       continue
     86 
     87     line = line.strip()
     88 
     89     # Empty line signals the end of the callchain.
     90     if not line:
     91       if (not skip_until_end_of_chain and current_chain
     92           and not hide_other):
     93         current_chain.append("[other]")
     94         yield current_chain
     95       # Reset parser status.
     96       current_chain = []
     97       skip_until_end_of_chain = False
     98       compiler_symbol_in_chain = False
     99       continue
    100 
    101     if skip_until_end_of_chain:
    102       continue
    103 
    104     # Trim the leading address and the trailing +offset, if present.
    105     symbol = line.split(" ", 1)[1].split("+", 1)[0]
    106     if not show_full_signatures:
    107       symbol = strip_function_parameters(symbol)
    108 
    109     # Avoid chains of [unknown]
    110     if (symbol == "[unknown]" and current_chain and
    111         current_chain[-1] == "[unknown]"):
    112       continue
    113 
    114     current_chain.append(symbol)
    115 
    116     if symbol.startswith("BytecodeHandler:"):
    117       current_chain.append("[interpreter]")
    118       yield current_chain
    119       skip_until_end_of_chain = True
    120     elif JIT_CODE_SYMBOLS_RE.match(symbol):
    121       if not hide_jit:
    122         current_chain.append("[jit]")
    123         yield current_chain
    124         skip_until_end_of_chain = True
    125     elif GC_SYMBOLS_RE.match(symbol):
    126       if not hide_gc:
    127         current_chain.append("[gc]")
    128         yield current_chain
    129         skip_until_end_of_chain = True
    130     elif symbol == "Stub:CEntryStub" and compiler_symbol_in_chain:
    131       if not hide_compiler:
    132         current_chain.append("[compiler]")
    133         yield current_chain
    134       skip_until_end_of_chain = True
    135     elif COMPILER_SYMBOLS_RE.match(symbol):
    136       compiler_symbol_in_chain = True
    137     elif symbol == "Builtin:InterpreterEntryTrampoline":
    138       if len(current_chain) == 1:
    139         yield ["[entry trampoline]"]
    140       else:
    141         # If we see an InterpreterEntryTrampoline which is not at the top of the
    142         # chain and doesn't have a BytecodeHandler above it, then we have
    143         # skipped the top BytecodeHandler due to the top-level stub not building
    144         # a frame. File the chain in the [misattributed] bucket.
    145         current_chain[-1] = "[misattributed]"
    146         yield current_chain
    147       skip_until_end_of_chain = True
    148 
    149 
    150 def calculate_samples_count_per_callchain(callchains):
    151   chain_counters = collections.defaultdict(int)
    152   for callchain in callchains:
    153     key = ";".join(reversed(callchain))
    154     chain_counters[key] += 1
    155   return chain_counters.items()
    156 
    157 
    158 def calculate_samples_count_per_handler(callchains):
    159   def strip_handler_prefix_if_any(handler):
    160     return handler if handler[0] == "[" else handler.split(":", 1)[1]
    161 
    162   handler_counters = collections.defaultdict(int)
    163   for callchain in callchains:
    164     handler = strip_handler_prefix_if_any(callchain[-1])
    165     handler_counters[handler] += 1
    166   return handler_counters.items()
    167 
    168 
    169 def write_flamegraph_input_file(output_stream, callchains):
    170   for callchain, count in calculate_samples_count_per_callchain(callchains):
    171     output_stream.write("{}; {}\n".format(callchain, count))
    172 
    173 
    174 def write_handlers_report(output_stream, callchains):
    175   handler_counters = calculate_samples_count_per_handler(callchains)
    176   samples_num = sum(counter for _, counter in handler_counters)
    177   # Sort by decreasing number of samples
    178   handler_counters.sort(key=lambda entry: entry[1], reverse=True)
    179   for bytecode_name, count in handler_counters:
    180     output_stream.write(
    181       "{}\t{}\t{:.3f}%\n".format(bytecode_name, count,
    182                                  100. * count / samples_num))
    183 
    184 
    185 def parse_command_line():
    186   command_line_parser = argparse.ArgumentParser(
    187     formatter_class=argparse.RawDescriptionHelpFormatter,
    188     description=__DESCRIPTION,
    189     epilog=__HELP_EPILOGUE)
    190 
    191   command_line_parser.add_argument(
    192     "perf_filename",
    193     help="perf sample file to process (default: perf.data)",
    194     nargs="?",
    195     default="perf.data",
    196     metavar="<perf filename>"
    197   )
    198   command_line_parser.add_argument(
    199     "--flamegraph", "-f",
    200     help="output an input file for flamegraph.pl, not a report",
    201     action="store_true",
    202     dest="output_flamegraph"
    203   )
    204   command_line_parser.add_argument(
    205     "--hide-other",
    206     help="Hide other samples",
    207     action="store_true"
    208   )
    209   command_line_parser.add_argument(
    210     "--hide-compiler",
    211     help="Hide samples during compilation",
    212     action="store_true"
    213   )
    214   command_line_parser.add_argument(
    215     "--hide-jit",
    216     help="Hide samples from JIT code execution",
    217     action="store_true"
    218   )
    219   command_line_parser.add_argument(
    220     "--hide-gc",
    221     help="Hide samples from garbage collection",
    222     action="store_true"
    223   )
    224   command_line_parser.add_argument(
    225     "--show-full-signatures", "-s",
    226     help="show full signatures instead of function names",
    227     action="store_true"
    228   )
    229   command_line_parser.add_argument(
    230     "--output", "-o",
    231     help="output file name (stdout if omitted)",
    232     type=argparse.FileType('wt'),
    233     default=sys.stdout,
    234     metavar="<output filename>",
    235     dest="output_stream"
    236   )
    237 
    238   return command_line_parser.parse_args()
    239 
    240 
    241 def main():
    242   program_options = parse_command_line()
    243 
    244   perf = subprocess.Popen(["perf", "script", "--fields", "ip,sym",
    245                            "-i", program_options.perf_filename],
    246                           stdout=subprocess.PIPE)
    247 
    248   callchains = collapsed_callchains_generator(
    249     perf.stdout, program_options.hide_other, program_options.hide_compiler,
    250     program_options.hide_jit, program_options.hide_gc,
    251     program_options.show_full_signatures)
    252 
    253   if program_options.output_flamegraph:
    254     write_flamegraph_input_file(program_options.output_stream, callchains)
    255   else:
    256     write_handlers_report(program_options.output_stream, callchains)
    257 
    258 
    259 if __name__ == "__main__":
    260   main()
    261