xref: /aosp_15_r20/external/perfetto/ui/src/core_plugins/track_utils/index.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2024 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this 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 {OmniboxMode} from '../../core/omnibox_manager';
16import {Trace} from '../../public/trace';
17import {PerfettoPlugin} from '../../public/plugin';
18import {AppImpl} from '../../core/app_impl';
19import {getTimeSpanOfSelectionOrVisibleWindow} from '../../public/utils';
20import {exists, RequiredField} from '../../base/utils';
21import {LONG, NUM, NUM_NULL} from '../../trace_processor/query_result';
22import {TrackNode} from '../../public/workspace';
23
24export default class implements PerfettoPlugin {
25  static readonly id = 'perfetto.TrackUtils';
26  async onTraceLoad(ctx: Trace): Promise<void> {
27    ctx.commands.registerCommand({
28      id: 'perfetto.RunQueryInSelectedTimeWindow',
29      name: `Run query in selected time window`,
30      callback: async () => {
31        const window = await getTimeSpanOfSelectionOrVisibleWindow(ctx);
32        const omnibox = AppImpl.instance.omnibox;
33        omnibox.setMode(OmniboxMode.Query);
34        omnibox.setText(
35          `select  where ts >= ${window.start} and ts < ${window.end}`,
36        );
37        omnibox.focus(/* cursorPlacement= */ 7);
38      },
39    });
40
41    ctx.commands.registerCommand({
42      id: 'perfetto.FindTrackByName',
43      name: 'Find track by name',
44      callback: async () => {
45        const tracksWithUris = ctx.workspace.flatTracksOrdered.filter(
46          (track) => track.uri !== undefined,
47        ) as ReadonlyArray<RequiredField<TrackNode, 'uri'>>;
48        const track = await ctx.omnibox.prompt('Choose a track...', {
49          values: tracksWithUris,
50          getName: (track) => track.title,
51        });
52        track &&
53          ctx.selection.selectTrack(track.uri, {
54            scrollToSelection: true,
55          });
56      },
57    });
58
59    ctx.commands.registerCommand({
60      id: 'perfetto.FindTrackByUri',
61      name: 'Find track by URI',
62      callback: async () => {
63        const tracksWithUris = ctx.workspace.flatTracksOrdered.filter(
64          (track) => track.uri !== undefined,
65        ) as ReadonlyArray<RequiredField<TrackNode, 'uri'>>;
66        const track = await ctx.omnibox.prompt('Choose a track...', {
67          values: tracksWithUris,
68          getName: (track) => track.uri,
69        });
70        track &&
71          ctx.selection.selectTrack(track.uri, {
72            scrollToSelection: true,
73          });
74      },
75    });
76
77    ctx.commands.registerCommand({
78      id: 'perfetto.PinTrackByName',
79      name: 'Pin track by name',
80      callback: async () => {
81        const tracksWithUris = ctx.workspace.flatTracksOrdered.filter(
82          (track) => track.uri !== undefined,
83        ) as ReadonlyArray<RequiredField<TrackNode, 'uri'>>;
84        const track = await ctx.omnibox.prompt('Choose a track...', {
85          values: tracksWithUris,
86          getName: (track) => track.title,
87        });
88        track && track.pin();
89      },
90    });
91
92    ctx.commands.registerCommand({
93      id: 'perfetto.SelectNextTrackEvent',
94      name: 'Select next track event',
95      defaultHotkey: '.',
96      callback: async () => {
97        await selectAdjacentTrackEvent(ctx, 'next');
98      },
99    });
100
101    ctx.commands.registerCommand({
102      id: 'perfetto.SelectPreviousTrackEvent',
103      name: 'Select previous track event',
104      defaultHotkey: ',',
105      callback: async () => {
106        await selectAdjacentTrackEvent(ctx, 'prev');
107      },
108    });
109  }
110}
111
112/**
113 * If a track event is currently selected, select the next or previous event on
114 * that same track chronologically ordered by `ts`.
115 */
116async function selectAdjacentTrackEvent(
117  ctx: Trace,
118  direction: 'next' | 'prev',
119) {
120  const selection = ctx.selection.selection;
121  if (selection.kind !== 'track_event') return;
122
123  const td = ctx.tracks.getTrack(selection.trackUri);
124  const dataset = td?.track.getDataset?.();
125  if (!dataset || !dataset.implements({id: NUM, ts: LONG})) return;
126
127  const windowFunc = direction === 'next' ? 'LEAD' : 'LAG';
128  const result = await ctx.engine.query(`
129      WITH
130        CTE AS (
131          SELECT
132            id,
133            ${windowFunc}(id) OVER (ORDER BY ts) AS resultId
134          FROM (${dataset.query()})
135        )
136      SELECT * FROM CTE WHERE id = ${selection.eventId}
137    `);
138  const resultId = result.maybeFirstRow({resultId: NUM_NULL})?.resultId;
139  if (!exists(resultId)) return;
140
141  ctx.selection.selectTrackEvent(selection.trackUri, resultId, {
142    scrollToSelection: true,
143  });
144}
145