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