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