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