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