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 --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
51COMPILER_SYMBOLS_RE = re.compile(
52  r"v8::internal::(?:\(anonymous namespace\)::)?Compile|v8::internal::Parser")
53JIT_CODE_SYMBOLS_RE = re.compile(
54  r"(LazyCompile|Compile|Eval|Script):(\*|~)")
55GC_SYMBOLS_RE = re.compile(
56  r"v8::internal::Heap::CollectGarbage")
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                                   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
150def 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
158def 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
169def 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
174def 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
185def 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
241def 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
259if __name__ == "__main__":
260  main()
261