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 {Arg, Args} from '../common/arg_types'; 16import {Engine} from '../common/engine'; 17import { 18 NUM, 19 singleRow, 20 singleRowUntyped, 21 slowlyCountRows, 22 STR 23} from '../common/query_iterator'; 24import {ChromeSliceSelection} from '../common/state'; 25import {translateState} from '../common/thread_state'; 26import {fromNs, toNs} from '../common/time'; 27import { 28 CounterDetails, 29 SliceDetails, 30 ThreadStateDetails 31} from '../frontend/globals'; 32import {SLICE_TRACK_KIND} from '../tracks/chrome_slices/common'; 33 34import {parseArgs} from './args_parser'; 35import {Controller} from './controller'; 36import {globals} from './globals'; 37 38export interface SelectionControllerArgs { 39 engine: Engine; 40} 41 42// This class queries the TP for the details on a specific slice that has 43// been clicked. 44export class SelectionController extends Controller<'main'> { 45 private lastSelectedId?: number|string; 46 private lastSelectedKind?: string; 47 constructor(private args: SelectionControllerArgs) { 48 super('main'); 49 } 50 51 run() { 52 const selection = globals.state.currentSelection; 53 if (!selection || selection.kind === 'AREA') return; 54 55 const selectWithId = 56 ['SLICE', 'COUNTER', 'CHROME_SLICE', 'HEAP_PROFILE', 'THREAD_STATE']; 57 if (!selectWithId.includes(selection.kind) || 58 (selectWithId.includes(selection.kind) && 59 selection.id === this.lastSelectedId && 60 selection.kind === this.lastSelectedKind)) { 61 return; 62 } 63 const selectedId = selection.id; 64 const selectedKind = selection.kind; 65 this.lastSelectedId = selectedId; 66 this.lastSelectedKind = selectedKind; 67 68 if (selectedId === undefined) return; 69 70 if (selection.kind === 'COUNTER') { 71 const selected: CounterDetails = {}; 72 this.counterDetails(selection.leftTs, selection.rightTs, selection.id) 73 .then(results => { 74 if (results !== undefined && selection && 75 selection.kind === selectedKind && 76 selection.id === selectedId) { 77 Object.assign(selected, results); 78 globals.publish('CounterDetails', selected); 79 } 80 }); 81 } else if (selection.kind === 'SLICE') { 82 this.sliceDetails(selectedId as number); 83 } else if (selection.kind === 'THREAD_STATE') { 84 this.threadStateDetails(selection.id); 85 } else if (selection.kind === 'CHROME_SLICE') { 86 this.chromeSliceDetails(selection); 87 } 88 } 89 90 async chromeSliceDetails(selection: ChromeSliceSelection) { 91 const selectedId = selection.id; 92 const table = selection.table; 93 94 let leafTable: string; 95 let promisedDescription: Promise<Map<string, string>>; 96 let promisedArgs: Promise<Args>; 97 // TODO(b/155483804): This is a hack to ensure annotation slices are 98 // selectable for now. We should tidy this up when improving this class. 99 if (table === 'annotation') { 100 leafTable = 'annotation_slice'; 101 promisedDescription = Promise.resolve(new Map()); 102 promisedArgs = Promise.resolve(new Map()); 103 } else { 104 const typeResult = singleRow( 105 { 106 leafTable: STR, 107 argSetId: NUM, 108 }, 109 await this.args.engine.query(` 110 SELECT 111 type as leafTable, 112 arg_set_id as argSetId 113 FROM slice WHERE id = ${selectedId}`)); 114 115 if (typeResult === undefined) { 116 return; 117 } 118 119 leafTable = typeResult.leafTable; 120 const argSetId = typeResult.argSetId; 121 promisedDescription = this.describeSlice(selectedId); 122 promisedArgs = this.getArgs(argSetId); 123 } 124 125 const promisedDetails = this.args.engine.query(` 126 SELECT * FROM ${leafTable} WHERE id = ${selectedId}; 127 `); 128 129 const [details, args, description] = 130 await Promise.all([promisedDetails, promisedArgs, promisedDescription]); 131 132 const row = singleRowUntyped(details); 133 if (row === undefined) { 134 return; 135 } 136 137 // A few columns are hard coded as part of the SliceDetails interface. 138 // Long term these should be handled generically as args but for now 139 // handle them specially: 140 let ts = undefined; 141 let dur = undefined; 142 let name = undefined; 143 let category = undefined; 144 145 for (const [k, v] of Object.entries(row)) { 146 switch (k) { 147 case 'id': 148 break; 149 case 'ts': 150 ts = fromNs(Number(v)) - globals.state.traceTime.startSec; 151 break; 152 case 'name': 153 name = `${v}`; 154 break; 155 case 'dur': 156 dur = fromNs(Number(v)); 157 break; 158 case 'category': 159 case 'cat': 160 category = `${v}`; 161 break; 162 default: 163 args.set(k, `${v}`); 164 } 165 } 166 167 const argsTree = parseArgs(args); 168 const selected: SliceDetails = { 169 id: selectedId, 170 ts, 171 dur, 172 name, 173 category, 174 args, 175 argsTree, 176 description, 177 }; 178 179 // Check selection is still the same on completion of query. 180 if (selection === globals.state.currentSelection) { 181 globals.publish('SliceDetails', selected); 182 } 183 } 184 185 async describeSlice(id: number): Promise<Map<string, string>> { 186 const map = new Map<string, string>(); 187 if (id === -1) return map; 188 const query = ` 189 select description, doc_link 190 from describe_slice 191 where slice_id = ${id} 192 `; 193 const result = await this.args.engine.query(query); 194 for (let i = 0; i < slowlyCountRows(result); i++) { 195 const description = result.columns[0].stringValues![i]; 196 const docLink = result.columns[1].stringValues![i]; 197 map.set('Description', description); 198 map.set('Documentation', docLink); 199 } 200 return map; 201 } 202 203 async getArgs(argId: number): Promise<Args> { 204 const args = new Map<string, Arg>(); 205 const query = ` 206 select 207 key AS name, 208 CAST(COALESCE(int_value, string_value, real_value) AS text) AS value 209 FROM args 210 WHERE arg_set_id = ${argId} 211 `; 212 const result = await this.args.engine.query(query); 213 for (let i = 0; i < slowlyCountRows(result); i++) { 214 const name = result.columns[0].stringValues![i]; 215 const value = result.columns[1].stringValues![i]; 216 if (name === 'destination slice id' && !isNaN(Number(value))) { 217 const destTrackId = await this.getDestTrackId(value); 218 args.set( 219 'Destination Slice', 220 {kind: 'SLICE', trackId: destTrackId, sliceId: Number(value)}); 221 } else { 222 args.set(name, value); 223 } 224 } 225 return args; 226 } 227 228 async getDestTrackId(sliceId: string): Promise<string> { 229 const trackIdQuery = `select track_id from slice 230 where slice_id = ${sliceId}`; 231 const destResult = await this.args.engine.query(trackIdQuery); 232 const trackIdTp = destResult.columns[0].longValues![0]; 233 // TODO(hjd): If we had a consistent mapping from TP track_id 234 // UI track id for slice tracks this would be unnecessary. 235 let trackId = ''; 236 for (const track of Object.values(globals.state.tracks)) { 237 if (track.kind === SLICE_TRACK_KIND && 238 (track.config as {trackId: number}).trackId === Number(trackIdTp)) { 239 trackId = track.id; 240 break; 241 } 242 } 243 return trackId; 244 } 245 246 async threadStateDetails(id: number) { 247 const query = ` 248 SELECT 249 ts, 250 thread_state.dur, 251 state, 252 io_wait, 253 thread_state.utid, 254 thread_state.cpu, 255 sched.id, 256 thread_state.blocked_function 257 from thread_state 258 left join sched using(ts) where thread_state.id = ${id} 259 `; 260 this.args.engine.query(query).then(result => { 261 const selection = globals.state.currentSelection; 262 const cols = result.columns; 263 if (slowlyCountRows(result) === 1 && selection) { 264 const ts = cols[0].longValues![0]; 265 const timeFromStart = fromNs(ts) - globals.state.traceTime.startSec; 266 const dur = fromNs(cols[1].longValues![0]); 267 const stateStr = cols[2].stringValues![0]; 268 const ioWait = 269 cols[3].isNulls![0] ? undefined : !!cols[3].longValues![0]; 270 const state = translateState(stateStr, ioWait); 271 const utid = cols[4].longValues![0]; 272 const cpu = cols[5].isNulls![0] ? undefined : cols[5].longValues![0]; 273 const sliceId = 274 cols[6].isNulls![0] ? undefined : cols[6].longValues![0]; 275 const blockedFunction = 276 cols[7].isNulls![0] ? undefined : cols[7].stringValues![0]; 277 const selected: ThreadStateDetails = { 278 ts: timeFromStart, 279 dur, 280 state, 281 utid, 282 cpu, 283 sliceId, 284 blockedFunction 285 }; 286 globals.publish('ThreadStateDetails', selected); 287 } 288 }); 289 } 290 291 async sliceDetails(id: number) { 292 const sqlQuery = `SELECT ts, dur, priority, end_state, utid, cpu, 293 thread_state.id FROM sched join thread_state using(ts, utid, dur, cpu) 294 WHERE sched.id = ${id}`; 295 this.args.engine.query(sqlQuery).then(result => { 296 // Check selection is still the same on completion of query. 297 const selection = globals.state.currentSelection; 298 if (slowlyCountRows(result) === 1 && selection) { 299 const ts = result.columns[0].longValues![0]; 300 const timeFromStart = fromNs(ts) - globals.state.traceTime.startSec; 301 const dur = fromNs(result.columns[1].longValues![0]); 302 const priority = result.columns[2].longValues![0]; 303 const endState = result.columns[3].stringValues![0]; 304 const utid = result.columns[4].longValues![0]; 305 const cpu = result.columns[5].longValues![0]; 306 const threadStateId = result.columns[6].longValues![0]; 307 const selected: SliceDetails = { 308 ts: timeFromStart, 309 dur, 310 priority, 311 endState, 312 cpu, 313 id, 314 utid, 315 threadStateId 316 }; 317 this.schedulingDetails(ts, utid) 318 .then(wakeResult => { 319 Object.assign(selected, wakeResult); 320 }) 321 .finally(() => { 322 globals.publish('SliceDetails', selected); 323 }); 324 } 325 }); 326 } 327 328 async counterDetails(ts: number, rightTs: number, id: number) { 329 const counter = await this.args.engine.query( 330 `SELECT value, track_id FROM counter WHERE id = ${id}`); 331 const value = counter.columns[0].doubleValues![0]; 332 const trackId = counter.columns[1].longValues![0]; 333 // Finding previous value. If there isn't previous one, it will return 0 for 334 // ts and value. 335 const previous = await this.args.engine.query( 336 `SELECT MAX(ts), value FROM counter WHERE ts < ${ts} and track_id = ${ 337 trackId}`); 338 const previousValue = previous.columns[1].doubleValues![0]; 339 const endTs = 340 rightTs !== -1 ? rightTs : toNs(globals.state.traceTime.endSec); 341 const delta = value - previousValue; 342 const duration = endTs - ts; 343 const startTime = fromNs(ts) - globals.state.traceTime.startSec; 344 return {startTime, value, delta, duration}; 345 } 346 347 async schedulingDetails(ts: number, utid: number|Long) { 348 let event = 'sched_waking'; 349 const waking = await this.args.engine.query( 350 `select * from instants where name = 'sched_waking' limit 1`); 351 const wakeup = await this.args.engine.query( 352 `select * from instants where name = 'sched_wakeup' limit 1`); 353 if (slowlyCountRows(waking) === 0) { 354 if (slowlyCountRows(wakeup) === 0) return undefined; 355 // Only use sched_wakeup if waking is not in the trace. 356 event = 'sched_wakeup'; 357 } 358 359 // Find the ts of the first sched_wakeup before the current slice. 360 const queryWakeupTs = `select ts from instants where name = '${event}' 361 and ref = ${utid} and ts < ${ts} order by ts desc limit 1`; 362 const wakeupRow = await this.args.engine.queryOneRow(queryWakeupTs); 363 // Find the previous sched slice for the current utid. 364 const queryPrevSched = `select ts from sched where utid = ${utid} 365 and ts < ${ts} order by ts desc limit 1`; 366 const prevSchedRow = await this.args.engine.queryOneRow(queryPrevSched); 367 // If this is the first sched slice for this utid or if the wakeup found 368 // was after the previous slice then we know the wakeup was for this slice. 369 if (wakeupRow[0] === undefined || 370 (prevSchedRow[0] !== undefined && wakeupRow[0] < prevSchedRow[0])) { 371 return undefined; 372 } 373 const wakeupTs = wakeupRow[0]; 374 // Find the sched slice with the utid of the waker running when the 375 // sched wakeup occurred. This is the waker. 376 const queryWaker = `select utid, cpu from sched where utid = 377 (select utid from raw where name = '${event}' and ts = ${wakeupTs}) 378 and ts < ${wakeupTs} and ts + dur >= ${wakeupTs};`; 379 const wakerRow = await this.args.engine.queryOneRow(queryWaker); 380 if (wakerRow) { 381 return { 382 wakeupTs: fromNs(wakeupTs), 383 wakerUtid: wakerRow[0], 384 wakerCpu: wakerRow[1] 385 }; 386 } else { 387 return undefined; 388 } 389 } 390} 391