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 {uuidv4, uuidv4Sql} from '../../base/uuid'; 16import {Actions, DeferredAction} from '../../common/actions'; 17import {PrimaryTrackSortKey, SCROLLING_TRACK_GROUP} from '../../common/state'; 18import {globals} from '../globals'; 19import {TrackDescriptor} from '../../public'; 20import {DebugSliceTrack} from './slice_track'; 21import { 22 createPerfettoTable, 23 matchesSqlValue, 24} from '../../trace_processor/sql_utils'; 25import {Engine} from '../../trace_processor/engine'; 26import {DebugCounterTrack} from './counter_track'; 27import {ARG_PREFIX} from './details_tab'; 28 29// We need to add debug tracks from the core and from plugins. In order to add a 30// debug track we need to pass a context through with we can add the track. This 31// is different for plugins vs the core. This interface defines the generic 32// shape of this context, which can be supplied from a plugin or built from 33// globals. 34// 35// TODO(stevegolton): In the future, both the core and plugins should 36// have access to some Context object which implements the various things we 37// want to do in a generic way, so that we don't have to do this mangling to get 38// this to work. 39interface Context { 40 engine: Engine; 41 registerTrack(track: TrackDescriptor): unknown; 42} 43 44// Names of the columns of the underlying view to be used as 45// ts / dur / name / pivot. 46export interface SliceColumns { 47 ts: string; 48 dur: string; 49 name: string; 50 pivot?: string; 51} 52 53let debugTrackCount = 0; 54 55export interface SqlDataSource { 56 // SQL source selecting the necessary data. 57 sqlSource: string; 58 59 // Optional: Rename columns from the query result. 60 // If omitted, original column names from the query are used instead. 61 // The caller is responsible for ensuring that the number of items in this 62 // list matches the number of columns returned by sqlSource. 63 columns?: string[]; 64} 65 66// Creates actions to add a debug track. The actions must be dispatched to 67// have an effect. Use this variant if you want to create many tracks at 68// once or want to tweak the actions once produced. Otherwise, use 69// addDebugSliceTrack(). 70function createAddDebugTrackActions( 71 trackName: string, 72 uri: string, 73): DeferredAction<{}>[] { 74 const debugTrackId = ++debugTrackCount; 75 const trackKey = uuidv4(); 76 77 const actions: DeferredAction<{}>[] = [ 78 Actions.addTrack({ 79 key: trackKey, 80 name: trackName.trim() || `Debug Track ${debugTrackId}`, 81 uri, 82 trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK, 83 trackGroup: SCROLLING_TRACK_GROUP, 84 closeable: true, 85 }), 86 Actions.toggleTrackPinned({trackKey}), 87 ]; 88 89 return actions; 90} 91 92export async function addPivotDebugSliceTracks( 93 ctx: Context, 94 data: SqlDataSource, 95 trackName: string, 96 sliceColumns: SliceColumns, 97 argColumns: string[], 98) { 99 if (sliceColumns.pivot) { 100 // Get distinct values to group by 101 const pivotValues = await ctx.engine.query(` 102 with all_vals as (${data.sqlSource}) 103 select DISTINCT ${sliceColumns.pivot} from all_vals;`); 104 105 const iter = pivotValues.iter({}); 106 107 for (; iter.valid(); iter.next()) { 108 const pivotDataSource: SqlDataSource = { 109 sqlSource: `select * from 110 (${data.sqlSource}) 111 where ${sliceColumns.pivot} ${matchesSqlValue( 112 iter.get(sliceColumns.pivot), 113 )}`, 114 }; 115 116 await addDebugSliceTrack( 117 ctx, 118 pivotDataSource, 119 `${trackName.trim() || 'Pivot Track'}: ${iter.get(sliceColumns.pivot)}`, 120 sliceColumns, 121 argColumns, 122 ); 123 } 124 } 125} 126 127// Adds a debug track immediately. Use createDebugSliceTrackActions() if you 128// want to create many tracks at once. 129export async function addDebugSliceTrack( 130 ctx: Context, 131 data: SqlDataSource, 132 trackName: string, 133 sliceColumns: SliceColumns, 134 argColumns: string[], 135): Promise<void> { 136 // Create a new table from the debug track definition. This will be used as 137 // the backing data source for our track and its details panel. 138 const tableName = `__debug_slice_${uuidv4Sql()}`; 139 140 // TODO(stevegolton): Right now we ignore the AsyncDisposable that this 141 // function returns, and so never clean up this table. The problem is we have 142 // no where sensible to do this cleanup. 143 // - If we did it in the track's onDestroy function, we could drop the table 144 // while the details panel still needs access to it. 145 // - If we did it in the plugin's onTraceUnload function, we could risk 146 // dropping it n the middle of a track update cycle as track lifecycles are 147 // not synchronized with plugin lifecycles. 148 await createPerfettoTable( 149 ctx.engine, 150 tableName, 151 createDebugSliceTrackTableExpr(data, sliceColumns, argColumns), 152 ); 153 154 const uri = `debug.slice.${uuidv4()}`; 155 ctx.registerTrack({ 156 uri, 157 trackFactory: (trackCtx) => { 158 return new DebugSliceTrack(ctx.engine, trackCtx, tableName); 159 }, 160 }); 161 162 // Create the actions to add this track to the tracklist 163 const actions = await createAddDebugTrackActions(trackName, uri); 164 globals.dispatchMultiple(actions); 165} 166 167function createDebugSliceTrackTableExpr( 168 data: SqlDataSource, 169 sliceColumns: SliceColumns, 170 argColumns: string[], 171): string { 172 const dataColumns = 173 data.columns !== undefined ? `(${data.columns.join(', ')})` : ''; 174 const dur = sliceColumns.dur === '0' ? 0 : sliceColumns.dur; 175 return ` 176 with data${dataColumns} as ( 177 ${data.sqlSource} 178 ), 179 prepared_data as ( 180 select 181 ${sliceColumns.ts} as ts, 182 ifnull(cast(${dur} as int), -1) as dur, 183 printf('%s', ${sliceColumns.name}) as name 184 ${argColumns.length > 0 ? ',' : ''} 185 ${argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`).join(',\n')} 186 from data 187 ) 188 select 189 row_number() over (order by ts) as id, 190 * 191 from prepared_data 192 order by ts 193 `; 194} 195 196// Names of the columns of the underlying view to be used as ts / dur / name. 197export interface CounterColumns { 198 ts: string; 199 value: string; 200} 201 202export interface CounterDebugTrackConfig { 203 data: SqlDataSource; 204 columns: CounterColumns; 205} 206 207export interface CounterDebugTrackCreateConfig { 208 pinned?: boolean; // default true 209 closeable?: boolean; // default true 210} 211 212// Adds a debug track immediately. Use createDebugCounterTrackActions() if you 213// want to create many tracks at once. 214export async function addDebugCounterTrack( 215 ctx: Context, 216 data: SqlDataSource, 217 trackName: string, 218 columns: CounterColumns, 219): Promise<void> { 220 // Create a new table from the debug track definition. This will be used as 221 // the backing data source for our track and its details panel. 222 const tableName = `__debug_counter_${uuidv4Sql()}`; 223 224 // TODO(stevegolton): Right now we ignore the AsyncDisposable that this 225 // function returns, and so never clean up this table. The problem is we have 226 // no where sensible to do this cleanup. 227 // - If we did it in the track's onDestroy function, we could drop the table 228 // while the details panel still needs access to it. 229 // - If we did it in the plugin's onTraceUnload function, we could risk 230 // dropping it n the middle of a track update cycle as track lifecycles are 231 // not synchronized with plugin lifecycles. 232 await createPerfettoTable( 233 ctx.engine, 234 tableName, 235 createDebugCounterTrackTableExpr(data, columns), 236 ); 237 238 const uri = `debug.counter.${uuidv4()}`; 239 ctx.registerTrack({ 240 uri, 241 trackFactory: (trackCtx) => { 242 return new DebugCounterTrack(ctx.engine, trackCtx, tableName); 243 }, 244 }); 245 246 // Create the actions to add this track to the tracklist 247 const actions = await createAddDebugTrackActions(trackName, uri); 248 globals.dispatchMultiple(actions); 249} 250 251function createDebugCounterTrackTableExpr( 252 data: SqlDataSource, 253 columns: CounterColumns, 254): string { 255 return ` 256 with data as ( 257 ${data.sqlSource} 258 ) 259 select 260 ${columns.ts} as ts, 261 ${columns.value} as value 262 from data 263 order by ts 264 `; 265} 266