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