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, misattributed samples | |
rmcilroy
2016/04/15 14:58:41
nit - I wouldn't mention misattributed samples her
Stefano Sanfilippo
2016/04/15 16:13:18
Done.
| |
25 # entry trampoline 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 # misattributed samples, entry trampoline samples and other samples. | |
34 $ # ... | |
35 $ tools/ignition/linux_perf_report.py \\ | |
36 --flamegraph --show-other -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 get_function_name(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_other=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_other: | |
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 = get_function_name(symbol) | |
rmcilroy
2016/04/15 14:58:41
nit - s/get_function_name/strip_function_parameter
Stefano Sanfilippo
2016/04/15 16:13:19
Done.
| |
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_other: | |
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 # The chain cannot be empty at this point | |
rmcilroy
2016/04/15 14:58:41
Not sure what this comment means. I think what you
Stefano Sanfilippo
2016/04/15 16:13:18
The comment was because the else branch assumes th
rmcilroy
2016/04/18 09:22:24
I don't think it is necessary, you unconditinally
Stefano Sanfilippo
2016/04/18 10:37:23
Acknowledged.
| |
117 if len(current_chain) == 1: | |
118 yield ["[entry trampoline]"] | |
119 elif show_other: | |
120 current_chain[-1] = "[misattributed]" | |
121 yield current_chain | |
122 skip_until_end_of_chain = True | |
123 | |
124 | |
125 def calculate_samples_count_per_callchain(callchains): | |
126 chain_counters = collections.defaultdict(int) | |
127 for callchain in callchains: | |
128 # flamegraph.pl appears to have problems when no ; is in the chain, | |
129 # so we add a dummy one at the end if there is only one entry in the chain. | |
130 if len(callchain) == 1: | |
131 key = callchain[0] + ";" | |
rmcilroy
2016/04/15 14:58:42
Could you just always add a ";" to the end rather
Stefano Sanfilippo
2016/04/15 16:13:19
This is not the format shown in flamegraph example
rmcilroy
2016/04/18 09:22:24
I think it would be better to make things consiste
Stefano Sanfilippo
2016/04/18 10:37:23
No problem, done.
| |
132 else: | |
133 key = ";".join(reversed(callchain)) | |
134 chain_counters[key] += 1 | |
135 return chain_counters.items() | |
136 | |
137 | |
138 def calculate_samples_count_per_handler(callchains): | |
139 def strip_handler_prefix_if_any(handler): | |
140 return handler if handler[0] == "[" else handler.split(":", 1)[1] | |
141 | |
142 handler_counters = collections.defaultdict(int) | |
143 for callchain in callchains: | |
144 handler = strip_handler_prefix_if_any(callchain[-1]) | |
145 handler_counters[handler] += 1 | |
146 return handler_counters.items() | |
147 | |
148 | |
149 def write_flamegraph_input_file(output_stream, callchains): | |
150 for callchain, count in calculate_samples_count_per_callchain(callchains): | |
151 output_stream.write("{} {}\n".format(callchain, count)) | |
152 | |
153 | |
154 def write_handlers_report(output_stream, callchains): | |
155 handler_counters = calculate_samples_count_per_handler(callchains) | |
156 samples_num = sum(counter for _, counter in handler_counters) | |
157 # Sort by decreasing number of samples | |
158 handler_counters.sort(key=lambda entry: entry[1], reverse=True) | |
159 for bytecode_name, count in handler_counters: | |
160 output_stream.write( | |
161 "{}\t{}\t{:.3f}%\n".format(bytecode_name, count, | |
162 100. * count / samples_num)) | |
163 | |
164 | |
165 def parse_command_line(): | |
166 command_line_parser = argparse.ArgumentParser( | |
167 formatter_class=argparse.RawDescriptionHelpFormatter, | |
168 description=__DESCRIPTION, | |
169 epilog=__HELP_EPILOGUE) | |
170 | |
171 command_line_parser.add_argument( | |
172 "perf_filename", | |
173 help="perf sample file to process (default: perf.data)", | |
174 nargs="?", | |
175 default="perf.data", | |
176 metavar="<perf filename>" | |
177 ) | |
178 command_line_parser.add_argument( | |
179 "--flamegraph", "-f", | |
180 help="output an input file for flamegraph.pl, not a report", | |
181 action="store_true", | |
182 dest="output_flamegraph" | |
183 ) | |
184 command_line_parser.add_argument( | |
185 "--show-other", "-r", | |
rmcilroy
2016/04/15 14:58:41
-r is a strange shortening. How about "--show-all"
Stefano Sanfilippo
2016/04/15 16:13:18
Done.
| |
186 help="show samples outside Ignition interpreter", | |
187 action="store_true" | |
188 ) | |
189 command_line_parser.add_argument( | |
190 "--show-full-signatures", "-s", | |
191 help="show full signatures instead of function names", | |
192 action="store_true" | |
193 ) | |
194 command_line_parser.add_argument( | |
195 "--output", "-o", | |
196 help="output file name (stdout if omitted)", | |
197 type=argparse.FileType('wt'), | |
198 default=sys.stdout, | |
199 metavar="<output filename>", | |
200 dest="output_stream" | |
201 ) | |
202 | |
203 return command_line_parser.parse_args() | |
204 | |
205 | |
206 def main(): | |
207 program_options = parse_command_line() | |
208 | |
209 perf = subprocess.Popen(["perf", "script", "-f", "ip,sym", | |
210 "-i", program_options.perf_filename], | |
211 stdout=subprocess.PIPE) | |
212 | |
213 callchains = collapsed_callchains_generator( | |
214 perf.stdout, program_options.show_other, | |
215 program_options.show_full_signatures) | |
216 | |
217 if program_options.output_flamegraph: | |
218 write_flamegraph_input_file(program_options.output_stream, callchains) | |
219 else: | |
220 write_handlers_report(program_options.output_stream, callchains) | |
221 | |
222 perf.communicate() | |
rmcilroy
2016/04/15 14:58:42
Why is this needed? Shouldn't it be before reading
Stefano Sanfilippo
2016/04/15 16:13:18
This was intended to work as a wait() to ensure we
rmcilroy
2016/04/18 09:22:24
I don't see why this is necessary at all. You are
Stefano Sanfilippo
2016/04/18 10:37:23
Agreed, removed.
| |
223 | |
224 | |
225 if __name__ == "__main__": | |
226 main() | |
OLD | NEW |