You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
8.5 KiB
303 lines
8.5 KiB
#!/usr/bin/env python |
|
# |
|
# Copyright (C) 2015 The Android Open Source Project |
|
# |
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
|
# you may not use this file except in compliance with the License. |
|
# You may obtain a copy of the License at |
|
# |
|
# http://www.apache.org/licenses/LICENSE-2.0 |
|
# |
|
# Unless required by applicable law or agreed to in writing, software |
|
# distributed under the License is distributed on an "AS IS" BASIS, |
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
# See the License for the specific language governing permissions and |
|
# limitations under the License. |
|
# |
|
|
|
"""Simpleperf gui reporter: provide gui interface for simpleperf report command. |
|
|
|
There are two ways to use gui reporter. One way is to pass it a report file |
|
generated by simpleperf report command, and reporter will display it. The |
|
other ways is to pass it any arguments you want to use when calling |
|
simpleperf report command. The reporter will call `simpleperf report` to |
|
generate report file, and display it. |
|
""" |
|
|
|
import os.path |
|
import re |
|
import subprocess |
|
import sys |
|
|
|
try: |
|
from tkinter import * |
|
from tkinter.font import Font |
|
from tkinter.ttk import * |
|
except ImportError: |
|
from Tkinter import * |
|
from tkFont import Font |
|
from ttk import * |
|
|
|
from utils import * |
|
|
|
PAD_X = 3 |
|
PAD_Y = 3 |
|
|
|
|
|
class CallTreeNode(object): |
|
|
|
"""Representing a node in call-graph.""" |
|
|
|
def __init__(self, percentage, function_name): |
|
self.percentage = percentage |
|
self.call_stack = [function_name] |
|
self.children = [] |
|
|
|
def add_call(self, function_name): |
|
self.call_stack.append(function_name) |
|
|
|
def add_child(self, node): |
|
self.children.append(node) |
|
|
|
def __str__(self): |
|
strs = self.dump() |
|
return '\n'.join(strs) |
|
|
|
def dump(self): |
|
strs = [] |
|
strs.append('CallTreeNode percentage = %.2f' % self.percentage) |
|
for function_name in self.call_stack: |
|
strs.append(' %s' % function_name) |
|
for child in self.children: |
|
child_strs = child.dump() |
|
strs.extend([' ' + x for x in child_strs]) |
|
return strs |
|
|
|
|
|
class ReportItem(object): |
|
|
|
"""Representing one item in report, may contain a CallTree.""" |
|
|
|
def __init__(self, raw_line): |
|
self.raw_line = raw_line |
|
self.call_tree = None |
|
|
|
def __str__(self): |
|
strs = [] |
|
strs.append('ReportItem (raw_line %s)' % self.raw_line) |
|
if self.call_tree is not None: |
|
strs.append('%s' % self.call_tree) |
|
return '\n'.join(strs) |
|
|
|
class EventReport(object): |
|
|
|
"""Representing report for one event attr.""" |
|
|
|
def __init__(self, common_report_context): |
|
self.context = common_report_context[:] |
|
self.title_line = None |
|
self.report_items = [] |
|
|
|
|
|
def parse_event_reports(lines): |
|
# Parse common report context |
|
common_report_context = [] |
|
line_id = 0 |
|
while line_id < len(lines): |
|
line = lines[line_id] |
|
if not line or line.find('Event:') == 0: |
|
break |
|
common_report_context.append(line) |
|
line_id += 1 |
|
|
|
event_reports = [] |
|
in_report_context = True |
|
cur_event_report = EventReport(common_report_context) |
|
cur_report_item = None |
|
call_tree_stack = {} |
|
vertical_columns = [] |
|
last_node = None |
|
|
|
has_skipped_callgraph = False |
|
|
|
for line in lines[line_id:]: |
|
if not line: |
|
in_report_context = not in_report_context |
|
if in_report_context: |
|
cur_event_report = EventReport(common_report_context) |
|
continue |
|
|
|
if in_report_context: |
|
cur_event_report.context.append(line) |
|
if line.find('Event:') == 0: |
|
event_reports.append(cur_event_report) |
|
continue |
|
|
|
if cur_event_report.title_line is None: |
|
cur_event_report.title_line = line |
|
elif not line[0].isspace(): |
|
cur_report_item = ReportItem(line) |
|
cur_event_report.report_items.append(cur_report_item) |
|
# Each report item can have different column depths. |
|
vertical_columns = [] |
|
else: |
|
for i in range(len(line)): |
|
if line[i] == '|': |
|
if not vertical_columns or vertical_columns[-1] < i: |
|
vertical_columns.append(i) |
|
|
|
if not line.strip('| \t'): |
|
continue |
|
if line.find('skipped in brief callgraph mode') != -1: |
|
has_skipped_callgraph = True |
|
continue |
|
|
|
if line.find('-') == -1: |
|
line = line.strip('| \t') |
|
function_name = line |
|
last_node.add_call(function_name) |
|
else: |
|
pos = line.find('-') |
|
depth = -1 |
|
for i in range(len(vertical_columns)): |
|
if pos >= vertical_columns[i]: |
|
depth = i |
|
assert depth != -1 |
|
|
|
line = line.strip('|- \t') |
|
m = re.search(r'^([\d\.]+)%[-\s]+(.+)$', line) |
|
if m: |
|
percentage = float(m.group(1)) |
|
function_name = m.group(2) |
|
else: |
|
percentage = 100.0 |
|
function_name = line |
|
|
|
node = CallTreeNode(percentage, function_name) |
|
if depth == 0: |
|
cur_report_item.call_tree = node |
|
else: |
|
call_tree_stack[depth - 1].add_child(node) |
|
call_tree_stack[depth] = node |
|
last_node = node |
|
|
|
if has_skipped_callgraph: |
|
log_warning('some callgraphs are skipped in brief callgraph mode') |
|
|
|
return event_reports |
|
|
|
|
|
class ReportWindow(object): |
|
|
|
"""A window used to display report file.""" |
|
|
|
def __init__(self, master, report_context, title_line, report_items): |
|
frame = Frame(master) |
|
frame.pack(fill=BOTH, expand=1) |
|
|
|
font = Font(family='courier', size=12) |
|
|
|
# Report Context |
|
for line in report_context: |
|
label = Label(frame, text=line, font=font) |
|
label.pack(anchor=W, padx=PAD_X, pady=PAD_Y) |
|
|
|
# Space |
|
label = Label(frame, text='', font=font) |
|
label.pack(anchor=W, padx=PAD_X, pady=PAD_Y) |
|
|
|
# Title |
|
label = Label(frame, text=' ' + title_line, font=font) |
|
label.pack(anchor=W, padx=PAD_X, pady=PAD_Y) |
|
|
|
# Report Items |
|
report_frame = Frame(frame) |
|
report_frame.pack(fill=BOTH, expand=1) |
|
|
|
yscrollbar = Scrollbar(report_frame) |
|
yscrollbar.pack(side=RIGHT, fill=Y) |
|
xscrollbar = Scrollbar(report_frame, orient=HORIZONTAL) |
|
xscrollbar.pack(side=BOTTOM, fill=X) |
|
|
|
tree = Treeview(report_frame, columns=[title_line], show='') |
|
tree.pack(side=LEFT, fill=BOTH, expand=1) |
|
tree.tag_configure('set_font', font=font) |
|
|
|
tree.config(yscrollcommand=yscrollbar.set) |
|
yscrollbar.config(command=tree.yview) |
|
tree.config(xscrollcommand=xscrollbar.set) |
|
xscrollbar.config(command=tree.xview) |
|
|
|
self.display_report_items(tree, report_items) |
|
|
|
def display_report_items(self, tree, report_items): |
|
for report_item in report_items: |
|
prefix_str = '+ ' if report_item.call_tree is not None else ' ' |
|
id = tree.insert( |
|
'', |
|
'end', |
|
None, |
|
values=[ |
|
prefix_str + |
|
report_item.raw_line], |
|
tag='set_font') |
|
if report_item.call_tree is not None: |
|
self.display_call_tree(tree, id, report_item.call_tree, 1) |
|
|
|
def display_call_tree(self, tree, parent_id, node, indent): |
|
id = parent_id |
|
indent_str = ' ' * indent |
|
|
|
if node.percentage != 100.0: |
|
percentage_str = '%.2f%% ' % node.percentage |
|
else: |
|
percentage_str = '' |
|
|
|
for i in range(len(node.call_stack)): |
|
s = indent_str |
|
s += '+ ' if node.children and i == len(node.call_stack) - 1 else ' ' |
|
s += percentage_str if i == 0 else ' ' * len(percentage_str) |
|
s += node.call_stack[i] |
|
child_open = False if i == len(node.call_stack) - 1 and indent > 1 else True |
|
id = tree.insert(id, 'end', None, values=[s], open=child_open, |
|
tag='set_font') |
|
|
|
for child in node.children: |
|
self.display_call_tree(tree, id, child, indent + 1) |
|
|
|
|
|
def display_report_file(report_file): |
|
fh = open(report_file, 'r') |
|
lines = fh.readlines() |
|
fh.close() |
|
|
|
lines = [x.rstrip() for x in lines] |
|
event_reports = parse_event_reports(lines) |
|
|
|
if event_reports: |
|
root = Tk() |
|
for i in range(len(event_reports)): |
|
report = event_reports[i] |
|
parent = root if i == 0 else Toplevel(root) |
|
ReportWindow(parent, report.context, report.title_line, report.report_items) |
|
root.mainloop() |
|
|
|
|
|
def call_simpleperf_report(args, report_file): |
|
output_fh = open(report_file, 'w') |
|
simpleperf_path = get_host_binary_path('simpleperf') |
|
args = [simpleperf_path, 'report', '--full-callgraph'] + args |
|
subprocess.check_call(args, stdout=output_fh) |
|
output_fh.close() |
|
|
|
|
|
def main(): |
|
if len(sys.argv) == 2 and os.path.isfile(sys.argv[1]): |
|
display_report_file(sys.argv[1]) |
|
else: |
|
call_simpleperf_report(sys.argv[1:], 'perf.report') |
|
display_report_file('perf.report') |
|
|
|
|
|
if __name__ == '__main__': |
|
main()
|
|
|