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