Chromium Code Reviews| Index: tools/convert_perf_script_to_tracing_json.py |
| diff --git a/tools/convert_perf_script_to_tracing_json.py b/tools/convert_perf_script_to_tracing_json.py |
| new file mode 100755 |
| index 0000000000000000000000000000000000000000..3bb468008d830e816f025baa8b3f524dc1d1cd21 |
| --- /dev/null |
| +++ b/tools/convert_perf_script_to_tracing_json.py |
| @@ -0,0 +1,172 @@ |
| +#!/usr/bin/python |
| +# Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
| +import json |
| +import re |
| +import sys |
| + |
| +"""Converts the output of "perf script" into json for about:tracing. |
| + |
| +Usage: perf script | convert_perf_script_to_tracing_json.py > trace.json |
| +""" |
| + |
| +# FIXME: Signal to the traceviewer that this is a CPU profile so it shows |
| +# 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
|
| + |
| +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
|
| + last_paren_index = line.rfind('(') |
| + if last_paren_index == -1: |
| + return line |
| + return line[:last_paren_index] |
| + |
| +def extract_function_name(line): |
| + # This information from the stack doesn't seem terribly useful. |
| + line = line.replace('(anonymous namespace)::', '') |
| + line = line.replace('non-virtual thunk to ', '') |
| + |
| + # Strip executable name. |
| + line = strip_to_last_paren(line) |
| + # 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
|
| + line = strip_to_last_paren(line) |
| + |
| + line = line.strip() |
| + line = re.sub('\s+', ' ', line) |
| + |
| + first_space_index = line.find(' ') |
| + if first_space_index == -1: |
| + # Unsymbolized addresses. |
| + return line |
| + return line[first_space_index + 1:] |
| + |
| +def collapse_perf_script_output(lines): |
| + collapsed_lines = {} |
| + stack_so_far = [] |
| + thread_id = '' |
| + |
| + for line in lines: |
| + line = line.strip() |
| + |
| + if not line: |
| + if stack_so_far: |
| + stack = ';'.join(stack_so_far) |
| + collapsed_lines[thread_id][stack] = ( |
| + collapsed_lines[thread_id].setdefault(stack, 0) + 1) |
| + stack_so_far = [] |
| + continue |
| + |
| + if line[0] == '#': |
| + continue |
| + |
| + match_header = re.match('\w+\ (\w+)\ cycles:\s*$', line) |
| + if match_header: |
| + thread_id = match_header.group(1) |
| + if not thread_id in collapsed_lines: |
| + collapsed_lines[thread_id] = {} |
| + continue |
| + |
| + stack_so_far.insert(0, extract_function_name(line)) |
| + |
| + return collapsed_lines |
| + |
| +def add_sample(root, thread_id, current_stack, start_time, end_time, |
| + original_stack): |
| + node_so_far = root |
| + for function in current_stack: |
| + # Can get the same stack on different threads, so identify samples by |
| + # combination of function name and thread_id. |
| + key = function + ';' + thread_id |
| + if key in node_so_far: |
| + node_so_far[key]['end_time'] = end_time |
| + else: |
| + node_so_far[key] = { |
| + 'children': {}, |
| + 'start_time': start_time, |
| + 'end_time': end_time, |
| + } |
| + node_so_far = node_so_far[key]['children'] |
| + |
| +def compute_tree(collapsed_lines): |
| + total_samples_per_thread = {} |
| + total_samples_for_all_threads = 0 |
| + tree = {} |
| + |
| + for thread_id in collapsed_lines: |
| + for stack in sorted(collapsed_lines[thread_id].iterkeys()): |
| + samples = collapsed_lines[thread_id][stack] |
| + total_samples_for_thread = total_samples_per_thread.setdefault( |
| + thread_id, 0) |
| + add_sample(tree, thread_id, stack.split(';'), |
| + total_samples_for_thread, total_samples_for_thread + samples, |
| + stack) |
| + total_samples_per_thread[thread_id] += samples |
| + total_samples_for_all_threads += samples |
| + |
| + return tree, total_samples_for_all_threads, total_samples_per_thread |
| + |
| +def json_for_subtree(node, trace_data, total_samples_for_all_threads, |
| + total_samples_per_thread): |
| + for key in sorted(node.iterkeys()): |
| + function, thread_id = key.split(';') |
| + start_time = int(node[key]['start_time']) |
| + end_time = int(node[key]['end_time']) |
| + duration = end_time - start_time |
| + process_percent = '%2.2f%%' % ( |
| + 100 * float(duration) / total_samples_for_all_threads) |
| + thread_percent = '%2.2f%%' % ( |
| + 100 * float(duration) / total_samples_per_thread[thread_id]) |
| + |
| + # FIXME: extract out process IDs. |
| + children = node[key]['children'] |
| + |
| + # If there are no children, we can use a Complete event instead two |
| + # Duration events. |
| + if not children: |
| + 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
|
| + "pid": 1, |
| + "tid": thread_id, |
| + "name": function, |
| + "ts": start_time, |
| + "dur": duration, |
| + "ph": "X", |
| + "args": { |
| + "process percent": process_percent, |
| + "thread percent": thread_percent |
| + }, |
| + }) |
| + continue |
| + |
| + 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
|
| + "pid": 1, |
| + "tid": thread_id, |
| + "name": function, |
| + "ts": start_time, |
| + "ph": "B", |
| + }) |
| + |
| + json_for_subtree(children, trace_data, total_samples_for_all_threads, |
| + total_samples_per_thread) |
| + |
| + trace_data.append({ |
| + "pid": 1, |
| + "tid": thread_id, |
| + "name": function, |
| + "ts": end_time, |
| + "ph": "E", |
| + "args": { |
| + "process percent": process_percent, |
| + "thread percent": thread_percent |
| + }, |
| + }) |
| + |
| +def stringified_json_output(lines): |
| + tree, total_samples_for_all_threads, total_samples_per_thread = ( |
| + compute_tree(collapse_perf_script_output(lines))) |
| + trace_data = [] |
| + json_for_subtree(tree, trace_data, total_samples_for_all_threads, |
| + total_samples_per_thread) |
| + return json.dumps(trace_data, separators=(',',':')) |
| + |
| +if __name__ == "__main__": |
| + print stringified_json_output(sys.stdin.readlines()) |