1#!/usr/bin/env python 2# 3# Copyright (C) 2015 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17 18"""Simpleperf gui reporter: provide gui interface for simpleperf report command. 19 20There are two ways to use gui reporter. One way is to pass it a report file 21generated by simpleperf report command, and reporter will display it. The 22other ways is to pass it any arguments you want to use when calling 23simpleperf report command. The reporter will call `simpleperf report` to 24generate report file, and display it. 25""" 26 27import os.path 28import re 29import subprocess 30import sys 31 32try: 33 from tkinter import * 34 from tkinter.font import Font 35 from tkinter.ttk import * 36except ImportError: 37 from Tkinter import * 38 from tkFont import Font 39 from ttk import * 40 41from utils import * 42 43PAD_X = 3 44PAD_Y = 3 45 46 47class CallTreeNode(object): 48 49 """Representing a node in call-graph.""" 50 51 def __init__(self, percentage, function_name): 52 self.percentage = percentage 53 self.call_stack = [function_name] 54 self.children = [] 55 56 def add_call(self, function_name): 57 self.call_stack.append(function_name) 58 59 def add_child(self, node): 60 self.children.append(node) 61 62 def __str__(self): 63 strs = self.dump() 64 return '\n'.join(strs) 65 66 def dump(self): 67 strs = [] 68 strs.append('CallTreeNode percentage = %.2f' % self.percentage) 69 for function_name in self.call_stack: 70 strs.append(' %s' % function_name) 71 for child in self.children: 72 child_strs = child.dump() 73 strs.extend([' ' + x for x in child_strs]) 74 return strs 75 76 77class ReportItem(object): 78 79 """Representing one item in report, may contain a CallTree.""" 80 81 def __init__(self, raw_line): 82 self.raw_line = raw_line 83 self.call_tree = None 84 85 def __str__(self): 86 strs = [] 87 strs.append('ReportItem (raw_line %s)' % self.raw_line) 88 if self.call_tree is not None: 89 strs.append('%s' % self.call_tree) 90 return '\n'.join(strs) 91 92class EventReport(object): 93 94 """Representing report for one event attr.""" 95 96 def __init__(self, common_report_context): 97 self.context = common_report_context[:] 98 self.title_line = None 99 self.report_items = [] 100 101 102def parse_event_reports(lines): 103 # Parse common report context 104 common_report_context = [] 105 line_id = 0 106 while line_id < len(lines): 107 line = lines[line_id] 108 if not line or line.find('Event:') == 0: 109 break 110 common_report_context.append(line) 111 line_id += 1 112 113 event_reports = [] 114 in_report_context = True 115 cur_event_report = EventReport(common_report_context) 116 cur_report_item = None 117 call_tree_stack = {} 118 vertical_columns = [] 119 last_node = None 120 121 has_skipped_callgraph = False 122 123 for line in lines[line_id:]: 124 if not line: 125 in_report_context = not in_report_context 126 if in_report_context: 127 cur_event_report = EventReport(common_report_context) 128 continue 129 130 if in_report_context: 131 cur_event_report.context.append(line) 132 if line.find('Event:') == 0: 133 event_reports.append(cur_event_report) 134 continue 135 136 if cur_event_report.title_line is None: 137 cur_event_report.title_line = line 138 elif not line[0].isspace(): 139 cur_report_item = ReportItem(line) 140 cur_event_report.report_items.append(cur_report_item) 141 # Each report item can have different column depths. 142 vertical_columns = [] 143 else: 144 for i in range(len(line)): 145 if line[i] == '|': 146 if not vertical_columns or vertical_columns[-1] < i: 147 vertical_columns.append(i) 148 149 if not line.strip('| \t'): 150 continue 151 if line.find('skipped in brief callgraph mode') != -1: 152 has_skipped_callgraph = True 153 continue 154 155 if line.find('-') == -1: 156 line = line.strip('| \t') 157 function_name = line 158 last_node.add_call(function_name) 159 else: 160 pos = line.find('-') 161 depth = -1 162 for i in range(len(vertical_columns)): 163 if pos >= vertical_columns[i]: 164 depth = i 165 assert depth != -1 166 167 line = line.strip('|- \t') 168 m = re.search(r'^([\d\.]+)%[-\s]+(.+)$', line) 169 if m: 170 percentage = float(m.group(1)) 171 function_name = m.group(2) 172 else: 173 percentage = 100.0 174 function_name = line 175 176 node = CallTreeNode(percentage, function_name) 177 if depth == 0: 178 cur_report_item.call_tree = node 179 else: 180 call_tree_stack[depth - 1].add_child(node) 181 call_tree_stack[depth] = node 182 last_node = node 183 184 if has_skipped_callgraph: 185 log_warning('some callgraphs are skipped in brief callgraph mode') 186 187 return event_reports 188 189 190class ReportWindow(object): 191 192 """A window used to display report file.""" 193 194 def __init__(self, master, report_context, title_line, report_items): 195 frame = Frame(master) 196 frame.pack(fill=BOTH, expand=1) 197 198 font = Font(family='courier', size=12) 199 200 # Report Context 201 for line in report_context: 202 label = Label(frame, text=line, font=font) 203 label.pack(anchor=W, padx=PAD_X, pady=PAD_Y) 204 205 # Space 206 label = Label(frame, text='', font=font) 207 label.pack(anchor=W, padx=PAD_X, pady=PAD_Y) 208 209 # Title 210 label = Label(frame, text=' ' + title_line, font=font) 211 label.pack(anchor=W, padx=PAD_X, pady=PAD_Y) 212 213 # Report Items 214 report_frame = Frame(frame) 215 report_frame.pack(fill=BOTH, expand=1) 216 217 yscrollbar = Scrollbar(report_frame) 218 yscrollbar.pack(side=RIGHT, fill=Y) 219 xscrollbar = Scrollbar(report_frame, orient=HORIZONTAL) 220 xscrollbar.pack(side=BOTTOM, fill=X) 221 222 tree = Treeview(report_frame, columns=[title_line], show='') 223 tree.pack(side=LEFT, fill=BOTH, expand=1) 224 tree.tag_configure('set_font', font=font) 225 226 tree.config(yscrollcommand=yscrollbar.set) 227 yscrollbar.config(command=tree.yview) 228 tree.config(xscrollcommand=xscrollbar.set) 229 xscrollbar.config(command=tree.xview) 230 231 self.display_report_items(tree, report_items) 232 233 def display_report_items(self, tree, report_items): 234 for report_item in report_items: 235 prefix_str = '+ ' if report_item.call_tree is not None else ' ' 236 id = tree.insert( 237 '', 238 'end', 239 None, 240 values=[ 241 prefix_str + 242 report_item.raw_line], 243 tag='set_font') 244 if report_item.call_tree is not None: 245 self.display_call_tree(tree, id, report_item.call_tree, 1) 246 247 def display_call_tree(self, tree, parent_id, node, indent): 248 id = parent_id 249 indent_str = ' ' * indent 250 251 if node.percentage != 100.0: 252 percentage_str = '%.2f%% ' % node.percentage 253 else: 254 percentage_str = '' 255 256 for i in range(len(node.call_stack)): 257 s = indent_str 258 s += '+ ' if node.children and i == len(node.call_stack) - 1 else ' ' 259 s += percentage_str if i == 0 else ' ' * len(percentage_str) 260 s += node.call_stack[i] 261 child_open = False if i == len(node.call_stack) - 1 and indent > 1 else True 262 id = tree.insert(id, 'end', None, values=[s], open=child_open, 263 tag='set_font') 264 265 for child in node.children: 266 self.display_call_tree(tree, id, child, indent + 1) 267 268 269def display_report_file(report_file): 270 fh = open(report_file, 'r') 271 lines = fh.readlines() 272 fh.close() 273 274 lines = [x.rstrip() for x in lines] 275 event_reports = parse_event_reports(lines) 276 277 if event_reports: 278 root = Tk() 279 for i in range(len(event_reports)): 280 report = event_reports[i] 281 parent = root if i == 0 else Toplevel(root) 282 ReportWindow(parent, report.context, report.title_line, report.report_items) 283 root.mainloop() 284 285 286def call_simpleperf_report(args, report_file): 287 output_fh = open(report_file, 'w') 288 simpleperf_path = get_host_binary_path('simpleperf') 289 args = [simpleperf_path, 'report', '--full-callgraph'] + args 290 subprocess.check_call(args, stdout=output_fh) 291 output_fh.close() 292 293 294def main(): 295 if len(sys.argv) == 2 and os.path.isfile(sys.argv[1]): 296 display_report_file(sys.argv[1]) 297 else: 298 call_simpleperf_report(sys.argv[1:], 'perf.report') 299 display_report_file('perf.report') 300 301 302if __name__ == '__main__': 303 main() 304