• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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):(\*|~)")
57GC_SYMBOLS_RE = re.compile(
58  r"v8::internal::Heap::CollectGarbage")
59
60
61def strip_function_parameters(symbol):
62  if symbol[-1] != ')': return symbol
63  pos = 1
64  parenthesis_count = 0
65  for c in reversed(symbol):
66    if c == ')':
67      parenthesis_count += 1
68    elif c == '(':
69      parenthesis_count -= 1
70    if parenthesis_count == 0:
71      break
72    else:
73      pos += 1
74  return symbol[:-pos]
75
76
77def collapsed_callchains_generator(perf_stream, hide_other=False,
78                                   hide_compiler=False, hide_jit=False,
79                                   hide_gc=False, show_full_signatures=False):
80  current_chain = []
81  skip_until_end_of_chain = False
82  compiler_symbol_in_chain = False
83
84  for line in perf_stream:
85    # Lines starting with a "#" are comments, skip them.
86    if line[0] == "#":
87      continue
88
89    line = line.strip()
90
91    # Empty line signals the end of the callchain.
92    if not line:
93      if (not skip_until_end_of_chain and current_chain
94          and not hide_other):
95        current_chain.append("[other]")
96        yield current_chain
97      # Reset parser status.
98      current_chain = []
99      skip_until_end_of_chain = False
100      compiler_symbol_in_chain = False
101      continue
102
103    if skip_until_end_of_chain:
104      continue
105
106    # Trim the leading address and the trailing +offset, if present.
107    symbol = line.split(" ", 1)[1].split("+", 1)[0]
108    if not show_full_signatures:
109      symbol = strip_function_parameters(symbol)
110
111    # Avoid chains of [unknown]
112    if (symbol == "[unknown]" and current_chain and
113        current_chain[-1] == "[unknown]"):
114      continue
115
116    current_chain.append(symbol)
117
118    if symbol.startswith("BytecodeHandler:"):
119      current_chain.append("[interpreter]")
120      yield current_chain
121      skip_until_end_of_chain = True
122    elif JIT_CODE_SYMBOLS_RE.match(symbol):
123      if not hide_jit:
124        current_chain.append("[jit]")
125        yield current_chain
126        skip_until_end_of_chain = True
127    elif GC_SYMBOLS_RE.match(symbol):
128      if not hide_gc:
129        current_chain.append("[gc]")
130        yield current_chain
131        skip_until_end_of_chain = True
132    elif symbol == "Stub:CEntryStub" and compiler_symbol_in_chain:
133      if not hide_compiler:
134        current_chain.append("[compiler]")
135        yield current_chain
136      skip_until_end_of_chain = True
137    elif COMPILER_SYMBOLS_RE.match(symbol):
138      compiler_symbol_in_chain = True
139    elif symbol == "Builtin:InterpreterEntryTrampoline":
140      if len(current_chain) == 1:
141        yield ["[entry trampoline]"]
142      else:
143        # If we see an InterpreterEntryTrampoline which is not at the top of the
144        # chain and doesn't have a BytecodeHandler above it, then we have
145        # skipped the top BytecodeHandler due to the top-level stub not building
146        # a frame. File the chain in the [misattributed] bucket.
147        current_chain[-1] = "[misattributed]"
148        yield current_chain
149      skip_until_end_of_chain = True
150
151
152def calculate_samples_count_per_callchain(callchains):
153  chain_counters = collections.defaultdict(int)
154  for callchain in callchains:
155    key = ";".join(reversed(callchain))
156    chain_counters[key] += 1
157  return chain_counters.items()
158
159
160def calculate_samples_count_per_handler(callchains):
161  def strip_handler_prefix_if_any(handler):
162    return handler if handler[0] == "[" else handler.split(":", 1)[1]
163
164  handler_counters = collections.defaultdict(int)
165  for callchain in callchains:
166    handler = strip_handler_prefix_if_any(callchain[-1])
167    handler_counters[handler] += 1
168  return handler_counters.items()
169
170
171def write_flamegraph_input_file(output_stream, callchains):
172  for callchain, count in calculate_samples_count_per_callchain(callchains):
173    output_stream.write("{}; {}\n".format(callchain, count))
174
175
176def write_handlers_report(output_stream, callchains):
177  handler_counters = calculate_samples_count_per_handler(callchains)
178  samples_num = sum(counter for _, counter in handler_counters)
179  # Sort by decreasing number of samples
180  handler_counters.sort(key=lambda entry: entry[1], reverse=True)
181  for bytecode_name, count in handler_counters:
182    output_stream.write(
183      "{}\t{}\t{:.3f}%\n".format(bytecode_name, count,
184                                 100. * count / samples_num))
185
186
187def parse_command_line():
188  command_line_parser = argparse.ArgumentParser(
189    formatter_class=argparse.RawDescriptionHelpFormatter,
190    description=__DESCRIPTION,
191    epilog=__HELP_EPILOGUE)
192
193  command_line_parser.add_argument(
194    "perf_filename",
195    help="perf sample file to process (default: perf.data)",
196    nargs="?",
197    default="perf.data",
198    metavar="<perf filename>"
199  )
200  command_line_parser.add_argument(
201    "--flamegraph", "-f",
202    help="output an input file for flamegraph.pl, not a report",
203    action="store_true",
204    dest="output_flamegraph"
205  )
206  command_line_parser.add_argument(
207    "--hide-other",
208    help="Hide other samples",
209    action="store_true"
210  )
211  command_line_parser.add_argument(
212    "--hide-compiler",
213    help="Hide samples during compilation",
214    action="store_true"
215  )
216  command_line_parser.add_argument(
217    "--hide-jit",
218    help="Hide samples from JIT code execution",
219    action="store_true"
220  )
221  command_line_parser.add_argument(
222    "--hide-gc",
223    help="Hide samples from garbage collection",
224    action="store_true"
225  )
226  command_line_parser.add_argument(
227    "--show-full-signatures", "-s",
228    help="show full signatures instead of function names",
229    action="store_true"
230  )
231  command_line_parser.add_argument(
232    "--output", "-o",
233    help="output file name (stdout if omitted)",
234    type=argparse.FileType('wt'),
235    default=sys.stdout,
236    metavar="<output filename>",
237    dest="output_stream"
238  )
239
240  return command_line_parser.parse_args()
241
242
243def main():
244  program_options = parse_command_line()
245
246  perf = subprocess.Popen(["perf", "script", "--fields", "ip,sym",
247                           "-i", program_options.perf_filename],
248                          stdout=subprocess.PIPE)
249
250  callchains = collapsed_callchains_generator(
251    perf.stdout, program_options.hide_other, program_options.hide_compiler,
252    program_options.hide_jit, program_options.hide_gc,
253    program_options.show_full_signatures)
254
255  if program_options.output_flamegraph:
256    write_flamegraph_input_file(program_options.output_stream, callchains)
257  else:
258    write_handlers_report(program_options.output_stream, callchains)
259
260
261if __name__ == "__main__":
262  main()
263