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