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