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 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 {assertTrue} from '../base/logging'; 16import {Time, time} from '../base/time'; 17import {Args, ArgValue} from '../common/arg_types'; 18import { 19 SelectionKind, 20 ThreadSliceSelection, 21 getLegacySelection, 22} from '../common/state'; 23import {THREAD_SLICE_TRACK_KIND} from '../core/track_kinds'; 24import {globals, SliceDetails, ThreadStateDetails} from '../frontend/globals'; 25import { 26 publishSliceDetails, 27 publishThreadStateDetails, 28} from '../frontend/publish'; 29import {Engine} from '../trace_processor/engine'; 30import { 31 durationFromSql, 32 LONG, 33 NUM, 34 NUM_NULL, 35 STR, 36 STR_NULL, 37 timeFromSql, 38} from '../trace_processor/query_result'; 39 40import {Controller} from './controller'; 41 42export interface SelectionControllerArgs { 43 engine: Engine; 44} 45 46interface ThreadDetails { 47 tid: number; 48 threadName?: string; 49} 50 51interface ProcessDetails { 52 pid?: number; 53 processName?: string; 54 uid?: number; 55 packageName?: string; 56 versionCode?: number; 57} 58 59// This class queries the TP for the details on a specific slice that has 60// been clicked. 61export class SelectionController extends Controller<'main'> { 62 private lastSelectedId?: number | string; 63 private lastSelectedKind?: string; 64 constructor(private args: SelectionControllerArgs) { 65 super('main'); 66 } 67 68 run() { 69 const selection = getLegacySelection(globals.state); 70 if (!selection) return; 71 72 const selectWithId: SelectionKind[] = [ 73 'SLICE', 74 'SCHED_SLICE', 75 'HEAP_PROFILE', 76 'THREAD_STATE', 77 ]; 78 if ( 79 !selectWithId.includes(selection.kind) || 80 (selectWithId.includes(selection.kind) && 81 selection.id === this.lastSelectedId && 82 selection.kind === this.lastSelectedKind) 83 ) { 84 return; 85 } 86 const selectedId = selection.id; 87 const selectedKind = selection.kind; 88 this.lastSelectedId = selectedId; 89 this.lastSelectedKind = selectedKind; 90 91 if (selectedId === undefined) return; 92 93 if (selection.kind === 'SCHED_SLICE') { 94 this.schedSliceDetails(selectedId as number); 95 } else if (selection.kind === 'THREAD_STATE') { 96 this.threadStateDetails(selection.id); 97 } else if (selection.kind === 'SLICE') { 98 this.sliceDetails(selection); 99 } 100 } 101 102 async sliceDetails(selection: ThreadSliceSelection) { 103 const selectedId = selection.id; 104 const table = selection.table; 105 106 let leafTable: string; 107 let promisedArgs: Promise<Args>; 108 // TODO(b/155483804): This is a hack to ensure annotation slices are 109 // selectable for now. We should tidy this up when improving this class. 110 if (table === 'annotation') { 111 leafTable = 'annotation_slice'; 112 promisedArgs = Promise.resolve(new Map()); 113 } else { 114 const result = await this.args.engine.query(` 115 SELECT 116 type as leafTable, 117 arg_set_id as argSetId 118 FROM slice WHERE id = ${selectedId}`); 119 120 if (result.numRows() === 0) { 121 return; 122 } 123 124 const row = result.firstRow({ 125 leafTable: STR, 126 argSetId: NUM, 127 }); 128 129 leafTable = row.leafTable; 130 const argSetId = row.argSetId; 131 promisedArgs = this.getArgs(argSetId); 132 } 133 134 const promisedDetails = this.args.engine.query(` 135 SELECT *, ABS_TIME_STR(ts) as absTime FROM ${leafTable} WHERE id = ${selectedId}; 136 `); 137 138 const [details, args] = await Promise.all([promisedDetails, promisedArgs]); 139 140 if (details.numRows() <= 0) return; 141 const rowIter = details.iter({}); 142 assertTrue(rowIter.valid()); 143 144 // A few columns are hard coded as part of the SliceDetails interface. 145 // Long term these should be handled generically as args but for now 146 // handle them specially: 147 let ts = undefined; 148 let absTime = undefined; 149 let dur = undefined; 150 let name = undefined; 151 let category = undefined; 152 let threadDur = undefined; 153 let threadTs = undefined; 154 let trackId = undefined; 155 156 // We select all columns from the leafTable to ensure that we include 157 // additional fields from the child tables (like `thread_dur` from 158 // `thread_slice` or `frame_number` from `frame_slice`). 159 // However, this also includes some basic columns (especially from `slice`) 160 // that are not interesting (i.e. `arg_set_id`, which has already been used 161 // to resolve and show the arguments) and should not be shown to the user. 162 const ignoredColumns = [ 163 'type', 164 'depth', 165 'parent_id', 166 'stack_id', 167 'parent_stack_id', 168 'arg_set_id', 169 'thread_instruction_count', 170 'thread_instruction_delta', 171 ]; 172 173 for (const k of details.columns()) { 174 const v = rowIter.get(k); 175 switch (k) { 176 case 'id': 177 break; 178 case 'ts': 179 ts = timeFromSql(v); 180 break; 181 case 'thread_ts': 182 threadTs = timeFromSql(v); 183 break; 184 case 'absTime': 185 /* eslint-disable @typescript-eslint/strict-boolean-expressions */ 186 if (v) absTime = `${v}`; 187 /* eslint-enable */ 188 break; 189 case 'name': 190 name = `${v}`; 191 break; 192 case 'dur': 193 dur = durationFromSql(v); 194 break; 195 case 'thread_dur': 196 threadDur = durationFromSql(v); 197 break; 198 case 'category': 199 case 'cat': 200 category = `${v}`; 201 break; 202 case 'track_id': 203 trackId = Number(v); 204 break; 205 default: 206 if (!ignoredColumns.includes(k)) args.set(k, `${v}`); 207 } 208 } 209 210 const selected: SliceDetails = { 211 id: selectedId, 212 ts, 213 threadTs, 214 absTime, 215 dur, 216 threadDur, 217 name, 218 category, 219 args, 220 }; 221 222 if (trackId !== undefined) { 223 const columnInfo = ( 224 await this.args.engine.query(` 225 WITH 226 leafTrackTable AS (SELECT type FROM track WHERE id = ${trackId}), 227 cols AS ( 228 SELECT name 229 FROM pragma_table_info((SELECT type FROM leafTrackTable)) 230 ) 231 SELECT 232 type as leafTrackTable, 233 'upid' in cols AS hasUpid, 234 'utid' in cols AS hasUtid 235 FROM leafTrackTable 236 `) 237 ).firstRow({hasUpid: NUM, hasUtid: NUM, leafTrackTable: STR}); 238 const hasUpid = columnInfo.hasUpid !== 0; 239 const hasUtid = columnInfo.hasUtid !== 0; 240 241 if (hasUtid) { 242 const utid = ( 243 await this.args.engine.query(` 244 SELECT utid 245 FROM ${columnInfo.leafTrackTable} 246 WHERE id = ${trackId}; 247 `) 248 ).firstRow({ 249 utid: NUM, 250 }).utid; 251 Object.assign(selected, await this.computeThreadDetails(utid)); 252 } else if (hasUpid) { 253 const upid = ( 254 await this.args.engine.query(` 255 SELECT upid 256 FROM ${columnInfo.leafTrackTable} 257 WHERE id = ${trackId}; 258 `) 259 ).firstRow({ 260 upid: NUM, 261 }).upid; 262 Object.assign(selected, await this.computeProcessDetails(upid)); 263 } 264 } 265 266 // Check selection is still the same on completion of query. 267 if (selection === getLegacySelection(globals.state)) { 268 publishSliceDetails(selected); 269 } 270 } 271 272 async getArgs(argId: number): Promise<Args> { 273 const args = new Map<string, ArgValue>(); 274 const query = ` 275 select 276 key AS name, 277 display_value AS value 278 FROM args 279 WHERE arg_set_id = ${argId} 280 `; 281 const result = await this.args.engine.query(query); 282 const it = result.iter({ 283 name: STR, 284 value: STR_NULL, 285 }); 286 for (; it.valid(); it.next()) { 287 const name = it.name; 288 const value = it.value || 'NULL'; 289 if (name === 'destination slice id' && !isNaN(Number(value))) { 290 const destTrackId = await this.getDestTrackId(value); 291 args.set('Destination Slice', { 292 kind: 'SCHED_SLICE', 293 trackId: destTrackId, 294 sliceId: Number(value), 295 rawValue: value, 296 }); 297 } else { 298 args.set(name, value); 299 } 300 } 301 return args; 302 } 303 304 async getDestTrackId(sliceId: string): Promise<string> { 305 const trackIdQuery = `select track_id as trackId from slice 306 where slice_id = ${sliceId}`; 307 const result = await this.args.engine.query(trackIdQuery); 308 const trackId = result.firstRow({trackId: NUM}).trackId; 309 // TODO(hjd): If we had a consistent mapping from TP track_id 310 // UI track id for slice tracks this would be unnecessary. 311 let trackKey = ''; 312 for (const track of Object.values(globals.state.tracks)) { 313 const trackInfo = globals.trackManager.resolveTrackInfo(track.uri); 314 if (trackInfo?.kind === THREAD_SLICE_TRACK_KIND) { 315 const trackIds = trackInfo?.trackIds; 316 if (trackIds && trackIds.length > 0 && trackIds[0] === trackId) { 317 trackKey = track.key; 318 break; 319 } 320 } 321 } 322 return trackKey; 323 } 324 325 // TODO(altimin): We currently rely on the ThreadStateDetails for supporting 326 // marking the area (the rest goes is handled by ThreadStateTab 327 // directly. Refactor it to be plugin-friendly and remove this. 328 async threadStateDetails(id: number) { 329 const query = ` 330 SELECT 331 ts, 332 thread_state.dur as dur 333 from thread_state 334 where thread_state.id = ${id} 335 `; 336 const result = await this.args.engine.query(query); 337 338 const selection = getLegacySelection(globals.state); 339 if (result.numRows() > 0 && selection) { 340 const row = result.firstRow({ 341 ts: LONG, 342 dur: LONG, 343 }); 344 const selected: ThreadStateDetails = { 345 ts: Time.fromRaw(row.ts), 346 dur: row.dur, 347 }; 348 publishThreadStateDetails(selected); 349 } 350 } 351 352 async schedSliceDetails(id: number) { 353 const sqlQuery = `SELECT 354 sched.ts, 355 sched.dur, 356 sched.priority, 357 sched.end_state as endState, 358 sched.utid, 359 sched.cpu, 360 thread_state.id as threadStateId 361 FROM sched left join thread_state using(ts, utid, cpu) 362 WHERE sched.id = ${id}`; 363 const result = await this.args.engine.query(sqlQuery); 364 // Check selection is still the same on completion of query. 365 const selection = getLegacySelection(globals.state); 366 if (result.numRows() > 0 && selection) { 367 const row = result.firstRow({ 368 ts: LONG, 369 dur: LONG, 370 priority: NUM, 371 endState: STR_NULL, 372 utid: NUM, 373 cpu: NUM, 374 threadStateId: NUM_NULL, 375 }); 376 const ts = Time.fromRaw(row.ts); 377 const dur = row.dur; 378 const priority = row.priority; 379 const endState = row.endState; 380 const utid = row.utid; 381 const cpu = row.cpu; 382 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 383 const threadStateId = row.threadStateId || undefined; 384 const selected: SliceDetails = { 385 ts, 386 dur, 387 priority, 388 endState, 389 cpu, 390 id, 391 utid, 392 threadStateId, 393 }; 394 Object.assign(selected, await this.computeThreadDetails(utid)); 395 396 this.schedulingDetails(ts, utid) 397 .then((wakeResult) => { 398 Object.assign(selected, wakeResult); 399 }) 400 .finally(() => { 401 publishSliceDetails(selected); 402 }); 403 } 404 } 405 406 async schedulingDetails(ts: time, utid: number) { 407 // Find the ts of the first wakeup before the current slice. 408 const wakeResult = await this.args.engine.query(` 409 select ts, waker_utid as wakerUtid 410 from thread_state 411 where utid = ${utid} and ts < ${ts} and state = 'R' 412 order by ts desc 413 limit 1 414 `); 415 if (wakeResult.numRows() === 0) { 416 return undefined; 417 } 418 419 const wakeFirstRow = wakeResult.firstRow({ts: LONG, wakerUtid: NUM_NULL}); 420 const wakeupTs = wakeFirstRow.ts; 421 const wakerUtid = wakeFirstRow.wakerUtid; 422 if (wakerUtid === null) { 423 return undefined; 424 } 425 426 // Find the previous sched slice for the current utid. 427 const prevSchedResult = await this.args.engine.query(` 428 select ts 429 from sched 430 where utid = ${utid} and ts < ${ts} 431 order by ts desc 432 limit 1 433 `); 434 435 // If this is the first sched slice for this utid or if the wakeup found 436 // was after the previous slice then we know the wakeup was for this slice. 437 if ( 438 prevSchedResult.numRows() !== 0 && 439 wakeupTs < prevSchedResult.firstRow({ts: LONG}).ts 440 ) { 441 return undefined; 442 } 443 444 // Find the sched slice with the utid of the waker running when the 445 // sched wakeup occurred. This is the waker. 446 const wakerResult = await this.args.engine.query(` 447 select cpu 448 from sched 449 where 450 utid = ${wakerUtid} and 451 ts < ${wakeupTs} and 452 ts + dur >= ${wakeupTs}; 453 `); 454 if (wakerResult.numRows() === 0) { 455 return undefined; 456 } 457 458 const wakerRow = wakerResult.firstRow({cpu: NUM}); 459 return {wakeupTs, wakerUtid, wakerCpu: wakerRow.cpu}; 460 } 461 462 async computeThreadDetails( 463 utid: number, 464 ): Promise<ThreadDetails & ProcessDetails> { 465 const threadInfo = ( 466 await this.args.engine.query(` 467 SELECT tid, name, upid 468 FROM thread 469 WHERE utid = ${utid}; 470 `) 471 ).firstRow({tid: NUM, name: STR_NULL, upid: NUM_NULL}); 472 const threadDetails = { 473 tid: threadInfo.tid, 474 threadName: threadInfo.name || undefined, 475 }; 476 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 477 if (threadInfo.upid) { 478 return Object.assign( 479 {}, 480 threadDetails, 481 await this.computeProcessDetails(threadInfo.upid), 482 ); 483 } 484 return threadDetails; 485 } 486 487 async computeProcessDetails(upid: number): Promise<ProcessDetails> { 488 const details: ProcessDetails = {}; 489 const processResult = ( 490 await this.args.engine.query(` 491 SELECT pid, name, uid FROM process WHERE upid = ${upid}; 492 `) 493 ).firstRow({pid: NUM, name: STR_NULL, uid: NUM_NULL}); 494 details.pid = processResult.pid; 495 details.processName = processResult.name || undefined; 496 if (processResult.uid === null) { 497 return details; 498 } 499 details.uid = processResult.uid; 500 501 const packageResult = await this.args.engine.query(` 502 SELECT 503 package_name as packageName, 504 version_code as versionCode 505 FROM package_list WHERE uid = ${details.uid}; 506 `); 507 // The package_list table is not populated in some traces so we need to 508 // check if the result has returned any rows. 509 if (packageResult.numRows() > 0) { 510 const packageDetails = packageResult.firstRow({ 511 packageName: STR, 512 versionCode: NUM, 513 }); 514 details.packageName = packageDetails.packageName; 515 details.versionCode = packageDetails.versionCode; 516 } 517 return details; 518 } 519} 520