OLD | NEW |
(Empty) | |
| 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 \\ |
| 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 |
| 53 COMPILER_SYMBOLS_RE = re.compile( |
| 54 r"v8::internal::(?:\(anonymous namespace\)::)?Compile|v8::internal::Parser") |
| 55 |
| 56 |
| 57 def strip_function_parameters(symbol): |
| 58 if symbol[-1] != ')': return symbol |
| 59 pos = 1 |
| 60 parenthesis_count = 0 |
| 61 for c in reversed(symbol): |
| 62 if c == ')': |
| 63 parenthesis_count += 1 |
| 64 elif c == '(': |
| 65 parenthesis_count -= 1 |
| 66 if parenthesis_count == 0: |
| 67 break |
| 68 else: |
| 69 pos += 1 |
| 70 return symbol[:-pos] |
| 71 |
| 72 |
| 73 def collapsed_callchains_generator(perf_stream, show_all=False, |
| 74 show_full_signatures=False): |
| 75 current_chain = [] |
| 76 skip_until_end_of_chain = False |
| 77 compiler_symbol_in_chain = False |
| 78 |
| 79 for line in perf_stream: |
| 80 # Lines starting with a "#" are comments, skip them. |
| 81 if line[0] == "#": |
| 82 continue |
| 83 |
| 84 line = line.strip() |
| 85 |
| 86 # Empty line signals the end of the callchain. |
| 87 if not line: |
| 88 if not skip_until_end_of_chain and current_chain and show_all: |
| 89 current_chain.append("[other]") |
| 90 yield current_chain |
| 91 # Reset parser status. |
| 92 current_chain = [] |
| 93 skip_until_end_of_chain = False |
| 94 compiler_symbol_in_chain = False |
| 95 continue |
| 96 |
| 97 if skip_until_end_of_chain: |
| 98 continue |
| 99 |
| 100 symbol = line.split(" ", 1)[1] |
| 101 if not show_full_signatures: |
| 102 symbol = strip_function_parameters(symbol) |
| 103 current_chain.append(symbol) |
| 104 |
| 105 if symbol.startswith("BytecodeHandler:"): |
| 106 yield current_chain |
| 107 skip_until_end_of_chain = True |
| 108 elif symbol == "Stub:CEntryStub" and compiler_symbol_in_chain: |
| 109 if show_all: |
| 110 current_chain[-1] = "[compiler]" |
| 111 yield current_chain |
| 112 skip_until_end_of_chain = True |
| 113 elif COMPILER_SYMBOLS_RE.match(symbol): |
| 114 compiler_symbol_in_chain = True |
| 115 elif symbol == "Builtin:InterpreterEntryTrampoline": |
| 116 if len(current_chain) == 1: |
| 117 yield ["[entry trampoline]"] |
| 118 elif show_all: |
| 119 # If we see an InterpreterEntryTrampoline which is not at the top of the |
| 120 # chain and doesn't have a BytecodeHandler above it, then we have |
| 121 # skipped the top BytecodeHandler due to the top-level stub not building |
| 122 # a frame. File the chain in the [misattributed] bucket. |
| 123 current_chain[-1] = "[misattributed]" |
| 124 yield current_chain |
| 125 skip_until_end_of_chain = True |
| 126 |
| 127 |
| 128 def calculate_samples_count_per_callchain(callchains): |
| 129 chain_counters = collections.defaultdict(int) |
| 130 for callchain in callchains: |
| 131 key = ";".join(reversed(callchain)) |
| 132 chain_counters[key] += 1 |
| 133 return chain_counters.items() |
| 134 |
| 135 |
| 136 def calculate_samples_count_per_handler(callchains): |
| 137 def strip_handler_prefix_if_any(handler): |
| 138 return handler if handler[0] == "[" else handler.split(":", 1)[1] |
| 139 |
| 140 handler_counters = collections.defaultdict(int) |
| 141 for callchain in callchains: |
| 142 handler = strip_handler_prefix_if_any(callchain[-1]) |
| 143 handler_counters[handler] += 1 |
| 144 return handler_counters.items() |
| 145 |
| 146 |
| 147 def write_flamegraph_input_file(output_stream, callchains): |
| 148 for callchain, count in calculate_samples_count_per_callchain(callchains): |
| 149 output_stream.write("{}; {}\n".format(callchain, count)) |
| 150 |
| 151 |
| 152 def write_handlers_report(output_stream, callchains): |
| 153 handler_counters = calculate_samples_count_per_handler(callchains) |
| 154 samples_num = sum(counter for _, counter in handler_counters) |
| 155 # Sort by decreasing number of samples |
| 156 handler_counters.sort(key=lambda entry: entry[1], reverse=True) |
| 157 for bytecode_name, count in handler_counters: |
| 158 output_stream.write( |
| 159 "{}\t{}\t{:.3f}%\n".format(bytecode_name, count, |
| 160 100. * count / samples_num)) |
| 161 |
| 162 |
| 163 def parse_command_line(): |
| 164 command_line_parser = argparse.ArgumentParser( |
| 165 formatter_class=argparse.RawDescriptionHelpFormatter, |
| 166 description=__DESCRIPTION, |
| 167 epilog=__HELP_EPILOGUE) |
| 168 |
| 169 command_line_parser.add_argument( |
| 170 "perf_filename", |
| 171 help="perf sample file to process (default: perf.data)", |
| 172 nargs="?", |
| 173 default="perf.data", |
| 174 metavar="<perf filename>" |
| 175 ) |
| 176 command_line_parser.add_argument( |
| 177 "--flamegraph", "-f", |
| 178 help="output an input file for flamegraph.pl, not a report", |
| 179 action="store_true", |
| 180 dest="output_flamegraph" |
| 181 ) |
| 182 command_line_parser.add_argument( |
| 183 "--show-all", "-a", |
| 184 help="show samples outside Ignition bytecode handlers", |
| 185 action="store_true" |
| 186 ) |
| 187 command_line_parser.add_argument( |
| 188 "--show-full-signatures", "-s", |
| 189 help="show full signatures instead of function names", |
| 190 action="store_true" |
| 191 ) |
| 192 command_line_parser.add_argument( |
| 193 "--output", "-o", |
| 194 help="output file name (stdout if omitted)", |
| 195 type=argparse.FileType('wt'), |
| 196 default=sys.stdout, |
| 197 metavar="<output filename>", |
| 198 dest="output_stream" |
| 199 ) |
| 200 |
| 201 return command_line_parser.parse_args() |
| 202 |
| 203 |
| 204 def main(): |
| 205 program_options = parse_command_line() |
| 206 |
| 207 perf = subprocess.Popen(["perf", "script", "-f", "ip,sym", |
| 208 "-i", program_options.perf_filename], |
| 209 stdout=subprocess.PIPE) |
| 210 |
| 211 callchains = collapsed_callchains_generator( |
| 212 perf.stdout, program_options.show_all, |
| 213 program_options.show_full_signatures) |
| 214 |
| 215 if program_options.output_flamegraph: |
| 216 write_flamegraph_input_file(program_options.output_stream, callchains) |
| 217 else: |
| 218 write_handlers_report(program_options.output_stream, callchains) |
| 219 |
| 220 |
| 221 if __name__ == "__main__": |
| 222 main() |
OLD | NEW |