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 {Trace} from '../../public/trace'; 16import {TrackNode} from '../../public/workspace'; 17import {SourceDataset} from '../../trace_processor/dataset'; 18import {Engine} from '../../trace_processor/engine'; 19import {LONG, NUM, STR} from '../../trace_processor/query_result'; 20import { 21 createPerfettoTable, 22 sqlValueToReadableString, 23 sqlValueToSqliteString, 24} from '../../trace_processor/sql_utils'; 25import {DatasetSliceTrack} from './dataset_slice_track'; 26import { 27 ARG_PREFIX, 28 DebugSliceTrackDetailsPanel, 29} from './debug_slice_track_details_panel'; 30import { 31 CounterColumnMapping, 32 SqlTableCounterTrack, 33} from './query_counter_track'; 34import {SliceColumnMapping, SqlDataSource} from './query_slice_track'; 35 36let trackCounter = 0; // For reproducible ids. 37 38function getUniqueTrackCounter() { 39 return trackCounter++; 40} 41 42export interface DebugSliceTrackArgs { 43 readonly trace: Trace; 44 readonly data: SqlDataSource; 45 readonly title?: string; 46 readonly columns?: Partial<SliceColumnMapping>; 47 readonly argColumns?: string[]; 48 readonly pivotOn?: string; 49} 50 51/** 52 * Adds a new debug slice track to the workspace. 53 * 54 * A debug slice track is a track based on a query which is: 55 * - Based on a query. 56 * - Uses automatic slice layout. 57 * - Automatically added to the top of the current workspace. 58 * - Pinned. 59 * - Has a close button to remove it. 60 * 61 * @param args - Args to pass to the trace. 62 * @param args.trace - The trace to use. 63 * @param args.data.sqlSource - The query to run. 64 * @param args.data.columns - Optional: Override columns. 65 * @param args.title - Optional: Title for the track. If pivotOn is supplied, 66 * this will be used as the root title for each track, but each title will have 67 * the value appended. 68 * @param args.columns - Optional: The columns names to use for the various 69 * essential column names. 70 * @param args.argColumns - Optional: A list of columns which are passed to the 71 * details panel. 72 * @param args.pivotOn - Optional: The name of a column on which to pivot. If 73 * provided, we will create N tracks, one for each distinct value of the pivotOn 74 * column. Each track will only show the slices which have the corresponding 75 * value in their pivotOn column. 76 */ 77export async function addDebugSliceTrack(args: DebugSliceTrackArgs) { 78 const tableId = getUniqueTrackCounter(); 79 const tableName = `__debug_track_${tableId}`; 80 const titleBase = args.title?.trim() || `Debug Slice Track ${tableId}`; 81 const uriBase = `debug.track${tableId}`; 82 83 // Create a table for this query before doing anything 84 await createTableForSliceTrack( 85 args.trace.engine, 86 tableName, 87 args.data, 88 args.columns, 89 args.argColumns, 90 args.pivotOn, 91 ); 92 93 if (args.pivotOn) { 94 await addPivotedSliceTracks( 95 args.trace, 96 tableName, 97 titleBase, 98 uriBase, 99 args.pivotOn, 100 ); 101 } else { 102 addSingleSliceTrack(args.trace, tableName, titleBase, uriBase); 103 } 104} 105 106async function createTableForSliceTrack( 107 engine: Engine, 108 tableName: string, 109 data: SqlDataSource, 110 columns: Partial<SliceColumnMapping> = {}, 111 argColumns?: string[], 112 pivotCol?: string, 113) { 114 const {ts = 'ts', dur = 'dur', name = 'name'} = columns; 115 116 // If the view has clashing names (e.g. "name" coming from joining two 117 // different tables, we will see names like "name_1", "name_2", but they 118 // won't be addressable from the SQL. So we explicitly name them through a 119 // list of columns passed to CTE. 120 const dataColumns = 121 data.columns !== undefined ? `(${data.columns.join(', ')})` : ''; 122 123 const cols = [ 124 `${ts} as ts`, 125 `ifnull(cast(${dur} as int), -1) as dur`, 126 `printf('%s', ${name}) as name`, 127 argColumns && argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`), 128 pivotCol && `${pivotCol} as pivot`, 129 ] 130 .flat() // Convert to flattened list 131 .filter(Boolean) // Remove falsy values 132 .join(','); 133 134 const query = ` 135 with data${dataColumns} as ( 136 ${data.sqlSource} 137 ), 138 prepared_data as ( 139 select ${cols} 140 from data 141 ) 142 select 143 row_number() over (order by ts) as id, 144 * 145 from prepared_data 146 order by ts 147 `; 148 149 return await createPerfettoTable(engine, tableName, query); 150} 151 152async function addPivotedSliceTracks( 153 trace: Trace, 154 tableName: string, 155 titleBase: string, 156 uriBase: string, 157 pivotColName: string, 158) { 159 const result = await trace.engine.query(` 160 SELECT DISTINCT pivot 161 FROM ${tableName} 162 ORDER BY pivot 163 `); 164 165 let trackCount = 0; 166 for (const iter = result.iter({}); iter.valid(); iter.next()) { 167 const uri = `${uriBase}_${trackCount++}`; 168 const pivotValue = iter.get('pivot'); 169 const title = `${titleBase}: ${pivotColName} = ${sqlValueToReadableString(pivotValue)}`; 170 171 trace.tracks.registerTrack({ 172 uri, 173 title, 174 track: new DatasetSliceTrack({ 175 trace, 176 uri, 177 dataset: new SourceDataset({ 178 schema: { 179 id: NUM, 180 ts: LONG, 181 dur: LONG, 182 name: STR, 183 }, 184 src: tableName, 185 filter: { 186 col: 'pivot', 187 eq: pivotValue, 188 }, 189 }), 190 detailsPanel: (row) => { 191 return new DebugSliceTrackDetailsPanel(trace, tableName, row.id); 192 }, 193 }), 194 }); 195 196 const trackNode = new TrackNode({uri, title, removable: true}); 197 trace.workspace.pinnedTracksNode.addChildLast(trackNode); 198 } 199} 200 201function addSingleSliceTrack( 202 trace: Trace, 203 tableName: string, 204 title: string, 205 uri: string, 206) { 207 trace.tracks.registerTrack({ 208 uri, 209 title, 210 track: new DatasetSliceTrack({ 211 trace, 212 uri, 213 dataset: new SourceDataset({ 214 schema: { 215 id: NUM, 216 ts: LONG, 217 dur: LONG, 218 name: STR, 219 }, 220 src: tableName, 221 }), 222 detailsPanel: (row) => { 223 return new DebugSliceTrackDetailsPanel(trace, tableName, row.id); 224 }, 225 }), 226 }); 227 228 const trackNode = new TrackNode({uri, title, removable: true}); 229 trace.workspace.pinnedTracksNode.addChildLast(trackNode); 230} 231 232export interface DebugCounterTrackArgs { 233 readonly trace: Trace; 234 readonly data: SqlDataSource; 235 readonly title?: string; 236 readonly columns?: Partial<CounterColumnMapping>; 237 readonly pivotOn?: string; 238} 239 240/** 241 * Adds a new debug counter track to the workspace. 242 * 243 * A debug slice track is a track based on a query which is: 244 * - Based on a query. 245 * - Automatically added to the top of the current workspace. 246 * - Pinned. 247 * - Has a close button to remove it. 248 * 249 * @param args - Args to pass to the trace. 250 * @param args.trace - The trace to use. 251 * @param args.data.sqlSource - The query to run. 252 * @param args.data.columns - Optional: Override columns. 253 * @param args.title - Optional: Title for the track. If pivotOn is supplied, 254 * this will be used as the root title for each track, but each title will have 255 * the value appended. 256 * @param args.columns - Optional: The columns names to use for the various 257 * essential column names. 258 * @param args.pivotOn - Optional: The name of a column on which to pivot. If 259 * provided, we will create N tracks, one for each distinct value of the pivotOn 260 * column. Each track will only show the slices which have the corresponding 261 * value in their pivotOn column. 262 */ 263export async function addDebugCounterTrack(args: DebugCounterTrackArgs) { 264 const tableId = getUniqueTrackCounter(); 265 const tableName = `__debug_track_${tableId}`; 266 const titleBase = args.title?.trim() || `Debug Slice Track ${tableId}`; 267 const uriBase = `debug.track${tableId}`; 268 269 // Create a table for this query before doing anything 270 await createTableForCounterTrack( 271 args.trace.engine, 272 tableName, 273 args.data, 274 args.columns, 275 args.pivotOn, 276 ); 277 278 if (args.pivotOn) { 279 await addPivotedCounterTracks( 280 args.trace, 281 tableName, 282 titleBase, 283 uriBase, 284 args.pivotOn, 285 ); 286 } else { 287 addSingleCounterTrack(args.trace, tableName, titleBase, uriBase); 288 } 289} 290 291async function createTableForCounterTrack( 292 engine: Engine, 293 tableName: string, 294 data: SqlDataSource, 295 columnMapping: Partial<CounterColumnMapping> = {}, 296 pivotCol?: string, 297) { 298 const {ts = 'ts', value = 'value'} = columnMapping; 299 const cols = [ 300 `${ts} as ts`, 301 `${value} as value`, 302 pivotCol && `${pivotCol} as pivot`, 303 ] 304 .flat() // Convert to flattened list 305 .filter(Boolean) // Remove falsy values 306 .join(','); 307 308 const query = ` 309 with data as ( 310 ${data.sqlSource} 311 ) 312 select ${cols} 313 from data 314 order by ts 315 `; 316 317 return await createPerfettoTable(engine, tableName, query); 318} 319 320async function addPivotedCounterTracks( 321 trace: Trace, 322 tableName: string, 323 titleBase: string, 324 uriBase: string, 325 pivotColName: string, 326) { 327 const result = await trace.engine.query(` 328 SELECT DISTINCT pivot 329 FROM ${tableName} 330 ORDER BY pivot 331 `); 332 333 let trackCount = 0; 334 for (const iter = result.iter({}); iter.valid(); iter.next()) { 335 const uri = `${uriBase}_${trackCount++}`; 336 const pivotValue = iter.get('pivot'); 337 const title = `${titleBase}: ${pivotColName} = ${sqlValueToReadableString(pivotValue)}`; 338 339 trace.tracks.registerTrack({ 340 uri, 341 title, 342 track: new SqlTableCounterTrack( 343 trace, 344 uri, 345 ` 346 SELECT * 347 FROM ${tableName} 348 WHERE pivot = ${sqlValueToSqliteString(pivotValue)} 349 `, 350 ), 351 }); 352 353 const trackNode = new TrackNode({uri, title, removable: true}); 354 trace.workspace.pinnedTracksNode.addChildLast(trackNode); 355 } 356} 357 358function addSingleCounterTrack( 359 trace: Trace, 360 tableName: string, 361 title: string, 362 uri: string, 363) { 364 trace.tracks.registerTrack({ 365 uri, 366 title, 367 track: new SqlTableCounterTrack(trace, uri, tableName), 368 }); 369 370 const trackNode = new TrackNode({uri, title, removable: true}); 371 trace.workspace.pinnedTracksNode.addChildLast(trackNode); 372} 373