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