• 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';
16
17import {Icons} from '../base/semantic_icons';
18import {duration, Time, TimeSpan} from '../base/time';
19import {exists} from '../base/utils';
20import {raf} from '../core/raf_scheduler';
21import {Engine} from '../trace_processor/engine';
22import {LONG, LONG_NULL, NUM, STR_NULL} from '../trace_processor/query_result';
23import {Button} from '../widgets/button';
24import {DetailsShell} from '../widgets/details_shell';
25import {GridLayout, GridLayoutColumn} from '../widgets/grid_layout';
26import {MenuItem, PopupMenu2} from '../widgets/menu';
27import {Section} from '../widgets/section';
28import {Tree, TreeNode} from '../widgets/tree';
29
30import {BottomTab, NewBottomTabArgs} from './bottom_tab';
31import {FlowPoint, globals} from './globals';
32import {hasArgs, renderArguments} from './slice_args';
33import {renderDetails} from './slice_details';
34import {getSlice, SliceDetails, SliceRef} from './sql/slice';
35import {
36  BreakdownByThreadState,
37  breakDownIntervalByThreadState,
38} from './sql/thread_state';
39import {asSliceSqlId} from './sql_types';
40import {DurationWidget} from './widgets/duration';
41import {addDebugSliceTrack} from './debug_tracks/debug_tracks';
42import {addQueryResultsTab} from './query_result_tab';
43
44interface ContextMenuItem {
45  name: string;
46  shouldDisplay(slice: SliceDetails): boolean;
47  run(slice: SliceDetails): void;
48}
49
50function getTidFromSlice(slice: SliceDetails): number | undefined {
51  return slice.thread?.tid;
52}
53
54function getPidFromSlice(slice: SliceDetails): number | undefined {
55  return slice.process?.pid;
56}
57
58function getProcessNameFromSlice(slice: SliceDetails): string | undefined {
59  return slice.process?.name;
60}
61
62function getThreadNameFromSlice(slice: SliceDetails): string | undefined {
63  return slice.thread?.name;
64}
65
66function hasName(slice: SliceDetails): boolean {
67  return slice.name !== undefined;
68}
69
70function hasTid(slice: SliceDetails): boolean {
71  return getTidFromSlice(slice) !== undefined;
72}
73
74function hasPid(slice: SliceDetails): boolean {
75  return getPidFromSlice(slice) !== undefined;
76}
77
78function hasProcessName(slice: SliceDetails): boolean {
79  return getProcessNameFromSlice(slice) !== undefined;
80}
81
82function hasThreadName(slice: SliceDetails): boolean {
83  return getThreadNameFromSlice(slice) !== undefined;
84}
85
86const ITEMS: ContextMenuItem[] = [
87  {
88    name: 'Average duration of slice name',
89    shouldDisplay: (slice: SliceDetails) => hasName(slice),
90    run: (slice: SliceDetails) =>
91      addQueryResultsTab({
92        query: `SELECT AVG(dur) / 1e9 FROM slice WHERE name = '${slice.name!}'`,
93        title: `${slice.name} average dur`,
94      }),
95  },
96  {
97    name: 'Binder txn names + monitor contention on thread',
98    shouldDisplay: (slice) =>
99      hasProcessName(slice) &&
100      hasThreadName(slice) &&
101      hasTid(slice) &&
102      hasPid(slice),
103    run: (slice: SliceDetails) => {
104      const engine = getEngine();
105      if (engine === undefined) return;
106      engine
107        .query(
108          `
109        INCLUDE PERFETTO MODULE android.binder;
110        INCLUDE PERFETTO MODULE android.monitor_contention;
111      `,
112        )
113        .then(() =>
114          addDebugSliceTrack(
115            // NOTE(stevegolton): This is a temporary patch, this menu should
116            // become part of another plugin, at which point we can just use the
117            // plugin's context object.
118            {
119              engine,
120              registerTrack: (x) => globals.trackManager.registerTrack(x),
121            },
122            {
123              sqlSource: `
124                                WITH merged AS (
125                                  SELECT s.ts, s.dur, tx.aidl_name AS name, 0 AS depth
126                                  FROM android_binder_txns tx
127                                  JOIN slice s
128                                    ON tx.binder_txn_id = s.id
129                                  JOIN thread_track
130                                    ON s.track_id = thread_track.id
131                                  JOIN thread
132                                    USING (utid)
133                                  JOIN process
134                                    USING (upid)
135                                  WHERE pid = ${getPidFromSlice(slice)}
136                                        AND tid = ${getTidFromSlice(slice)}
137                                        AND aidl_name IS NOT NULL
138                                  UNION ALL
139                                  SELECT
140                                    s.ts,
141                                    s.dur,
142                                    short_blocked_method || ' -> ' || blocking_thread_name || ':' || short_blocking_method AS name,
143                                    1 AS depth
144                                  FROM android_binder_txns tx
145                                  JOIN android_monitor_contention m
146                                    ON m.binder_reply_tid = tx.server_tid AND m.binder_reply_ts = tx.server_ts
147                                  JOIN slice s
148                                    ON tx.binder_txn_id = s.id
149                                  JOIN thread_track
150                                    ON s.track_id = thread_track.id
151                                  JOIN thread ON thread.utid = thread_track.utid
152                                  JOIN process ON process.upid = thread.upid
153                                  WHERE process.pid = ${getPidFromSlice(slice)}
154                                        AND thread.tid = ${getTidFromSlice(
155                                          slice,
156                                        )}
157                                        AND short_blocked_method IS NOT NULL
158                                  ORDER BY depth
159                                ) SELECT ts, dur, name FROM merged`,
160            },
161            `Binder names (${getProcessNameFromSlice(
162              slice,
163            )}:${getThreadNameFromSlice(slice)})`,
164            {ts: 'ts', dur: 'dur', name: 'name'},
165            [],
166          ),
167        );
168    },
169  },
170];
171
172function getSliceContextMenuItems(slice: SliceDetails) {
173  return ITEMS.filter((item) => item.shouldDisplay(slice));
174}
175
176function getEngine(): Engine | undefined {
177  const engineId = globals.getCurrentEngine()?.id;
178  if (engineId === undefined) {
179    return undefined;
180  }
181  const engine = globals.engines.get(engineId)?.getProxy('SlicePanel');
182  return engine;
183}
184
185async function getAnnotationSlice(
186  engine: Engine,
187  id: number,
188): Promise<SliceDetails | undefined> {
189  const query = await engine.query(`
190    SELECT
191      id,
192      name,
193      ts,
194      dur,
195      track_id as trackId,
196      thread_dur as threadDur,
197      cat,
198      ABS_TIME_STR(ts) as absTime
199    FROM annotation_slice
200    where id = ${id}`);
201
202  const it = query.firstRow({
203    id: NUM,
204    name: STR_NULL,
205    ts: LONG,
206    dur: LONG,
207    trackId: NUM,
208    threadDur: LONG_NULL,
209    cat: STR_NULL,
210    absTime: STR_NULL,
211  });
212
213  return {
214    id: asSliceSqlId(it.id),
215    name: it.name ?? 'null',
216    ts: Time.fromRaw(it.ts),
217    dur: it.dur,
218    depth: 0,
219    trackId: it.trackId,
220    threadDur: it.threadDur ?? undefined,
221    category: it.cat ?? undefined,
222    absTime: it.absTime ?? undefined,
223  };
224}
225
226async function getSliceDetails(
227  engine: Engine,
228  id: number,
229  table: string,
230): Promise<SliceDetails | undefined> {
231  if (table === 'annotation_slice') {
232    return getAnnotationSlice(engine, id);
233  } else {
234    return getSlice(engine, asSliceSqlId(id));
235  }
236}
237
238interface ThreadSliceDetailsTabConfig {
239  id: number;
240  table: string;
241}
242
243export class ThreadSliceDetailsTab extends BottomTab<ThreadSliceDetailsTabConfig> {
244  private sliceDetails?: SliceDetails;
245  private breakdownByThreadState?: BreakdownByThreadState;
246
247  static create(
248    args: NewBottomTabArgs<ThreadSliceDetailsTabConfig>,
249  ): ThreadSliceDetailsTab {
250    return new ThreadSliceDetailsTab(args);
251  }
252
253  constructor(args: NewBottomTabArgs<ThreadSliceDetailsTabConfig>) {
254    super(args);
255    this.load();
256  }
257
258  async load() {
259    // Start loading the slice details
260    const {id, table} = this.config;
261    const details = await getSliceDetails(this.engine, id, table);
262
263    if (
264      details !== undefined &&
265      details.thread !== undefined &&
266      details.dur > 0
267    ) {
268      this.breakdownByThreadState = await breakDownIntervalByThreadState(
269        this.engine,
270        TimeSpan.fromTimeAndDuration(details.ts, details.dur),
271        details.thread.utid,
272      );
273    }
274
275    this.sliceDetails = details;
276    raf.scheduleFullRedraw();
277  }
278
279  getTitle(): string {
280    return `Current Selection`;
281  }
282
283  viewTab() {
284    if (!exists(this.sliceDetails)) {
285      return m(DetailsShell, {title: 'Slice', description: 'Loading...'});
286    }
287    const slice = this.sliceDetails;
288    return m(
289      DetailsShell,
290      {
291        title: 'Slice',
292        description: slice.name,
293        buttons: this.renderContextButton(slice),
294      },
295      m(
296        GridLayout,
297        renderDetails(slice, this.breakdownByThreadState),
298        this.renderRhs(this.engine, slice),
299      ),
300    );
301  }
302
303  isLoading() {
304    return !exists(this.sliceDetails);
305  }
306
307  private renderRhs(engine: Engine, slice: SliceDetails): m.Children {
308    const precFlows = this.renderPrecedingFlows(slice);
309    const followingFlows = this.renderFollowingFlows(slice);
310    const args =
311      hasArgs(slice.args) &&
312      m(
313        Section,
314        {title: 'Arguments'},
315        m(Tree, renderArguments(engine, slice.args)),
316      );
317    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
318    if (precFlows ?? followingFlows ?? args) {
319      return m(GridLayoutColumn, precFlows, followingFlows, args);
320    } else {
321      return undefined;
322    }
323  }
324
325  private renderPrecedingFlows(slice: SliceDetails): m.Children {
326    const flows = globals.connectedFlows;
327    const inFlows = flows.filter(({end}) => end.sliceId === slice.id);
328
329    if (inFlows.length > 0) {
330      const isRunTask =
331        slice.name === 'ThreadControllerImpl::RunTask' ||
332        slice.name === 'ThreadPool_RunTask';
333
334      return m(
335        Section,
336        {title: 'Preceding Flows'},
337        m(
338          Tree,
339          inFlows.map(({begin, dur}) =>
340            this.renderFlow(begin, dur, !isRunTask),
341          ),
342        ),
343      );
344    } else {
345      return null;
346    }
347  }
348
349  private renderFollowingFlows(slice: SliceDetails): m.Children {
350    const flows = globals.connectedFlows;
351    const outFlows = flows.filter(({begin}) => begin.sliceId === slice.id);
352
353    if (outFlows.length > 0) {
354      const isPostTask =
355        slice.name === 'ThreadPool_PostTask' ||
356        slice.name === 'SequenceManager PostTask';
357
358      return m(
359        Section,
360        {title: 'Following Flows'},
361        m(
362          Tree,
363          outFlows.map(({end, dur}) => this.renderFlow(end, dur, !isPostTask)),
364        ),
365      );
366    } else {
367      return null;
368    }
369  }
370
371  private renderFlow(
372    flow: FlowPoint,
373    dur: duration,
374    includeProcessName: boolean,
375  ): m.Children {
376    const description =
377      flow.sliceChromeCustomName === undefined
378        ? flow.sliceName
379        : flow.sliceChromeCustomName;
380    const threadName = includeProcessName
381      ? `${flow.threadName} (${flow.processName})`
382      : flow.threadName;
383
384    return m(
385      TreeNode,
386      {left: 'Flow'},
387      m(TreeNode, {
388        left: 'Slice',
389        right: m(SliceRef, {
390          id: asSliceSqlId(flow.sliceId),
391          name: description,
392          ts: flow.sliceStartTs,
393          dur: flow.sliceEndTs - flow.sliceStartTs,
394          sqlTrackId: flow.trackId,
395        }),
396      }),
397      m(TreeNode, {left: 'Delay', right: m(DurationWidget, {dur})}),
398      m(TreeNode, {left: 'Thread', right: threadName}),
399    );
400  }
401
402  private renderContextButton(sliceInfo: SliceDetails): m.Children {
403    const contextMenuItems = getSliceContextMenuItems(sliceInfo);
404    if (contextMenuItems.length > 0) {
405      const trigger = m(Button, {
406        compact: true,
407        label: 'Contextual Options',
408        rightIcon: Icons.ContextMenu,
409      });
410      return m(
411        PopupMenu2,
412        {trigger},
413        contextMenuItems.map(({name, run}) =>
414          m(MenuItem, {label: name, onclick: () => run(sliceInfo)}),
415        ),
416      );
417    } else {
418      return undefined;
419    }
420  }
421}
422