| // 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; |
| } |
| } |
| } |