blob: 472ccc2cfc566ea4ddbb10622028ce31554f05b4 [file] [log] [blame]
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use size 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 m from 'mithril';
import {sqliteString} from '../base/string_utils';
import {Actions} from '../common/actions';
import {ArgValue} from '../common/arg_types';
import {EngineProxy} from '../common/engine';
import {runQuery} from '../common/queries';
import {
formatDuration,
TPDuration,
tpDurationToSeconds,
TPTime,
} from '../common/time';
import {Argument, convertArgsToTree, Key} from '../controller/args_parser';
import {Anchor} from './anchor';
import {FlowPoint, globals, SliceDetails} from './globals';
import {runQueryInNewTab} from './query_result_tab';
import {verticalScrollToTrack} from './scroll_helper';
import {Icons} from './semantic_icons';
import {asTPTimestamp} from './sql_types';
import {Button} from './widgets/button';
import {DetailsShell} from './widgets/details_shell';
import {Duration} from './widgets/duration';
import {Column, GridLayout} from './widgets/grid_layout';
import {MenuItem, PopupMenu2} from './widgets/menu';
import {Section} from './widgets/section';
import {SqlRef} from './widgets/sql_ref';
import {Timestamp} from './widgets/timestamp';
import {Tree, TreeNode} from './widgets/tree';
import {exists} from './widgets/utils';
interface ContextMenuItem {
name: string;
shouldDisplay(slice: SliceDetails): boolean;
getAction(slice: SliceDetails): void;
}
const ITEMS: ContextMenuItem[] = [
{
name: 'Average duration',
shouldDisplay: (slice: SliceDetails) => slice.name !== undefined,
getAction: (slice: SliceDetails) => runQueryInNewTab(
`SELECT AVG(dur) / 1e9 FROM slice WHERE name = '${slice.name!}'`,
`${slice.name} average dur`,
),
},
{
name: 'Binder by TXN',
shouldDisplay: () => true,
getAction: () => runQueryInNewTab(
`SELECT IMPORT('android.binder');
SELECT *
FROM android_sync_binder_metrics_by_txn
ORDER BY client_dur DESC`,
'Binder by TXN',
),
},
{
name: 'Binder call names',
shouldDisplay: () => true,
getAction: (slice: SliceDetails) => {
const engine = getEngine();
if (engine === undefined) return;
runQuery(`SELECT IMPORT('android.binder');`, engine)
.then(
() => runQueryInNewTab(
`
SELECT s.ts, s.dur, tx.aidl_name AS name, s.id
FROM android_sync_binder_metrics_by_txn tx
JOIN slice s ON tx.binder_txn_id = s.id
JOIN thread_track ON s.track_id = thread_track.id
JOIN thread USING (utid)
JOIN process USING (upid)
WHERE aidl_name IS NOT NULL
AND pid = ${slice.pid}
AND tid = ${slice.tid}`,
`Binder names (${slice.processName}:${slice.tid})`,
));
},
},
{
name: 'Lock graph',
shouldDisplay: (slice: SliceDetails) => slice.id !== undefined,
getAction: (slice: SliceDetails) => runQueryInNewTab(
`SELECT IMPORT('android.monitor_contention');
DROP TABLE IF EXISTS FAST;
CREATE TABLE FAST
AS
WITH slice_process AS (
SELECT process.name, process.upid FROM slice
JOIN thread_track ON thread_track.id = slice.track_id
JOIN thread USING(utid)
JOIN process USING(upid)
WHERE slice.id = ${slice.id}
)
SELECT *,
IIF(blocked_thread_name LIKE 'binder:%', 'binder', blocked_thread_name)
AS blocked_thread_name_norm,
IIF(blocking_thread_name LIKE 'binder:%', 'binder', blocking_thread_name)
AS blocking_thread_name_norm
FROM android_monitor_contention_chain, slice_process
WHERE android_monitor_contention_chain.upid = slice_process.upid;
WITH
R AS (
SELECT
id,
dur,
CAT_STACKS(blocked_thread_name_norm || ':' || short_blocked_method,
blocking_thread_name_norm || ':' || short_blocking_method) AS stack
FROM FAST
WHERE parent_id IS NULL
UNION ALL
SELECT
c.id,
c.dur AS dur,
CAT_STACKS(stack, blocking_thread_name_norm || ':' || short_blocking_method) AS stack
FROM FAST c, R AS p
WHERE p.id = c.parent_id
)
SELECT TITLE.process_name, EXPERIMENTAL_PROFILE(stack, 'duration', 'ns', dur) AS pprof
FROM R, (SELECT process_name FROM FAST LIMIT 1) TITLE;`,
'Lock graph',
),
},
];
function getSliceContextMenuItems(slice: SliceDetails) {
return ITEMS.filter((item) => item.shouldDisplay(slice));
}
function getEngine(): EngineProxy|undefined {
const engineId = globals.getCurrentEngine()?.id;
if (engineId === undefined) {
return undefined;
}
const engine = globals.engines.get(engineId)?.getProxy('SlicePanel');
return engine;
}
function getArgValueRaw(value: ArgValue): string {
if (typeof value === 'object') {
return value.rawValue;
} else {
return value;
}
}
// Renders a key with a button to get dropdown things
function renderArgKey(
key: string, fullKey?: string, value?: ArgValue): m.Children {
if (value === undefined || fullKey === undefined) {
return key;
} else {
return m(
PopupMenu2,
{trigger: m(Anchor, {icon: Icons.ContextMenu}, key)},
fullKey && m(MenuItem, {
label: 'Copy full key',
icon: 'content_copy',
onclick: () => {
navigator.clipboard.writeText(fullKey);
},
}),
value && fullKey && m(MenuItem, {
label: 'Find slices with same arg value',
icon: 'search',
onclick: () => {
runQueryInNewTab(
`
select slice.*
from slice
join args using (arg_set_id)
where key=${sqliteString(fullKey)} and display_value=${
sqliteString(getArgValueRaw(value))}
`,
`Arg: ${sqliteString(fullKey)}=${
sqliteString(getArgValueRaw(value))}`);
},
}),
value && fullKey && m(MenuItem, {
label: 'Visualise argument values',
icon: 'query_stats',
onclick: () => {
globals.dispatch(Actions.addVisualisedArg({argName: fullKey}));
},
}),
);
}
}
// Try to render arg value as a special value, otherwise just render the text.
function renderArgValue(value: ArgValue): m.Children {
if (typeof value === 'object' && 'kind' in value) {
const {kind} = value;
if (kind === 'SLICE') {
// Value looks like a slice link.
const {sliceId, trackId} = value;
return renderSliceLink(sliceId, trackId, `slice[${sliceId}]`);
} else {
const x: never = kind;
throw new Error(`No support for args of kind '${x}'`);
}
} else if (typeof value === 'string') {
if (value.startsWith('http://') || value.startsWith('https://')) {
// Value looks like a web link.
return m(
Anchor, {href: value, target: '_blank', icon: 'open_in_new'}, value);
} else {
// Value is nothing special.
return value;
}
} else {
const x: never = value;
throw new Error(`Unable to process '${x}' as an arg value`);
}
}
function renderSliceLink(id: number, trackId: string, name: string) {
return m(
Anchor,
{
icon: 'call_made',
onclick: () => {
globals.makeSelection(
Actions.selectChromeSlice({id, trackId, table: 'slice'}));
// Ideally we want to have a callback to
// findCurrentSelection after this selection has been
// made. Here we do not have the info for horizontally
// scrolling to ts.
verticalScrollToTrack(trackId, true);
},
},
name);
}
function renderSummary(children: Argument<ArgValue>[]): m.Children {
const summary = children.slice(0, 2).map(({key}) => key).join(', ');
const remaining = children.length - 2;
if (remaining > 0) {
return `{${summary}, ... (${remaining} more items)}`;
} else {
return `{${summary}}`;
}
}
// Format any number of keys into a composite key with standardized formatting.
function stringifyKey(...key: Key[]): string {
return key
.map((element, index) => {
if (typeof element === 'number') {
return `[${element}]`;
} else {
return (index === 0 ? '' : '.') + element;
}
})
.join('');
}
function renderArgTreeNodes(args: Argument<ArgValue>[]): m.Children {
return args.map((arg) => {
const {key, path, value, children} = arg;
if (children && children.length === 1) {
// If we only have one child, collapse into self and combine keys
const child = children[0];
const compositeArg = {
...child,
key: stringifyKey(key, child.key),
};
return renderArgTreeNodes([compositeArg]);
} else {
return m(
TreeNode,
{
left: renderArgKey(stringifyKey(key), path, value),
right: exists(value) && renderArgValue(value),
summary: children && renderSummary(children),
},
children && renderArgTreeNodes(children),
);
}
});
}
interface Sliceish extends SliceDetails {
ts: TPTime;
dur: TPDuration;
name: string;
}
function isSliceish(slice: SliceDetails): slice is Sliceish {
return exists(slice.ts) && exists(slice.dur) && exists(slice.name);
}
function getDisplayName(name: string|undefined, id: number|undefined): string|
undefined {
if (name === undefined) {
return id === undefined ? undefined : `${id}`;
} else {
return id === undefined ? name : `${name} ${id}`;
}
}
function computeDuration(ts: TPTime, dur: TPDuration): m.Children {
if (dur === -1n) {
const minDuration = globals.state.traceTime.end - ts;
return `${formatDuration(minDuration)} (Did not end)`;
} else {
return m(Duration, {dur});
}
}
export class ChromeSliceDetailsPanel implements m.ClassComponent {
view() {
const slice = globals.sliceDetails;
if (isSliceish(slice)) {
return m(
DetailsShell,
{
title: 'Slice',
description: slice.name,
buttons: this.renderContextButton(slice),
},
m(
GridLayout,
this.renderDetails(slice),
this.renderRhs(slice),
),
);
} else {
return m(DetailsShell, {title: 'Slice', description: 'Loading...'});
}
}
private renderRhs(slice: Sliceish): m.Children {
const precFlows = this.renderPrecedingFlows(slice);
const followingFlows = this.renderFollowingFlows(slice);
const args = this.renderArguments(slice);
if (precFlows ?? followingFlows ?? args) {
return m(
Column,
precFlows,
followingFlows,
args,
);
} else {
return undefined;
}
}
private renderDetails(slice: Sliceish) {
return m(
Section,
{title: 'Details'},
m(Tree,
m(TreeNode, {left: 'Name', right: slice.name}),
m(TreeNode, {
left: 'Category',
right: !slice.category || slice.category === '[NULL]' ?
'N/A' :
slice.category,
}),
m(TreeNode, {
left: 'Start time',
right: m(Timestamp, {ts: asTPTimestamp(slice.ts)}),
}),
exists(slice.absTime) &&
m(TreeNode, {left: 'Absolute Time', right: slice.absTime}),
m(TreeNode, {
left: 'Duration',
right: computeDuration(slice.ts, slice.dur),
}),
this.renderThreadDuration(slice),
Array.from(this.getProcessThreadDetails(slice))
.map(
([key, value]) =>
exists(value) && m(TreeNode, {left: key, right: value})),
m(TreeNode, {
left: 'SQL ID',
right: m(SqlRef, {table: 'slice', id: slice.id}),
}),
slice.description &&
Array.from(slice.description)
.map(
([key, value]) => m(TreeNode, {left: key, right: value}),
)));
}
private getProcessThreadDetails(sliceInfo: SliceDetails) {
return new Map<string, string|undefined>([
['Thread', getDisplayName(sliceInfo.threadName, sliceInfo.tid)],
['Process', getDisplayName(sliceInfo.processName, sliceInfo.pid)],
['User ID', sliceInfo.uid ? String(sliceInfo.uid) : undefined],
['Package name', sliceInfo.packageName],
[
'Version code',
sliceInfo.versionCode ? String(sliceInfo.versionCode) : undefined,
],
]);
}
private renderThreadDuration(sliceInfo: Sliceish) {
if (exists(sliceInfo.threadTs) && exists(sliceInfo.threadDur)) {
// If we have valid thread duration, also display a percentage of
// |threadDur| compared to |dur|.
const ratio = tpDurationToSeconds(sliceInfo.threadDur) /
tpDurationToSeconds(sliceInfo.dur);
const threadDurFractionSuffix =
sliceInfo.threadDur === -1n ? '' : ` (${(ratio * 100).toFixed(2)}%)`;
return m(TreeNode, {
left: 'Thread duration',
right: [
computeDuration(sliceInfo.threadTs, sliceInfo.threadDur),
threadDurFractionSuffix,
],
});
} else {
return undefined;
}
}
private renderPrecedingFlows(slice: Sliceish): m.Children {
const flows = globals.connectedFlows;
const inFlows = flows.filter(({end}) => end.sliceId === slice.id);
if (inFlows.length > 0) {
const isRunTask = slice.name === 'ThreadControllerImpl::RunTask' ||
slice.name === 'ThreadPool_RunTask';
return m(
Section,
{title: 'Preceding Flows'},
m(
Tree,
inFlows.map(
({begin, dur}) => this.renderFlow(begin, dur, !isRunTask)),
));
} else {
return null;
}
}
private renderFollowingFlows(slice: Sliceish): m.Children {
const flows = globals.connectedFlows;
const outFlows = flows.filter(({begin}) => begin.sliceId === slice.id);
if (outFlows.length > 0) {
const isPostTask = slice.name === 'ThreadPool_PostTask' ||
slice.name === 'SequenceManager PostTask';
return m(
Section,
{title: 'Following Flows'},
m(
Tree,
outFlows.map(
({end, dur}) => this.renderFlow(end, dur, !isPostTask)),
));
} else {
return null;
}
}
private renderFlow(
flow: FlowPoint, dur: TPDuration,
includeProcessName: boolean): m.Children {
const sliceId = flow.sliceId;
const trackId = globals.state.uiTrackIdByTraceTrackId[flow.trackId];
const description = flow.sliceChromeCustomName === undefined ?
flow.sliceName :
flow.sliceChromeCustomName;
const sliceLink = renderSliceLink(sliceId, trackId, description);
const threadName = includeProcessName ?
`${flow.threadName} (${flow.processName})` :
flow.threadName;
return m(
TreeNode,
{left: 'Flow'},
m(TreeNode, {left: 'Slice', right: sliceLink}),
m(TreeNode, {left: 'Delay', right: m(Duration, {dur})}),
m(TreeNode, {left: 'Thread', right: threadName}),
);
}
private renderArguments(slice: Sliceish): m.Children {
if (slice.args && slice.args.size > 0) {
const tree = convertArgsToTree(slice.args);
return m(
Section, {title: 'Arguments'}, m(Tree, renderArgTreeNodes(tree)));
} else {
return undefined;
}
}
private renderContextButton(sliceInfo: SliceDetails): m.Children {
const contextMenuItems = getSliceContextMenuItems(sliceInfo);
if (contextMenuItems.length > 0) {
const trigger = m(Button, {
minimal: true,
compact: true,
label: 'Contextual Options',
rightIcon: Icons.ContextMenu,
});
return m(
PopupMenu2,
{trigger},
contextMenuItems.map(
({name, getAction}) =>
m(MenuItem, {label: name, onclick: getAction})),
);
} else {
return undefined;
}
}
}