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