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