blob: d29ecbacb908bbbf0a5138e6b8d07a3620334288 [file]
// 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 {Icons} from '../../base/semantic_icons';
import {TimeSpan} from '../../base/time';
import {exists} from '../../base/utils';
import type {Engine} from '../../trace_processor/engine';
import {Button, ButtonVariant} from '../../widgets/button';
import {DetailsShell} from '../../widgets/details_shell';
import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
import {MenuItem, PopupMenu} from '../../widgets/menu';
import {Section} from '../../widgets/section';
import {Tree} from '../../widgets/tree';
import type {FlowPoint} from '../../core/flow_types';
import {hasArgs} from './args';
import {
type DistributionScope,
findSliceTrackDataset,
findWholeTraceSliceDataset,
renderDetails,
sliceDistributionConfig,
} from './slice_details';
import {getSlice, type SliceDetails} from '../sql_utils/slice';
import {
type BreakdownByThreadState,
breakDownIntervalByThreadState,
} from './thread_state';
import {asSliceSqlId} from '../sql_utils/core_types';
import {DurationWidget} from '../widgets/duration';
import {Grid, GridCell, GridHeaderCell} from '../../widgets/grid';
import {ensureIsInstance} from '../../base/assert';
import type {Trace} from '../../public/trace';
import type {TrackEventDetailsPanel} from '../../public/details_panel';
import type {TrackEventSelection} from '../../public/selection';
import {extensions} from '../extensions';
import {TraceImpl} from '../../core/trace_impl';
import {renderSliceArguments} from './slice_args';
import {SLICE_TABLE} from '../widgets/sql/table_definitions';
import {TrackEventRef} from '../widgets/track_event_ref';
import {
DistributionSummary,
HISTOGRAM_HELP,
SCOPE_HELP,
helpIcon,
openDistributionTab,
renderDistributionPlaceholder,
titleWithHelp,
} from '../distribution_panel';
import type {Dataset} from '../../trace_processor/dataset';
interface ContextMenuItem {
name: string;
shouldDisplay(slice: SliceDetails): boolean;
run(slice: SliceDetails, trace: Trace): void;
}
function getTidFromSlice(slice: SliceDetails): bigint | undefined {
return slice.thread?.tid;
}
function getPidFromSlice(slice: SliceDetails): bigint | undefined {
return slice.process?.pid;
}
function getProcessNameFromSlice(slice: SliceDetails): string | undefined {
return slice.process?.name;
}
function getThreadNameFromSlice(slice: SliceDetails): string | undefined {
return slice.thread?.name;
}
function hasTid(slice: SliceDetails): boolean {
return getTidFromSlice(slice) !== undefined;
}
function hasPid(slice: SliceDetails): boolean {
return getPidFromSlice(slice) !== undefined;
}
function hasProcessName(slice: SliceDetails): boolean {
return getProcessNameFromSlice(slice) !== undefined;
}
function hasThreadName(slice: SliceDetails): boolean {
return getThreadNameFromSlice(slice) !== undefined;
}
const ITEMS: ContextMenuItem[] = [
{
name: 'Ancestor slices',
shouldDisplay: (slice: SliceDetails) => slice.parentId !== undefined,
run: (slice: SliceDetails, trace: Trace) =>
extensions.addLegacySqlTableTab(trace, {
table: SLICE_TABLE,
filters: [
{
op: (cols) =>
`${cols[0]} IN (SELECT id FROM _slice_ancestor_and_self(${slice.id}))`,
columns: ['id'],
},
],
imports: ['slices.hierarchy'],
}),
},
{
name: 'Descendant slices',
shouldDisplay: () => true,
run: (slice: SliceDetails, trace: Trace) =>
extensions.addLegacySqlTableTab(trace, {
table: SLICE_TABLE,
filters: [
{
op: (cols) =>
`${cols[0]} IN (SELECT id FROM _slice_descendant_and_self(${slice.id}))`,
columns: ['id'],
},
],
imports: ['slices.hierarchy'],
}),
},
{
name: 'Binder txn names + monitor contention on thread',
shouldDisplay: (slice) =>
hasProcessName(slice) &&
hasThreadName(slice) &&
hasTid(slice) &&
hasPid(slice),
run: (slice: SliceDetails, trace: Trace) => {
trace.engine
.query(
`INCLUDE PERFETTO MODULE android.binder;
INCLUDE PERFETTO MODULE android.monitor_contention;`,
)
.then(() =>
extensions.addDebugSliceTrack({
trace,
data: {
sqlSource: `
WITH merged AS (
SELECT s.ts, s.dur, tx.aidl_name AS name, 0 AS depth
FROM android_binder_txns 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 pid = ${getPidFromSlice(slice)}
AND tid = ${getTidFromSlice(slice)}
AND aidl_name IS NOT NULL
UNION ALL
SELECT
s.ts,
s.dur,
short_blocked_method || ' -> ' || blocking_thread_name || ':' || short_blocking_method AS name,
1 AS depth
FROM android_binder_txns tx
JOIN android_monitor_contention m
ON m.binder_reply_tid = tx.server_tid AND m.binder_reply_ts = tx.server_ts
JOIN slice s
ON tx.binder_txn_id = s.id
JOIN thread_track
ON s.track_id = thread_track.id
JOIN thread ON thread.utid = thread_track.utid
JOIN process ON process.upid = thread.upid
WHERE process.pid = ${getPidFromSlice(slice)}
AND thread.tid = ${getTidFromSlice(slice)}
AND short_blocked_method IS NOT NULL
ORDER BY depth
) SELECT ts, dur, name FROM merged`,
},
title: `Binder names (${getProcessNameFromSlice(
slice,
)}:${getThreadNameFromSlice(slice)})`,
}),
);
},
},
];
function getSliceContextMenuItems(slice: SliceDetails) {
return ITEMS.filter((item) => item.shouldDisplay(slice));
}
async function getSliceDetails(
engine: Engine,
id: number,
): Promise<SliceDetails | undefined> {
return getSlice(engine, asSliceSqlId(id));
}
// Interface for additional sections that can be composed
// with ThreadSliceDetailsPanel
export interface TrackEventDetailsPanelSection {
load(selection: TrackEventSelection): Promise<void>;
render(): m.Children;
}
export interface ThreadSliceDetailsPanelAttrs {
// Optional additional sections to render in the left column
leftSections?: TrackEventDetailsPanelSection[];
// Optional additional sections to render in the right column
rightSections?: TrackEventDetailsPanelSection[];
}
export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel {
private sliceDetails?: SliceDetails;
private breakdownByThreadState?: BreakdownByThreadState;
private distributionLoaded = false;
private distributionScope: DistributionScope = 'track';
private cachedWholeTraceDataset?: {dataset: Dataset | undefined};
private readonly trace: TraceImpl;
private readonly attrs: ThreadSliceDetailsPanelAttrs;
constructor(trace: Trace, attrs?: ThreadSliceDetailsPanelAttrs) {
// Rationale for the assertIsInstance: ThreadSliceDetailsPanel requires a
// TraceImpl (because of flows) but here we must take a Trace interface,
// because this track is exposed to plugins (which see only Trace).
this.trace = ensureIsInstance(trace, TraceImpl);
this.attrs = attrs ?? {};
}
async load(selection: TrackEventSelection) {
const {trace} = this;
const {eventId} = selection;
const details = await getSliceDetails(trace.engine, eventId);
if (
details !== undefined &&
details.thread !== undefined &&
details.dur > 0
) {
this.breakdownByThreadState = await breakDownIntervalByThreadState(
trace.engine,
TimeSpan.fromTimeAndDuration(details.ts, details.dur),
details.thread.utid,
);
}
this.sliceDetails = details;
// Load additional sections
const sectionsToLoad = [
...(this.attrs.leftSections ?? []),
...(this.attrs.rightSections ?? []),
];
if (sectionsToLoad.length > 0) {
await Promise.all(
sectionsToLoad.map((section) => section.load(selection)),
);
}
}
render() {
if (!exists(this.sliceDetails)) {
return m(DetailsShell, {title: 'Slice', description: 'Loading...'});
}
const slice = this.sliceDetails;
// Render additional left and right sections
const additionalLeft = this.attrs.leftSections?.map((section) =>
section.render(),
);
const additionalRight = this.attrs.rightSections?.map((section) =>
section.render(),
);
return m(
DetailsShell,
{
title: 'Slice',
description: slice.name,
buttons: this.renderContextButton(slice),
},
m(
GridLayout,
m(
GridLayoutColumn,
renderDetails(this.trace, slice, this.breakdownByThreadState),
additionalLeft,
),
this.renderRhs(this.trace, slice, additionalRight),
),
);
}
private renderRhs(
trace: Trace,
slice: SliceDetails,
additionalSections?: m.Children,
): m.Children {
const distribution = this.renderDistribution(slice);
const precFlows = this.renderPrecedingFlows(slice);
const followingFlows = this.renderFollowingFlows(slice);
const args =
hasArgs(slice.args) &&
m(
Section,
{title: 'Arguments'},
m(Tree, renderSliceArguments(trace, slice.args)),
);
if (
distribution !== undefined ||
precFlows !== undefined ||
followingFlows !== undefined ||
args !== undefined ||
additionalSections !== undefined
) {
return m(
GridLayoutColumn,
precFlows,
followingFlows,
args,
additionalSections,
distribution,
);
} else {
return undefined;
}
}
private renderPrecedingFlows(slice: SliceDetails): m.Children {
const flows = this.trace.flows.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(Grid, {
columns: [
{key: 'sliceName', header: m(GridHeaderCell, 'Slice')},
{key: 'delay', header: m(GridHeaderCell, 'Delay')},
{key: 'thread', header: m(GridHeaderCell, 'Thread')},
],
rowData: inFlows.map((flow) => [
m(
GridCell,
m(TrackEventRef, {
trace: this.trace,
table: 'slice',
id: flow.begin.sliceId,
name: flow.begin.sliceChromeCustomName ?? flow.begin.sliceName,
}),
),
m(
GridCell,
m(DurationWidget, {
trace: this.trace,
dur: flow.end.sliceStartTs - flow.begin.sliceEndTs,
}),
),
m(GridCell, this.getThreadNameForFlow(flow.begin, !isRunTask)),
]),
}),
);
} else {
return null;
}
}
private renderFollowingFlows(slice: SliceDetails): m.Children {
const flows = this.trace.flows.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(Grid, {
columns: [
{key: 'slice', header: m(GridHeaderCell, 'Slice')},
{key: 'delay', header: m(GridHeaderCell, 'Delay')},
{key: 'thread', header: m(GridHeaderCell, 'Thread')},
],
rowData: outFlows.map((flow) => [
m(
GridCell,
m(TrackEventRef, {
trace: this.trace,
table: 'slice',
id: flow.end.sliceId,
name: flow.end.sliceChromeCustomName ?? flow.end.sliceName,
}),
),
m(
GridCell,
m(DurationWidget, {
trace: this.trace,
dur: flow.end.sliceStartTs - flow.begin.sliceEndTs,
}),
),
m(GridCell, this.getThreadNameForFlow(flow.end, !isPostTask)),
]),
}),
);
} else {
return null;
}
}
private getThreadNameForFlow(
flow: FlowPoint,
includeProcessName: boolean,
): string {
return includeProcessName
? `${flow.threadName} (${flow.processName})`
: flow.threadName;
}
private renderDistribution(slice: SliceDetails): m.Children {
const sliceName = slice.name;
if (sliceName === undefined) return undefined;
const trackDataset = findSliceTrackDataset(this.trace, slice.trackId);
if (trackDataset === undefined) return undefined;
const body = this.distributionLoaded
? this.renderLoadedBody(sliceName, trackDataset, Number(slice.dur))
: this.renderGatedPlaceholder();
return m(
Section,
{title: titleWithHelp(`Histogram for '${sliceName}'`, HISTOGRAM_HELP)},
this.renderScopePicker(),
body,
);
}
private renderGatedPlaceholder(): m.Children {
return m(
'.pf-distribution-summary-gate',
renderDistributionPlaceholder(),
m(
'.pf-distribution-summary-gate__overlay',
m(Button, {
label: 'Load distribution',
variant: ButtonVariant.Filled,
onclick: () => {
this.distributionLoaded = true;
},
}),
),
);
}
private renderLoadedBody(
sliceName: string,
trackDataset: Dataset,
sliceDur: number,
): m.Children {
const dataset = this.activeDataset(trackDataset);
if (dataset === undefined) return renderDistributionPlaceholder();
return [
m(DistributionSummary, {
trace: this.trace,
dataset,
filter: {col: 'name', eq: sliceName},
valueColumn: 'dur',
highlightValue: sliceDur,
}),
m(
'.pf-thread-slice-distribution-link',
m(Button, {
label: 'View matching slices',
icon: Icons.ExternalLink,
onclick: () =>
openDistributionTab(
this.trace,
sliceDistributionConfig(
this.trace,
sliceName,
dataset,
this.distributionScope,
),
),
}),
),
];
}
private activeDataset(trackDataset: Dataset): Dataset | undefined {
return this.distributionScope === 'track'
? trackDataset
: this.getWholeTraceDataset();
}
private getWholeTraceDataset(): Dataset | undefined {
if (this.cachedWholeTraceDataset === undefined) {
this.cachedWholeTraceDataset = {
dataset: findWholeTraceSliceDataset(this.trace),
};
}
return this.cachedWholeTraceDataset.dataset;
}
private renderScopePicker(): m.Children {
const scope = this.distributionScope;
const label = scope === 'track' ? 'This track' : 'Across whole trace';
return m(
'.pf-thread-slice-distribution-scope',
'Scope: ',
helpIcon(SCOPE_HELP),
m(
PopupMenu,
{
trigger: m(Button, {
label,
rightIcon: Icons.ContextMenu,
compact: true,
}),
},
m(MenuItem, {
label: 'This track',
active: scope === 'track',
onclick: () => {
this.distributionScope = 'track';
},
}),
m(MenuItem, {
label: 'Across whole trace',
active: scope === 'all',
onclick: () => {
this.distributionScope = 'all';
},
}),
),
);
}
private renderContextButton(sliceInfo: SliceDetails): m.Children {
const contextMenuItems = getSliceContextMenuItems(sliceInfo);
if (contextMenuItems.length > 0) {
const trigger = m(Button, {
label: 'Contextual Options',
rightIcon: Icons.ContextMenu,
});
return m(
PopupMenu,
{trigger},
contextMenuItems.map(({name, run}) =>
m(MenuItem, {label: name, onclick: () => run(sliceInfo, this.trace)}),
),
);
} else {
return undefined;
}
}
}