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