• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2019 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use size file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import m from 'mithril';
16import {Icons} from '../../base/semantic_icons';
17import {TimeSpan} from '../../base/time';
18import {exists} from '../../base/utils';
19import {Engine} from '../../trace_processor/engine';
20import {Button} from '../../widgets/button';
21import {DetailsShell} from '../../widgets/details_shell';
22import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
23import {MenuItem, PopupMenu} from '../../widgets/menu';
24import {Section} from '../../widgets/section';
25import {Tree} from '../../widgets/tree';
26import {Flow, FlowPoint} from '../../core/flow_types';
27import {hasArgs, renderArguments} from './slice_args';
28import {renderDetails} from './slice_details';
29import {getSlice, SliceDetails} from '../sql_utils/slice';
30import {
31  BreakdownByThreadState,
32  breakDownIntervalByThreadState,
33} from './thread_state';
34import {asSliceSqlId} from '../sql_utils/core_types';
35import {DurationWidget} from '../widgets/duration';
36import {SliceRef} from '../widgets/slice';
37import {BasicTable} from '../../widgets/basic_table';
38import {getSqlTableDescription} from '../widgets/sql/table/sql_table_registry';
39import {assertExists, assertIsInstance} from '../../base/logging';
40import {Trace} from '../../public/trace';
41import {TrackEventDetailsPanel} from '../../public/details_panel';
42import {TrackEventSelection} from '../../public/selection';
43import {extensions} from '../extensions';
44import {TraceImpl} from '../../core/trace_impl';
45
46interface ContextMenuItem {
47  name: string;
48  shouldDisplay(slice: SliceDetails): boolean;
49  run(slice: SliceDetails, trace: Trace): void;
50}
51
52function getTidFromSlice(slice: SliceDetails): number | undefined {
53  return slice.thread?.tid;
54}
55
56function getPidFromSlice(slice: SliceDetails): number | undefined {
57  return slice.process?.pid;
58}
59
60function getProcessNameFromSlice(slice: SliceDetails): string | undefined {
61  return slice.process?.name;
62}
63
64function getThreadNameFromSlice(slice: SliceDetails): string | undefined {
65  return slice.thread?.name;
66}
67
68function hasName(slice: SliceDetails): boolean {
69  return slice.name !== undefined;
70}
71
72function hasTid(slice: SliceDetails): boolean {
73  return getTidFromSlice(slice) !== undefined;
74}
75
76function hasPid(slice: SliceDetails): boolean {
77  return getPidFromSlice(slice) !== undefined;
78}
79
80function hasProcessName(slice: SliceDetails): boolean {
81  return getProcessNameFromSlice(slice) !== undefined;
82}
83
84function hasThreadName(slice: SliceDetails): boolean {
85  return getThreadNameFromSlice(slice) !== undefined;
86}
87
88const ITEMS: ContextMenuItem[] = [
89  {
90    name: 'Ancestor slices',
91    shouldDisplay: (slice: SliceDetails) => slice.parentId !== undefined,
92    run: (slice: SliceDetails, trace: Trace) =>
93      extensions.addLegacySqlTableTab(trace, {
94        table: assertExists(getSqlTableDescription('slice')),
95        filters: [
96          {
97            op: (cols) =>
98              `${cols[0]} IN (SELECT id FROM _slice_ancestor_and_self(${slice.id}))`,
99            columns: ['id'],
100          },
101        ],
102        imports: ['slices.hierarchy'],
103      }),
104  },
105  {
106    name: 'Descendant slices',
107    shouldDisplay: () => true,
108    run: (slice: SliceDetails, trace: Trace) =>
109      extensions.addLegacySqlTableTab(trace, {
110        table: assertExists(getSqlTableDescription('slice')),
111        filters: [
112          {
113            op: (cols) =>
114              `${cols[0]} IN (SELECT id FROM _slice_descendant_and_self(${slice.id}))`,
115            columns: ['id'],
116          },
117        ],
118        imports: ['slices.hierarchy'],
119      }),
120  },
121  {
122    name: 'Average duration of slice name',
123    shouldDisplay: (slice: SliceDetails) => hasName(slice),
124    run: (slice: SliceDetails, trace: Trace) =>
125      extensions.addQueryResultsTab(trace, {
126        query: `SELECT AVG(dur) / 1e9 FROM slice WHERE name = '${slice.name!}'`,
127        title: `${slice.name} average dur`,
128      }),
129  },
130  {
131    name: 'Binder txn names + monitor contention on thread',
132    shouldDisplay: (slice) =>
133      hasProcessName(slice) &&
134      hasThreadName(slice) &&
135      hasTid(slice) &&
136      hasPid(slice),
137    run: (slice: SliceDetails, trace: Trace) => {
138      trace.engine
139        .query(
140          `INCLUDE PERFETTO MODULE android.binder;
141           INCLUDE PERFETTO MODULE android.monitor_contention;`,
142        )
143        .then(() =>
144          extensions.addDebugSliceTrack({
145            trace,
146            data: {
147              sqlSource: `
148                                WITH merged AS (
149                                  SELECT s.ts, s.dur, tx.aidl_name AS name, 0 AS depth
150                                  FROM android_binder_txns tx
151                                  JOIN slice s
152                                    ON tx.binder_txn_id = s.id
153                                  JOIN thread_track
154                                    ON s.track_id = thread_track.id
155                                  JOIN thread
156                                    USING (utid)
157                                  JOIN process
158                                    USING (upid)
159                                  WHERE pid = ${getPidFromSlice(slice)}
160                                        AND tid = ${getTidFromSlice(slice)}
161                                        AND aidl_name IS NOT NULL
162                                  UNION ALL
163                                  SELECT
164                                    s.ts,
165                                    s.dur,
166                                    short_blocked_method || ' -> ' || blocking_thread_name || ':' || short_blocking_method AS name,
167                                    1 AS depth
168                                  FROM android_binder_txns tx
169                                  JOIN android_monitor_contention m
170                                    ON m.binder_reply_tid = tx.server_tid AND m.binder_reply_ts = tx.server_ts
171                                  JOIN slice s
172                                    ON tx.binder_txn_id = s.id
173                                  JOIN thread_track
174                                    ON s.track_id = thread_track.id
175                                  JOIN thread ON thread.utid = thread_track.utid
176                                  JOIN process ON process.upid = thread.upid
177                                  WHERE process.pid = ${getPidFromSlice(slice)}
178                                        AND thread.tid = ${getTidFromSlice(
179                                          slice,
180                                        )}
181                                        AND short_blocked_method IS NOT NULL
182                                  ORDER BY depth
183                                ) SELECT ts, dur, name FROM merged`,
184            },
185            title: `Binder names (${getProcessNameFromSlice(
186              slice,
187            )}:${getThreadNameFromSlice(slice)})`,
188          }),
189        );
190    },
191  },
192];
193
194function getSliceContextMenuItems(slice: SliceDetails) {
195  return ITEMS.filter((item) => item.shouldDisplay(slice));
196}
197
198async function getSliceDetails(
199  engine: Engine,
200  id: number,
201): Promise<SliceDetails | undefined> {
202  return getSlice(engine, asSliceSqlId(id));
203}
204
205export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel {
206  private sliceDetails?: SliceDetails;
207  private breakdownByThreadState?: BreakdownByThreadState;
208  private readonly trace: TraceImpl;
209
210  constructor(trace: Trace) {
211    // Rationale for the assertIsInstance: ThreadSliceDetailsPanel requires a
212    // TraceImpl (because of flows) but here we must take a Trace interface,
213    // because this track is exposed to plugins (which see only Trace).
214    this.trace = assertIsInstance(trace, TraceImpl);
215  }
216
217  async load({eventId}: TrackEventSelection) {
218    const {trace} = this;
219    const details = await getSliceDetails(trace.engine, eventId);
220
221    if (
222      details !== undefined &&
223      details.thread !== undefined &&
224      details.dur > 0
225    ) {
226      this.breakdownByThreadState = await breakDownIntervalByThreadState(
227        trace.engine,
228        TimeSpan.fromTimeAndDuration(details.ts, details.dur),
229        details.thread.utid,
230      );
231    }
232
233    this.sliceDetails = details;
234  }
235
236  render() {
237    if (!exists(this.sliceDetails)) {
238      return m(DetailsShell, {title: 'Slice', description: 'Loading...'});
239    }
240    const slice = this.sliceDetails;
241    return m(
242      DetailsShell,
243      {
244        title: 'Slice',
245        description: slice.name,
246        buttons: this.renderContextButton(slice),
247      },
248      m(
249        GridLayout,
250        renderDetails(this.trace, slice, this.breakdownByThreadState),
251        this.renderRhs(this.trace, slice),
252      ),
253    );
254  }
255
256  private renderRhs(trace: Trace, slice: SliceDetails): m.Children {
257    const precFlows = this.renderPrecedingFlows(slice);
258    const followingFlows = this.renderFollowingFlows(slice);
259    const args =
260      hasArgs(slice.args) &&
261      m(
262        Section,
263        {title: 'Arguments'},
264        m(Tree, renderArguments(trace, slice.args)),
265      );
266    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
267    if (precFlows ?? followingFlows ?? args) {
268      return m(GridLayoutColumn, precFlows, followingFlows, args);
269    } else {
270      return undefined;
271    }
272  }
273
274  private renderPrecedingFlows(slice: SliceDetails): m.Children {
275    const flows = this.trace.flows.connectedFlows;
276    const inFlows = flows.filter(({end}) => end.sliceId === slice.id);
277
278    if (inFlows.length > 0) {
279      const isRunTask =
280        slice.name === 'ThreadControllerImpl::RunTask' ||
281        slice.name === 'ThreadPool_RunTask';
282
283      return m(
284        Section,
285        {title: 'Preceding Flows'},
286        m(BasicTable<Flow>, {
287          columns: [
288            {
289              title: 'Slice',
290              render: (flow: Flow) =>
291                m(SliceRef, {
292                  id: asSliceSqlId(flow.begin.sliceId),
293                  name:
294                    flow.begin.sliceChromeCustomName ?? flow.begin.sliceName,
295                }),
296            },
297            {
298              title: 'Delay',
299              render: (flow: Flow) =>
300                m(DurationWidget, {
301                  dur: flow.end.sliceStartTs - flow.begin.sliceEndTs,
302                }),
303            },
304            {
305              title: 'Thread',
306              render: (flow: Flow) =>
307                this.getThreadNameForFlow(flow.begin, !isRunTask),
308            },
309          ],
310          data: inFlows,
311        }),
312      );
313    } else {
314      return null;
315    }
316  }
317
318  private renderFollowingFlows(slice: SliceDetails): m.Children {
319    const flows = this.trace.flows.connectedFlows;
320    const outFlows = flows.filter(({begin}) => begin.sliceId === slice.id);
321
322    if (outFlows.length > 0) {
323      const isPostTask =
324        slice.name === 'ThreadPool_PostTask' ||
325        slice.name === 'SequenceManager PostTask';
326
327      return m(
328        Section,
329        {title: 'Following Flows'},
330        m(BasicTable<Flow>, {
331          columns: [
332            {
333              title: 'Slice',
334              render: (flow: Flow) =>
335                m(SliceRef, {
336                  id: asSliceSqlId(flow.end.sliceId),
337                  name: flow.end.sliceChromeCustomName ?? flow.end.sliceName,
338                }),
339            },
340            {
341              title: 'Delay',
342              render: (flow: Flow) =>
343                m(DurationWidget, {
344                  dur: flow.end.sliceStartTs - flow.begin.sliceEndTs,
345                }),
346            },
347            {
348              title: 'Thread',
349              render: (flow: Flow) =>
350                this.getThreadNameForFlow(flow.end, !isPostTask),
351            },
352          ],
353          data: outFlows,
354        }),
355      );
356    } else {
357      return null;
358    }
359  }
360
361  private getThreadNameForFlow(
362    flow: FlowPoint,
363    includeProcessName: boolean,
364  ): string {
365    return includeProcessName
366      ? `${flow.threadName} (${flow.processName})`
367      : flow.threadName;
368  }
369
370  private renderContextButton(sliceInfo: SliceDetails): m.Children {
371    const contextMenuItems = getSliceContextMenuItems(sliceInfo);
372    if (contextMenuItems.length > 0) {
373      const trigger = m(Button, {
374        compact: true,
375        label: 'Contextual Options',
376        rightIcon: Icons.ContextMenu,
377      });
378      return m(
379        PopupMenu,
380        {trigger},
381        contextMenuItems.map(({name, run}) =>
382          m(MenuItem, {label: name, onclick: () => run(sliceInfo, this.trace)}),
383        ),
384      );
385    } else {
386      return undefined;
387    }
388  }
389}
390