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