Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/python | |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 import json | |
| 7 import re | |
| 8 import sys | |
| 9 | |
| 10 """Converts the output of "perf script" into json for about:tracing. | |
| 11 | |
| 12 Usage: perf script | convert_perf_script_to_tracing_json.py > trace.json | |
| 13 """ | |
| 14 | |
| 15 # FIXME: Signal to the traceviewer that this is a CPU profile so it shows | |
| 16 # samples instead of ms as the units. | |
|
eseidel
2014/04/05 00:03:13
The samples themselves can sorta represent 0.1ms o
ojan
2014/04/05 00:13:50
The perf script output doesn't say what the sampli
vmiura
2014/04/08 19:33:50
In my experience the perf sampling interval is qui
| |
| 17 | |
| 18 def strip_to_last_paren(line): | |
|
eseidel
2014/04/05 00:03:13
I'm surprised you didn't just use a regexp to matc
| |
| 19 last_paren_index = line.rfind('(') | |
| 20 if last_paren_index == -1: | |
| 21 return line | |
| 22 return line[:last_paren_index] | |
| 23 | |
| 24 def extract_function_name(line): | |
| 25 # This information from the stack doesn't seem terribly useful. | |
| 26 line = line.replace('(anonymous namespace)::', '') | |
| 27 line = line.replace('non-virtual thunk to ', '') | |
| 28 | |
| 29 # Strip executable name. | |
| 30 line = strip_to_last_paren(line) | |
| 31 # Strip function arguments. | |
|
eseidel
2014/04/05 00:03:13
Yeah, I think a regexp would be more readable, may
ojan
2014/04/05 00:13:50
I had a lot of trouble coming up with a regexp tha
| |
| 32 line = strip_to_last_paren(line) | |
| 33 | |
| 34 line = line.strip() | |
| 35 line = re.sub('\s+', ' ', line) | |
| 36 | |
| 37 first_space_index = line.find(' ') | |
| 38 if first_space_index == -1: | |
| 39 # Unsymbolized addresses. | |
| 40 return line | |
| 41 return line[first_space_index + 1:] | |
| 42 | |
| 43 def collapse_perf_script_output(lines): | |
| 44 collapsed_lines = {} | |
| 45 stack_so_far = [] | |
| 46 thread_id = '' | |
| 47 | |
| 48 for line in lines: | |
| 49 line = line.strip() | |
| 50 | |
| 51 if not line: | |
| 52 if stack_so_far: | |
| 53 stack = ';'.join(stack_so_far) | |
| 54 collapsed_lines[thread_id][stack] = ( | |
| 55 collapsed_lines[thread_id].setdefault(stack, 0) + 1) | |
| 56 stack_so_far = [] | |
| 57 continue | |
| 58 | |
| 59 if line[0] == '#': | |
| 60 continue | |
| 61 | |
| 62 match_header = re.match('\w+\ (\w+)\ cycles:\s*$', line) | |
| 63 if match_header: | |
| 64 thread_id = match_header.group(1) | |
| 65 if not thread_id in collapsed_lines: | |
| 66 collapsed_lines[thread_id] = {} | |
| 67 continue | |
| 68 | |
| 69 stack_so_far.insert(0, extract_function_name(line)) | |
| 70 | |
| 71 return collapsed_lines | |
| 72 | |
| 73 def add_sample(root, thread_id, current_stack, start_time, end_time, | |
| 74 original_stack): | |
| 75 node_so_far = root | |
| 76 for function in current_stack: | |
| 77 # Can get the same stack on different threads, so identify samples by | |
| 78 # combination of function name and thread_id. | |
| 79 key = function + ';' + thread_id | |
| 80 if key in node_so_far: | |
| 81 node_so_far[key]['end_time'] = end_time | |
| 82 else: | |
| 83 node_so_far[key] = { | |
| 84 'children': {}, | |
| 85 'start_time': start_time, | |
| 86 'end_time': end_time, | |
| 87 } | |
| 88 node_so_far = node_so_far[key]['children'] | |
| 89 | |
| 90 def compute_tree(collapsed_lines): | |
| 91 total_samples_per_thread = {} | |
| 92 total_samples_for_all_threads = 0 | |
| 93 tree = {} | |
| 94 | |
| 95 for thread_id in collapsed_lines: | |
| 96 for stack in sorted(collapsed_lines[thread_id].iterkeys()): | |
| 97 samples = collapsed_lines[thread_id][stack] | |
| 98 total_samples_for_thread = total_samples_per_thread.setdefault( | |
| 99 thread_id, 0) | |
| 100 add_sample(tree, thread_id, stack.split(';'), | |
| 101 total_samples_for_thread, total_samples_for_thread + samples, | |
| 102 stack) | |
| 103 total_samples_per_thread[thread_id] += samples | |
| 104 total_samples_for_all_threads += samples | |
| 105 | |
| 106 return tree, total_samples_for_all_threads, total_samples_per_thread | |
| 107 | |
| 108 def json_for_subtree(node, trace_data, total_samples_for_all_threads, | |
| 109 total_samples_per_thread): | |
| 110 for key in sorted(node.iterkeys()): | |
| 111 function, thread_id = key.split(';') | |
| 112 start_time = int(node[key]['start_time']) | |
| 113 end_time = int(node[key]['end_time']) | |
| 114 duration = end_time - start_time | |
| 115 process_percent = '%2.2f%%' % ( | |
| 116 100 * float(duration) / total_samples_for_all_threads) | |
| 117 thread_percent = '%2.2f%%' % ( | |
| 118 100 * float(duration) / total_samples_per_thread[thread_id]) | |
| 119 | |
| 120 # FIXME: extract out process IDs. | |
| 121 children = node[key]['children'] | |
| 122 | |
| 123 # If there are no children, we can use a Complete event instead two | |
| 124 # Duration events. | |
| 125 if not children: | |
| 126 trace_data.append({ | |
|
eseidel
2014/04/05 00:05:09
Crazy. When I started down this path I just used
ojan
2014/04/05 00:13:50
What are immediate events?
dsinclair
2014/04/08 02:28:36
Immediate events are drawn as a line, instead of a
ojan
2014/04/08 19:38:40
The heading fragment doesn't seem to work. Searchi
dsinclair
2014/04/08 19:42:44
Sorry, I should use the right words, we call them
| |
| 127 "pid": 1, | |
| 128 "tid": thread_id, | |
| 129 "name": function, | |
| 130 "ts": start_time, | |
| 131 "dur": duration, | |
| 132 "ph": "X", | |
| 133 "args": { | |
| 134 "process percent": process_percent, | |
| 135 "thread percent": thread_percent | |
| 136 }, | |
| 137 }) | |
| 138 continue | |
| 139 | |
| 140 trace_data.append({ | |
|
dsinclair
2014/04/08 02:28:36
I think, although I maybe mistaken, you could use
ojan
2014/04/08 19:38:40
I'm not sure what you're suggesting...how would th
dsinclair
2014/04/08 19:42:44
We create the nested slices based on timestamps no
| |
| 141 "pid": 1, | |
| 142 "tid": thread_id, | |
| 143 "name": function, | |
| 144 "ts": start_time, | |
| 145 "ph": "B", | |
| 146 }) | |
| 147 | |
| 148 json_for_subtree(children, trace_data, total_samples_for_all_threads, | |
| 149 total_samples_per_thread) | |
| 150 | |
| 151 trace_data.append({ | |
| 152 "pid": 1, | |
| 153 "tid": thread_id, | |
| 154 "name": function, | |
| 155 "ts": end_time, | |
| 156 "ph": "E", | |
| 157 "args": { | |
| 158 "process percent": process_percent, | |
| 159 "thread percent": thread_percent | |
| 160 }, | |
| 161 }) | |
| 162 | |
| 163 def stringified_json_output(lines): | |
| 164 tree, total_samples_for_all_threads, total_samples_per_thread = ( | |
| 165 compute_tree(collapse_perf_script_output(lines))) | |
| 166 trace_data = [] | |
| 167 json_for_subtree(tree, trace_data, total_samples_for_all_threads, | |
| 168 total_samples_per_thread) | |
| 169 return json.dumps(trace_data, separators=(',',':')) | |
| 170 | |
| 171 if __name__ == "__main__": | |
| 172 print stringified_json_output(sys.stdin.readlines()) | |
| OLD | NEW |