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
8import argparse
9import collections
10import re
11import subprocess
12import sys
13
14
15__DESCRIPTION = """
16Processes a perf.data sample file and reports the hottest Ignition bytecodes,
17or write an input file for flamegraph.pl.
18"""
19
20
21__HELP_EPILOGUE = """
22examples:
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 \\
28        --ignition --noturbo --nocrankshaft run.js
29  $ tools/ignition/linux_perf_report.py --flamegraph -o out.collapsed
30  $ flamegraph.pl --colors js out.collapsed > out.svg
31
32  # Same as above, but show all samples, including time spent compiling JS code,
33  # entry trampoline samples and other samples.
34  $ # ...
35  $ tools/ignition/linux_perf_report.py \\
36      --flamegraph --show-all -o out.collapsed
37  $ # ...
38
39  # Same as above, but show full function signatures in the flamegraph.
40  $ # ...
41  $ tools/ignition/linux_perf_report.py \\
42      --flamegraph --show-full-signatures -o out.collapsed
43  $ # ...
44
45  # See the hottest bytecodes on Octane benchmark, by number of samples.
46  #
47  $ tools/run-perf.sh out/x64.release/d8 \\
48        --ignition --noturbo --nocrankshaft octane/run.js
49  $ tools/ignition/linux_perf_report.py
50"""
51
52
53COMPILER_SYMBOLS_RE = re.compile(
54  r"v8::internal::(?:\(anonymous namespace\)::)?Compile|v8::internal::Parser")
55JIT_CODE_SYMBOLS_RE = re.compile(
56  r"(LazyCompile|Compile|Eval|Script):(\*|~)")
57
58
59def 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
75def collapsed_callchains_generator(perf_stream, hide_other=False,
76                                   hide_compiler=False, hide_jit=False,
77                                   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 symbol == "Stub:CEntryStub" and compiler_symbol_in_chain:
126      if not hide_compiler:
127        current_chain.append("[compiler]")
128        yield current_chain
129      skip_until_end_of_chain = True
130    elif COMPILER_SYMBOLS_RE.match(symbol):
131      compiler_symbol_in_chain = True
132    elif symbol == "Builtin:InterpreterEntryTrampoline":
133      if len(current_chain) == 1:
134        yield ["[entry trampoline]"]
135      else:
136        # If we see an InterpreterEntryTrampoline which is not at the top of the
137        # chain and doesn't have a BytecodeHandler above it, then we have
138        # skipped the top BytecodeHandler due to the top-level stub not building
139        # a frame. File the chain in the [misattributed] bucket.
140        current_chain[-1] = "[misattributed]"
141        yield current_chain
142      skip_until_end_of_chain = True
143
144
145def calculate_samples_count_per_callchain(callchains):
146  chain_counters = collections.defaultdict(int)
147  for callchain in callchains:
148    key = ";".join(reversed(callchain))
149    chain_counters[key] += 1
150  return chain_counters.items()
151
152
153def calculate_samples_count_per_handler(callchains):
154  def strip_handler_prefix_if_any(handler):
155    return handler if handler[0] == "[" else handler.split(":", 1)[1]
156
157  handler_counters = collections.defaultdict(int)
158  for callchain in callchains:
159    handler = strip_handler_prefix_if_any(callchain[-1])
160    handler_counters[handler] += 1
161  return handler_counters.items()
162
163
164def write_flamegraph_input_file(output_stream, callchains):
165  for callchain, count in calculate_samples_count_per_callchain(callchains):
166    output_stream.write("{}; {}\n".format(callchain, count))
167
168
169def write_handlers_report(output_stream, callchains):
170  handler_counters = calculate_samples_count_per_handler(callchains)
171  samples_num = sum(counter for _, counter in handler_counters)
172  # Sort by decreasing number of samples
173  handler_counters.sort(key=lambda entry: entry[1], reverse=True)
174  for bytecode_name, count in handler_counters:
175    output_stream.write(
176      "{}\t{}\t{:.3f}%\n".format(bytecode_name, count,
177                                 100. * count / samples_num))
178
179
180def parse_command_line():
181  command_line_parser = argparse.ArgumentParser(
182    formatter_class=argparse.RawDescriptionHelpFormatter,
183    description=__DESCRIPTION,
184    epilog=__HELP_EPILOGUE)
185
186  command_line_parser.add_argument(
187    "perf_filename",
188    help="perf sample file to process (default: perf.data)",
189    nargs="?",
190    default="perf.data",
191    metavar="<perf filename>"
192  )
193  command_line_parser.add_argument(
194    "--flamegraph", "-f",
195    help="output an input file for flamegraph.pl, not a report",
196    action="store_true",
197    dest="output_flamegraph"
198  )
199  command_line_parser.add_argument(
200    "--hide-other",
201    help="Hide other samples",
202    action="store_true"
203  )
204  command_line_parser.add_argument(
205    "--hide-compiler",
206    help="Hide samples during compilation",
207    action="store_true"
208  )
209  command_line_parser.add_argument(
210    "--hide-jit",
211    help="Hide samples from JIT code execution",
212    action="store_true"
213  )
214  command_line_parser.add_argument(
215    "--show-full-signatures", "-s",
216    help="show full signatures instead of function names",
217    action="store_true"
218  )
219  command_line_parser.add_argument(
220    "--output", "-o",
221    help="output file name (stdout if omitted)",
222    type=argparse.FileType('wt'),
223    default=sys.stdout,
224    metavar="<output filename>",
225    dest="output_stream"
226  )
227
228  return command_line_parser.parse_args()
229
230
231def main():
232  program_options = parse_command_line()
233
234  perf = subprocess.Popen(["perf", "script", "--fields", "ip,sym",
235                           "-i", program_options.perf_filename],
236                          stdout=subprocess.PIPE)
237
238  callchains = collapsed_callchains_generator(
239    perf.stdout, program_options.hide_other, program_options.hide_compiler,
240    program_options.hide_jit, program_options.show_full_signatures)
241
242  if program_options.output_flamegraph:
243    write_flamegraph_input_file(program_options.output_stream, callchains)
244  else:
245    write_handlers_report(program_options.output_stream, callchains)
246
247
248if __name__ == "__main__":
249  main()
250