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 28import os.path 29import re 30import subprocess 31import sys 32 33try: 34 from tkinter import * 35 from tkinter.font import Font 36 from tkinter.ttk import * 37except ImportError: 38 from Tkinter import * 39 from tkFont import Font 40 from ttk import * 41 42from utils import * 43 44PAD_X = 3 45PAD_Y = 3 46 47 48class CallTreeNode(object): 49 50 """Representing a node in call-graph.""" 51 52 def __init__(self, percentage, function_name): 53 self.percentage = percentage 54 self.call_stack = [function_name] 55 self.children = [] 56 57 def add_call(self, function_name): 58 self.call_stack.append(function_name) 59 60 def add_child(self, node): 61 self.children.append(node) 62 63 def __str__(self): 64 strs = self.dump() 65 return '\n'.join(strs) 66 67 def dump(self): 68 strs = [] 69 strs.append('CallTreeNode percentage = %.2f' % self.percentage) 70 for function_name in self.call_stack: 71 strs.append(' %s' % function_name) 72 for child in self.children: 73 child_strs = child.dump() 74 strs.extend([' ' + x for x in child_strs]) 75 return strs 76 77 78class ReportItem(object): 79 80 """Representing one item in report, may contain a CallTree.""" 81 82 def __init__(self, raw_line): 83 self.raw_line = raw_line 84 self.call_tree = None 85 86 def __str__(self): 87 strs = [] 88 strs.append('ReportItem (raw_line %s)' % self.raw_line) 89 if self.call_tree is not None: 90 strs.append('%s' % self.call_tree) 91 return '\n'.join(strs) 92 93class EventReport(object): 94 95 """Representing report for one event attr.""" 96 97 def __init__(self, common_report_context): 98 self.context = common_report_context[:] 99 self.title_line = None 100 self.report_items = [] 101 102 103def parse_event_reports(lines): 104 # Parse common report context 105 common_report_context = [] 106 line_id = 0 107 while line_id < len(lines): 108 line = lines[line_id] 109 if not line or line.find('Event:') == 0: 110 break 111 common_report_context.append(line) 112 line_id += 1 113 114 event_reports = [] 115 in_report_context = True 116 cur_event_report = EventReport(common_report_context) 117 cur_report_item = None 118 call_tree_stack = {} 119 vertical_columns = [] 120 last_node = None 121 122 has_skipped_callgraph = False 123 124 for line in lines[line_id:]: 125 if not line: 126 in_report_context = not in_report_context 127 if in_report_context: 128 cur_event_report = EventReport(common_report_context) 129 continue 130 131 if in_report_context: 132 cur_event_report.context.append(line) 133 if line.find('Event:') == 0: 134 event_reports.append(cur_event_report) 135 continue 136 137 if cur_event_report.title_line is None: 138 cur_event_report.title_line = line 139 elif not line[0].isspace(): 140 cur_report_item = ReportItem(line) 141 cur_event_report.report_items.append(cur_report_item) 142 # Each report item can have different column depths. 143 vertical_columns = [] 144 else: 145 for i in range(len(line)): 146 if line[i] == '|': 147 if not vertical_columns or vertical_columns[-1] < i: 148 vertical_columns.append(i) 149 150 if not line.strip('| \t'): 151 continue 152 if 'skipped in brief callgraph mode' in line: 153 has_skipped_callgraph = True 154 continue 155 156 if line.find('-') == -1: 157 line = line.strip('| \t') 158 function_name = line 159 last_node.add_call(function_name) 160 else: 161 pos = line.find('-') 162 depth = -1 163 for i in range(len(vertical_columns)): 164 if pos >= vertical_columns[i]: 165 depth = i 166 assert depth != -1 167 168 line = line.strip('|- \t') 169 m = re.search(r'^([\d\.]+)%[-\s]+(.+)$', line) 170 if m: 171 percentage = float(m.group(1)) 172 function_name = m.group(2) 173 else: 174 percentage = 100.0 175 function_name = line 176 177 node = CallTreeNode(percentage, function_name) 178 if depth == 0: 179 cur_report_item.call_tree = node 180 else: 181 call_tree_stack[depth - 1].add_child(node) 182 call_tree_stack[depth] = node 183 last_node = node 184 185 if has_skipped_callgraph: 186 log_warning('some callgraphs are skipped in brief callgraph mode') 187 188 return event_reports 189 190 191class ReportWindow(object): 192 193 """A window used to display report file.""" 194 195 def __init__(self, main, report_context, title_line, report_items): 196 frame = Frame(main) 197 frame.pack(fill=BOTH, expand=1) 198 199 font = Font(family='courier', size=12) 200 201 # Report Context 202 for line in report_context: 203 label = Label(frame, text=line, font=font) 204 label.pack(anchor=W, padx=PAD_X, pady=PAD_Y) 205 206 # Space 207 label = Label(frame, text='', font=font) 208 label.pack(anchor=W, padx=PAD_X, pady=PAD_Y) 209 210 # Title 211 label = Label(frame, text=' ' + title_line, font=font) 212 label.pack(anchor=W, padx=PAD_X, pady=PAD_Y) 213 214 # Report Items 215 report_frame = Frame(frame) 216 report_frame.pack(fill=BOTH, expand=1) 217 218 yscrollbar = Scrollbar(report_frame) 219 yscrollbar.pack(side=RIGHT, fill=Y) 220 xscrollbar = Scrollbar(report_frame, orient=HORIZONTAL) 221 xscrollbar.pack(side=BOTTOM, fill=X) 222 223 tree = Treeview(report_frame, columns=[title_line], show='') 224 tree.pack(side=LEFT, fill=BOTH, expand=1) 225 tree.tag_configure('set_font', font=font) 226 227 tree.config(yscrollcommand=yscrollbar.set) 228 yscrollbar.config(command=tree.yview) 229 tree.config(xscrollcommand=xscrollbar.set) 230 xscrollbar.config(command=tree.xview) 231 232 self.display_report_items(tree, report_items) 233 234 def display_report_items(self, tree, report_items): 235 for report_item in report_items: 236 prefix_str = '+ ' if report_item.call_tree is not None else ' ' 237 id = tree.insert( 238 '', 239 'end', 240 None, 241 values=[ 242 prefix_str + 243 report_item.raw_line], 244 tag='set_font') 245 if report_item.call_tree is not None: 246 self.display_call_tree(tree, id, report_item.call_tree, 1) 247 248 def display_call_tree(self, tree, parent_id, node, indent): 249 id = parent_id 250 indent_str = ' ' * indent 251 252 if node.percentage != 100.0: 253 percentage_str = '%.2f%% ' % node.percentage 254 else: 255 percentage_str = '' 256 257 for i in range(len(node.call_stack)): 258 s = indent_str 259 s += '+ ' if node.children and i == len(node.call_stack) - 1 else ' ' 260 s += percentage_str if i == 0 else ' ' * len(percentage_str) 261 s += node.call_stack[i] 262 child_open = False if i == len(node.call_stack) - 1 and indent > 1 else True 263 id = tree.insert(id, 'end', None, values=[s], open=child_open, 264 tag='set_font') 265 266 for child in node.children: 267 self.display_call_tree(tree, id, child, indent + 1) 268 269 270def display_report_file(report_file, self_kill_after_sec): 271 fh = open(report_file, 'r') 272 lines = fh.readlines() 273 fh.close() 274 275 lines = [x.rstrip() for x in lines] 276 event_reports = parse_event_reports(lines) 277 278 if event_reports: 279 root = Tk() 280 for i in range(len(event_reports)): 281 report = event_reports[i] 282 parent = root if i == 0 else Toplevel(root) 283 ReportWindow(parent, report.context, report.title_line, report.report_items) 284 if self_kill_after_sec: 285 root.after(self_kill_after_sec * 1000, lambda: root.destroy()) 286 root.mainloop() 287 288 289def call_simpleperf_report(args, show_gui, self_kill_after_sec): 290 simpleperf_path = get_host_binary_path('simpleperf') 291 if not show_gui: 292 subprocess.check_call([simpleperf_path, 'report'] + args) 293 else: 294 report_file = 'perf.report' 295 subprocess.check_call([simpleperf_path, 'report', '--full-callgraph'] + args + 296 ['-o', report_file]) 297 display_report_file(report_file, self_kill_after_sec=self_kill_after_sec) 298 299 300def get_simpleperf_report_help_msg(): 301 simpleperf_path = get_host_binary_path('simpleperf') 302 args = [simpleperf_path, 'report', '-h'] 303 proc = subprocess.Popen(args, stdout=subprocess.PIPE) 304 (stdoutdata, _) = proc.communicate() 305 stdoutdata = bytes_to_str(stdoutdata) 306 return stdoutdata[stdoutdata.find('\n') + 1:] 307 308 309def main(): 310 self_kill_after_sec = 0 311 args = sys.argv[1:] 312 if args and args[0] == "--self-kill-for-testing": 313 self_kill_after_sec = 1 314 args = args[1:] 315 if len(args) == 1 and os.path.isfile(args[0]): 316 display_report_file(args[0], self_kill_after_sec=self_kill_after_sec) 317 318 i = 0 319 args_for_report_cmd = [] 320 show_gui = False 321 while i < len(args): 322 if args[i] == '-h' or args[i] == '--help': 323 print('report.py A python wrapper for simpleperf report command.') 324 print('Options supported by simpleperf report command:') 325 print(get_simpleperf_report_help_msg()) 326 print('\nOptions supported by report.py:') 327 print('--gui Show report result in a gui window.') 328 print('\nIt also supports showing a report generated by simpleperf report cmd:') 329 print('\n python report.py report_file') 330 sys.exit(0) 331 elif args[i] == '--gui': 332 show_gui = True 333 i += 1 334 else: 335 args_for_report_cmd.append(args[i]) 336 i += 1 337 338 call_simpleperf_report(args_for_report_cmd, show_gui, self_kill_after_sec) 339 340 341if __name__ == '__main__': 342 main() 343