• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright (C) 2021 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
18import argparse
19import bisect
20import jinja2
21import io
22import math
23import os
24import pandas as pd
25from pathlib import Path
26import re
27import sys
28
29from bokeh.embed import components
30from bokeh.io import output_file, show
31from bokeh.layouts import layout, Spacer
32from bokeh.models import ColumnDataSource, CustomJS, WheelZoomTool, HoverTool, FuncTickFormatter
33from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
34from bokeh.models.ranges import FactorRange
35from bokeh.palettes import Category20b
36from bokeh.plotting import figure
37from bokeh.resources import INLINE
38from bokeh.transform import jitter
39from bokeh.util.browser import view
40from functools import cmp_to_key
41
42# fmt: off
43simpleperf_path = Path(__file__).absolute().parents[1]
44sys.path.insert(0, str(simpleperf_path))
45import simpleperf_report_lib as sp
46from simpleperf_utils import BaseArgumentParser
47# fmt: on
48
49
50def create_graph(args, source, data_range):
51    graph = figure(
52        sizing_mode='stretch_both', x_range=data_range,
53        tools=['pan', 'wheel_zoom', 'ywheel_zoom', 'xwheel_zoom', 'reset', 'tap', 'box_select'],
54        active_drag='box_select', active_scroll='wheel_zoom',
55        tooltips=[('thread', '@thread'),
56                  ('callchain', '@callchain{safe}')],
57        title=args.title, name='graph')
58
59    # a crude way to avoid process name cluttering at some zoom levels.
60    # TODO: remove processes from the ticker base on the number of samples currently visualized.
61    # The process with most samples visualized should always be visible on the ticker
62    graph.xaxis.formatter = FuncTickFormatter(args={'range': data_range, 'graph': graph}, code="""
63    var pixels_per_entry = graph.inner_height / (range.end - range.start) //Do not rond end and start here
64    var entries_to_skip = Math.ceil(12 / pixels_per_entry) // kind of 12 px per entry
65    var desc = tick.split(/:| /)
66    // desc[0] == desc[1] for main threads
67    var keep = (desc[0] == desc[1]) &&
68      !(desc[2].includes('unknown') ||
69        desc[2].includes('Binder')  ||
70        desc[2].includes('kworker'))
71
72    if (pixels_per_entry < 8 && !keep) {
73      //if (index + Math.round(range.start)) % entries_to_skip != 0) {
74      return ""
75    }
76
77    return tick """)
78
79    graph.xaxis.major_label_orientation = math.pi/6
80
81    graph.circle(y='time',
82                 x='thread',
83                 source=source,
84                 color='color',
85                 alpha=0.3,
86                 selection_fill_color='White',
87                 selection_line_color='Black',
88                 selection_line_width=0.5,
89                 selection_alpha=1.0)
90
91    graph.y_range.range_padding = 0
92    graph.xgrid.grid_line_color = None
93    return graph
94
95
96def create_table(graph):
97    # Empty dataframe, will be filled up in js land
98    empty_data = {'thread': [], 'count': []}
99    table_source = ColumnDataSource(pd.DataFrame(
100        empty_data, columns=['thread', 'count'], index=None))
101    graph_source = graph.renderers[0].data_source
102
103    columns = [
104        TableColumn(field='thread', title='Thread'),
105        TableColumn(field='count', title='Count')
106    ]
107
108    # start with a small table size (stretch doesn't reduce from the preferred size)
109    table = DataTable(
110        width=100,
111        height=100,
112        sizing_mode='stretch_both',
113        source=table_source,
114        columns=columns,
115        index_position=None,
116        name='table')
117
118    graph_selection_cb = CustomJS(code='update_selections()')
119
120    graph_source.selected.js_on_change('indices', graph_selection_cb)
121    table_source.selected.js_on_change('indices', CustomJS(args={}, code='update_flamegraph()'))
122
123    return table
124
125
126def generate_template(template_file='index.html.jinja2'):
127    loader = jinja2.FileSystemLoader(
128        searchpath=os.path.dirname(os.path.realpath(__file__)) + '/templates/')
129
130    env = jinja2.Environment(loader=loader)
131    return env.get_template(template_file)
132
133
134def generate_html(args, components_dict, title):
135    resources = INLINE.render()
136    script, div = components(components_dict)
137    return generate_template().render(
138        resources=resources, plot_script=script, plot_div=div, title=title)
139
140
141class ThreadDescriptor:
142    def __init__(self, pid, tid, name):
143        self.name = name
144        self.tid = tid
145        self.pid = pid
146
147    def __lt__(self, other):
148        return self.pid < other.pid or (self.pid == other.pid and self.tid < other.tid)
149
150    def __gt__(self, other):
151        return self.pid > other.pid or (self.pid == other.pid and self.tid > other.tid)
152
153    def __eq__(self, other):
154        return self.pid == other.pid and self.tid == other.tid and self.name == other.name
155
156    def __str__(self):
157        return str(self.pid) + ':' + str(self.tid) + ' ' + self.name
158
159
160def generate_datasource(args):
161    lib = sp.ReportLib()
162    lib.ShowIpForUnknownSymbol()
163
164    if args.usyms:
165        lib.SetSymfs(args.usyms)
166
167    if args.input_file:
168        lib.SetRecordFile(args.input_file)
169
170    if args.ksyms:
171        lib.SetKallsymsFile(args.ksyms)
172
173    lib.SetReportOptions(args.report_lib_options)
174
175    product = lib.MetaInfo().get('product_props')
176
177    if product:
178        manufacturer, model, name = product.split(':')
179
180    start_time = -1
181    end_time = -1
182
183    times = []
184    threads = []
185    thread_descs = []
186    callchains = []
187
188    while True:
189        sample = lib.GetNextSample()
190
191        if sample is None:
192            lib.Close()
193            break
194
195        symbol = lib.GetSymbolOfCurrentSample()
196        callchain = lib.GetCallChainOfCurrentSample()
197
198        if start_time == -1:
199            start_time = sample.time
200
201        sample_time = (sample.time - start_time) / 1e6  # convert to ms
202
203        times.append(sample_time)
204
205        if sample_time > end_time:
206            end_time = sample_time
207
208        thread_desc = ThreadDescriptor(sample.pid, sample.tid, sample.thread_comm)
209
210        threads.append(str(thread_desc))
211
212        if thread_desc not in thread_descs:
213            bisect.insort(thread_descs, thread_desc)
214
215        callchain_str = ''
216
217        for i in range(callchain.nr):
218            symbol = callchain.entries[i].symbol  # SymbolStruct
219            entry_line = ''
220
221            if args.include_dso_names:
222                entry_line += symbol._dso_name.decode('utf-8') + ':'
223
224            entry_line += symbol._symbol_name.decode('utf-8')
225
226            if args.include_symbols_addr:
227                entry_line += ':' + hex(symbol.symbol_addr)
228
229            if i < callchain.nr - 1:
230                callchain_str += entry_line + '<br>'
231
232        callchains.append(callchain_str)
233
234    # define colors per-process
235    palette = Category20b[20]
236    color_map = {}
237
238    last_pid = -1
239    palette_index = 0
240
241    for thread_desc in thread_descs:
242        if thread_desc.pid != last_pid:
243            last_pid = thread_desc.pid
244            palette_index += 1
245            palette_index %= len(palette)
246
247            color_map[str(thread_desc.pid)] = palette[palette_index]
248
249    colors = []
250    for sample_thread in threads:
251        pid = str(sample_thread.split(':')[0])
252        colors.append(color_map[pid])
253
254    threads_range = [str(thread_desc) for thread_desc in thread_descs]
255    data_range = FactorRange(factors=threads_range, bounds='auto')
256
257    data = {'time': times,
258            'thread': threads,
259            'callchain': callchains,
260            'color': colors}
261
262    source = ColumnDataSource(data)
263
264    return source, data_range
265
266
267def main():
268    parser = BaseArgumentParser()
269    parser.add_argument('-i', '--input_file', type=str, required=True, help='input file')
270    parser.add_argument('--title', '-t', type=str, help='document title')
271    parser.add_argument('--ksyms', '-k', type=str, help='path to kernel symbols (kallsyms)')
272    parser.add_argument('--usyms', '-u', type=str, help='path to tree with user space symbols')
273    parser.add_argument('--output', '-o', type=str, help='output file')
274    parser.add_argument('--dont_open', '-d', action='store_true', help='Don\'t open output file')
275    parser.add_argument('--include_dso_names', '-n', action='store_true',
276                        help='Include dso names in backtraces')
277    parser.add_argument('--include_symbols_addr', '-s', action='store_true',
278                        help='Include addresses of symbols in backtraces')
279    parser.add_report_lib_options(default_show_art_frames=True)
280
281    args = parser.parse_args()
282
283    # TODO test hierarchical ranges too
284    source, data_range = generate_datasource(args)
285
286    graph = create_graph(args, source, data_range)
287    table = create_table(graph)
288
289    output_filename = args.output
290
291    if not output_filename:
292        output_filename = os.path.splitext(os.path.basename(args.input_file))[0] + '.html'
293
294    title = os.path.splitext(os.path.basename(output_filename))[0]
295
296    html = generate_html(args, {'graph': graph, 'table': table}, title)
297
298    with io.open(output_filename, mode='w', encoding='utf-8') as fout:
299        fout.write(html)
300
301    if not args.dont_open:
302        view(output_filename)
303
304
305if __name__ == "__main__":
306    main()
307