Android Build Tools
This commit is contained in:
306
Android/android-ndk-r27d/simpleperf/purgatorio/purgatorio.py
Normal file
306
Android/android-ndk-r27d/simpleperf/purgatorio/purgatorio.py
Normal file
@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (C) 2021 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import argparse
|
||||
import bisect
|
||||
import jinja2
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from bokeh.embed import components
|
||||
from bokeh.io import output_file, show
|
||||
from bokeh.layouts import layout, Spacer
|
||||
from bokeh.models import ColumnDataSource, CustomJS, WheelZoomTool, HoverTool, FuncTickFormatter
|
||||
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
|
||||
from bokeh.models.ranges import FactorRange
|
||||
from bokeh.palettes import Category20b
|
||||
from bokeh.plotting import figure
|
||||
from bokeh.resources import INLINE
|
||||
from bokeh.transform import jitter
|
||||
from bokeh.util.browser import view
|
||||
from functools import cmp_to_key
|
||||
|
||||
# fmt: off
|
||||
simpleperf_path = Path(__file__).absolute().parents[1]
|
||||
sys.path.insert(0, str(simpleperf_path))
|
||||
import simpleperf_report_lib as sp
|
||||
from simpleperf_utils import BaseArgumentParser
|
||||
# fmt: on
|
||||
|
||||
|
||||
def create_graph(args, source, data_range):
|
||||
graph = figure(
|
||||
sizing_mode='stretch_both', x_range=data_range,
|
||||
tools=['pan', 'wheel_zoom', 'ywheel_zoom', 'xwheel_zoom', 'reset', 'tap', 'box_select'],
|
||||
active_drag='box_select', active_scroll='wheel_zoom',
|
||||
tooltips=[('thread', '@thread'),
|
||||
('callchain', '@callchain{safe}')],
|
||||
title=args.title, name='graph')
|
||||
|
||||
# a crude way to avoid process name cluttering at some zoom levels.
|
||||
# TODO: remove processes from the ticker base on the number of samples currently visualized.
|
||||
# The process with most samples visualized should always be visible on the ticker
|
||||
graph.xaxis.formatter = FuncTickFormatter(args={'range': data_range, 'graph': graph}, code="""
|
||||
var pixels_per_entry = graph.inner_height / (range.end - range.start) //Do not rond end and start here
|
||||
var entries_to_skip = Math.ceil(12 / pixels_per_entry) // kind of 12 px per entry
|
||||
var desc = tick.split(/:| /)
|
||||
// desc[0] == desc[1] for main threads
|
||||
var keep = (desc[0] == desc[1]) &&
|
||||
!(desc[2].includes('unknown') ||
|
||||
desc[2].includes('Binder') ||
|
||||
desc[2].includes('kworker'))
|
||||
|
||||
if (pixels_per_entry < 8 && !keep) {
|
||||
//if (index + Math.round(range.start)) % entries_to_skip != 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return tick """)
|
||||
|
||||
graph.xaxis.major_label_orientation = math.pi/6
|
||||
|
||||
graph.circle(y='time',
|
||||
x='thread',
|
||||
source=source,
|
||||
color='color',
|
||||
alpha=0.3,
|
||||
selection_fill_color='White',
|
||||
selection_line_color='Black',
|
||||
selection_line_width=0.5,
|
||||
selection_alpha=1.0)
|
||||
|
||||
graph.y_range.range_padding = 0
|
||||
graph.xgrid.grid_line_color = None
|
||||
return graph
|
||||
|
||||
|
||||
def create_table(graph):
|
||||
# Empty dataframe, will be filled up in js land
|
||||
empty_data = {'thread': [], 'count': []}
|
||||
table_source = ColumnDataSource(pd.DataFrame(
|
||||
empty_data, columns=['thread', 'count'], index=None))
|
||||
graph_source = graph.renderers[0].data_source
|
||||
|
||||
columns = [
|
||||
TableColumn(field='thread', title='Thread'),
|
||||
TableColumn(field='count', title='Count')
|
||||
]
|
||||
|
||||
# start with a small table size (stretch doesn't reduce from the preferred size)
|
||||
table = DataTable(
|
||||
width=100,
|
||||
height=100,
|
||||
sizing_mode='stretch_both',
|
||||
source=table_source,
|
||||
columns=columns,
|
||||
index_position=None,
|
||||
name='table')
|
||||
|
||||
graph_selection_cb = CustomJS(code='update_selections()')
|
||||
|
||||
graph_source.selected.js_on_change('indices', graph_selection_cb)
|
||||
table_source.selected.js_on_change('indices', CustomJS(args={}, code='update_flamegraph()'))
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def generate_template(template_file='index.html.jinja2'):
|
||||
loader = jinja2.FileSystemLoader(
|
||||
searchpath=os.path.dirname(os.path.realpath(__file__)) + '/templates/')
|
||||
|
||||
env = jinja2.Environment(loader=loader)
|
||||
return env.get_template(template_file)
|
||||
|
||||
|
||||
def generate_html(args, components_dict, title):
|
||||
resources = INLINE.render()
|
||||
script, div = components(components_dict)
|
||||
return generate_template().render(
|
||||
resources=resources, plot_script=script, plot_div=div, title=title)
|
||||
|
||||
|
||||
class ThreadDescriptor:
|
||||
def __init__(self, pid, tid, name):
|
||||
self.name = name
|
||||
self.tid = tid
|
||||
self.pid = pid
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.pid < other.pid or (self.pid == other.pid and self.tid < other.tid)
|
||||
|
||||
def __gt__(self, other):
|
||||
return self.pid > other.pid or (self.pid == other.pid and self.tid > other.tid)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.pid == other.pid and self.tid == other.tid and self.name == other.name
|
||||
|
||||
def __str__(self):
|
||||
return str(self.pid) + ':' + str(self.tid) + ' ' + self.name
|
||||
|
||||
|
||||
def generate_datasource(args):
|
||||
lib = sp.ReportLib()
|
||||
lib.ShowIpForUnknownSymbol()
|
||||
|
||||
if args.usyms:
|
||||
lib.SetSymfs(args.usyms)
|
||||
|
||||
if args.input_file:
|
||||
lib.SetRecordFile(args.input_file)
|
||||
|
||||
if args.ksyms:
|
||||
lib.SetKallsymsFile(args.ksyms)
|
||||
|
||||
lib.SetReportOptions(args.report_lib_options)
|
||||
|
||||
product = lib.MetaInfo().get('product_props')
|
||||
|
||||
if product:
|
||||
manufacturer, model, name = product.split(':')
|
||||
|
||||
start_time = -1
|
||||
end_time = -1
|
||||
|
||||
times = []
|
||||
threads = []
|
||||
thread_descs = []
|
||||
callchains = []
|
||||
|
||||
while True:
|
||||
sample = lib.GetNextSample()
|
||||
|
||||
if sample is None:
|
||||
lib.Close()
|
||||
break
|
||||
|
||||
symbol = lib.GetSymbolOfCurrentSample()
|
||||
callchain = lib.GetCallChainOfCurrentSample()
|
||||
|
||||
if start_time == -1:
|
||||
start_time = sample.time
|
||||
|
||||
sample_time = (sample.time - start_time) / 1e6 # convert to ms
|
||||
|
||||
times.append(sample_time)
|
||||
|
||||
if sample_time > end_time:
|
||||
end_time = sample_time
|
||||
|
||||
thread_desc = ThreadDescriptor(sample.pid, sample.tid, sample.thread_comm)
|
||||
|
||||
threads.append(str(thread_desc))
|
||||
|
||||
if thread_desc not in thread_descs:
|
||||
bisect.insort(thread_descs, thread_desc)
|
||||
|
||||
callchain_str = ''
|
||||
|
||||
for i in range(callchain.nr):
|
||||
symbol = callchain.entries[i].symbol # SymbolStruct
|
||||
entry_line = ''
|
||||
|
||||
if args.include_dso_names:
|
||||
entry_line += symbol._dso_name.decode('utf-8') + ':'
|
||||
|
||||
entry_line += symbol._symbol_name.decode('utf-8')
|
||||
|
||||
if args.include_symbols_addr:
|
||||
entry_line += ':' + hex(symbol.symbol_addr)
|
||||
|
||||
if i < callchain.nr - 1:
|
||||
callchain_str += entry_line + '<br>'
|
||||
|
||||
callchains.append(callchain_str)
|
||||
|
||||
# define colors per-process
|
||||
palette = Category20b[20]
|
||||
color_map = {}
|
||||
|
||||
last_pid = -1
|
||||
palette_index = 0
|
||||
|
||||
for thread_desc in thread_descs:
|
||||
if thread_desc.pid != last_pid:
|
||||
last_pid = thread_desc.pid
|
||||
palette_index += 1
|
||||
palette_index %= len(palette)
|
||||
|
||||
color_map[str(thread_desc.pid)] = palette[palette_index]
|
||||
|
||||
colors = []
|
||||
for sample_thread in threads:
|
||||
pid = str(sample_thread.split(':')[0])
|
||||
colors.append(color_map[pid])
|
||||
|
||||
threads_range = [str(thread_desc) for thread_desc in thread_descs]
|
||||
data_range = FactorRange(factors=threads_range, bounds='auto')
|
||||
|
||||
data = {'time': times,
|
||||
'thread': threads,
|
||||
'callchain': callchains,
|
||||
'color': colors}
|
||||
|
||||
source = ColumnDataSource(data)
|
||||
|
||||
return source, data_range
|
||||
|
||||
|
||||
def main():
|
||||
parser = BaseArgumentParser()
|
||||
parser.add_argument('-i', '--input_file', type=str, required=True, help='input file')
|
||||
parser.add_argument('--title', '-t', type=str, help='document title')
|
||||
parser.add_argument('--ksyms', '-k', type=str, help='path to kernel symbols (kallsyms)')
|
||||
parser.add_argument('--usyms', '-u', type=str, help='path to tree with user space symbols')
|
||||
parser.add_argument('--output', '-o', type=str, help='output file')
|
||||
parser.add_argument('--dont_open', '-d', action='store_true', help='Don\'t open output file')
|
||||
parser.add_argument('--include_dso_names', '-n', action='store_true',
|
||||
help='Include dso names in backtraces')
|
||||
parser.add_argument('--include_symbols_addr', '-s', action='store_true',
|
||||
help='Include addresses of symbols in backtraces')
|
||||
parser.add_report_lib_options(default_show_art_frames=True)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# TODO test hierarchical ranges too
|
||||
source, data_range = generate_datasource(args)
|
||||
|
||||
graph = create_graph(args, source, data_range)
|
||||
table = create_table(graph)
|
||||
|
||||
output_filename = args.output
|
||||
|
||||
if not output_filename:
|
||||
output_filename = os.path.splitext(os.path.basename(args.input_file))[0] + '.html'
|
||||
|
||||
title = os.path.splitext(os.path.basename(output_filename))[0]
|
||||
|
||||
html = generate_html(args, {'graph': graph, 'table': table}, title)
|
||||
|
||||
with io.open(output_filename, mode='w', encoding='utf-8') as fout:
|
||||
fout.write(html)
|
||||
|
||||
if not args.dont_open:
|
||||
view(output_filename)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user