1// Copyright (C) 2020 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use size 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 {Time} from '../base/time'; 16import {featureFlags} from './feature_flags'; 17import {FlowDirection, Flow} from './flow_types'; 18import {asSliceSqlId} from '../components/sql_utils/core_types'; 19import {LONG, NUM, STR_NULL} from '../trace_processor/query_result'; 20import { 21 ACTUAL_FRAMES_SLICE_TRACK_KIND, 22 SLICE_TRACK_KIND, 23} from '../public/track_kinds'; 24import {TrackDescriptor, TrackManager} from '../public/track'; 25import {AreaSelection, Selection, SelectionManager} from '../public/selection'; 26import {raf} from './raf_scheduler'; 27import {Engine} from '../trace_processor/engine'; 28 29const SHOW_INDIRECT_PRECEDING_FLOWS_FLAG = featureFlags.register({ 30 id: 'showIndirectPrecedingFlows', 31 name: 'Show indirect preceding flows', 32 description: 33 'Show indirect preceding flows (connected through ancestor ' + 34 'slices) when a slice is selected.', 35 defaultValue: false, 36}); 37 38export class FlowManager { 39 private _connectedFlows: Flow[] = []; 40 private _selectedFlows: Flow[] = []; 41 private _curSelection?: Selection; 42 private _focusedFlowIdLeft = -1; 43 private _focusedFlowIdRight = -1; 44 private _visibleCategories = new Map<string, boolean>(); 45 private _initialized = false; 46 47 constructor( 48 private engine: Engine, 49 private trackMgr: TrackManager, 50 private selectionMgr: SelectionManager, 51 ) {} 52 53 // TODO(primiano): the only reason why this is not done in the constructor is 54 // because when loading the UI with no trace, we initialize globals with a 55 // FakeTraceImpl with a FakeEngine, which crashes when issuing queries. 56 // This can be moved in the ctor once globals go away. 57 private initialize() { 58 if (this._initialized) return; 59 this._initialized = true; 60 // Create |CHROME_CUSTOME_SLICE_NAME| helper, which combines slice name 61 // and args for some slices (scheduler tasks and mojo messages) for more 62 // helpful messages. 63 // In the future, it should be replaced with this a more scalable and 64 // customisable solution. 65 // Note that a function here is significantly faster than a join. 66 this.engine.query(` 67 SELECT CREATE_FUNCTION( 68 'CHROME_CUSTOM_SLICE_NAME(slice_id LONG)', 69 'STRING', 70 'select case 71 when name="Receive mojo message" then 72 printf("Receive mojo message (interface=%s, hash=%s)", 73 EXTRACT_ARG(arg_set_id, 74 "chrome_mojo_event_info.mojo_interface_tag"), 75 EXTRACT_ARG(arg_set_id, "chrome_mojo_event_info.ipc_hash")) 76 when name="ThreadControllerImpl::RunTask" or 77 name="ThreadPool_RunTask" then 78 printf("RunTask(posted_from=%s:%s)", 79 EXTRACT_ARG(arg_set_id, "task.posted_from.file_name"), 80 EXTRACT_ARG(arg_set_id, "task.posted_from.function_name")) 81 end 82 from slice where id=$slice_id' 83 );`); 84 } 85 86 async queryFlowEvents(query: string): Promise<Flow[]> { 87 const result = await this.engine.query(query); 88 const flows: Flow[] = []; 89 90 const it = result.iter({ 91 beginSliceId: NUM, 92 beginTrackId: NUM, 93 beginSliceName: STR_NULL, 94 beginSliceChromeCustomName: STR_NULL, 95 beginSliceCategory: STR_NULL, 96 beginSliceStartTs: LONG, 97 beginSliceEndTs: LONG, 98 beginDepth: NUM, 99 beginThreadName: STR_NULL, 100 beginProcessName: STR_NULL, 101 endSliceId: NUM, 102 endTrackId: NUM, 103 endSliceName: STR_NULL, 104 endSliceChromeCustomName: STR_NULL, 105 endSliceCategory: STR_NULL, 106 endSliceStartTs: LONG, 107 endSliceEndTs: LONG, 108 endDepth: NUM, 109 endThreadName: STR_NULL, 110 endProcessName: STR_NULL, 111 name: STR_NULL, 112 category: STR_NULL, 113 id: NUM, 114 flowToDescendant: NUM, 115 }); 116 117 const nullToStr = (s: null | string): string => { 118 return s === null ? 'NULL' : s; 119 }; 120 121 const nullToUndefined = (s: null | string): undefined | string => { 122 return s === null ? undefined : s; 123 }; 124 125 const nodes = []; 126 127 for (; it.valid(); it.next()) { 128 // Category and name present only in version 1 flow events 129 // It is most likelly NULL for all other versions 130 const category = nullToUndefined(it.category); 131 const name = nullToUndefined(it.name); 132 const id = it.id; 133 134 const begin = { 135 trackId: it.beginTrackId, 136 sliceId: asSliceSqlId(it.beginSliceId), 137 sliceName: nullToStr(it.beginSliceName), 138 sliceChromeCustomName: nullToUndefined(it.beginSliceChromeCustomName), 139 sliceCategory: nullToStr(it.beginSliceCategory), 140 sliceStartTs: Time.fromRaw(it.beginSliceStartTs), 141 sliceEndTs: Time.fromRaw(it.beginSliceEndTs), 142 depth: it.beginDepth, 143 threadName: nullToStr(it.beginThreadName), 144 processName: nullToStr(it.beginProcessName), 145 }; 146 147 const end = { 148 trackId: it.endTrackId, 149 sliceId: asSliceSqlId(it.endSliceId), 150 sliceName: nullToStr(it.endSliceName), 151 sliceChromeCustomName: nullToUndefined(it.endSliceChromeCustomName), 152 sliceCategory: nullToStr(it.endSliceCategory), 153 sliceStartTs: Time.fromRaw(it.endSliceStartTs), 154 sliceEndTs: Time.fromRaw(it.endSliceEndTs), 155 depth: it.endDepth, 156 threadName: nullToStr(it.endThreadName), 157 processName: nullToStr(it.endProcessName), 158 }; 159 160 nodes.push(begin); 161 nodes.push(end); 162 163 flows.push({ 164 id, 165 begin, 166 end, 167 dur: it.endSliceStartTs - it.beginSliceEndTs, 168 category, 169 name, 170 flowToDescendant: !!it.flowToDescendant, 171 }); 172 } 173 174 // Everything below here is a horrible hack to support flows for 175 // async slice tracks. 176 // In short the issue is this: 177 // - For most slice tracks there is a one-to-one mapping between 178 // the track in the UI and the track in the TP. n.b. Even in this 179 // case the UI 'trackId' and the TP 'track.id' may not be the 180 // same. In this case 'depth' in the TP is the exact depth in the 181 // UI. 182 // - In the case of aysnc tracks however the mapping is 183 // one-to-many. Each async slice track in the UI is 'backed' but 184 // multiple TP tracks. In order to render this track we need 185 // to adjust depth to avoid overlapping slices. In the render 186 // path we use experimental_slice_layout for this purpose. This 187 // is a virtual table in the TP which, for an arbitrary collection 188 // of TP trackIds, computes for each slice a 'layout_depth'. 189 // - Everything above in this function and its callers doesn't 190 // know anything about layout_depth. 191 // 192 // So if we stopped here we would have incorrect rendering for 193 // async slice tracks. Instead we want to 'fix' depth for these 194 // cases. We do this in two passes. 195 // - First we collect all the information we need in 'Info' POJOs 196 // - Secondly we loop over those Infos querying 197 // the database to find the layout_depth for each sliceId 198 // TODO(hjd): This should not be needed after TracksV2 lands. 199 200 // We end up with one Info POJOs for each UI async slice track 201 // which has at least one flow {begin,end}ing in one of its slices. 202 interface Info { 203 siblingTrackIds: number[]; 204 sliceIds: number[]; 205 nodes: Array<{ 206 sliceId: number; 207 depth: number; 208 }>; 209 } 210 211 const trackUriToInfo = new Map<string, null | Info>(); 212 const trackIdToInfo = new Map<number, null | Info>(); 213 214 const trackIdToTrack = new Map<number, TrackDescriptor>(); 215 this.trackMgr 216 .getAllTracks() 217 .forEach((trackDescriptor) => 218 trackDescriptor.tags?.trackIds?.forEach((trackId) => 219 trackIdToTrack.set(trackId, trackDescriptor), 220 ), 221 ); 222 223 const getInfo = (trackId: number): null | Info => { 224 let info = trackIdToInfo.get(trackId); 225 if (info !== undefined) { 226 return info; 227 } 228 229 const trackDescriptor = trackIdToTrack.get(trackId); 230 if (trackDescriptor === undefined) { 231 trackIdToInfo.set(trackId, null); 232 return null; 233 } 234 235 info = trackUriToInfo.get(trackDescriptor.uri); 236 if (info !== undefined) { 237 return info; 238 } 239 240 // If 'trackIds' is undefined this is not an async slice track so 241 // we don't need to do anything. We also don't need to do 242 // anything if there is only one TP track in this async track. In 243 // that case experimental_slice_layout is just an expensive way 244 // to find out depth === layout_depth. 245 const trackIds = trackDescriptor?.tags?.trackIds; 246 if (trackIds === undefined || trackIds.length <= 1) { 247 trackUriToInfo.set(trackDescriptor.uri, null); 248 trackIdToInfo.set(trackId, null); 249 return null; 250 } 251 252 const newInfo = { 253 siblingTrackIds: [...trackIds], 254 sliceIds: [], 255 nodes: [], 256 }; 257 258 trackUriToInfo.set(trackDescriptor.uri, newInfo); 259 trackIdToInfo.set(trackId, newInfo); 260 261 return newInfo; 262 }; 263 264 // First pass, collect: 265 // - all slices that belong to async slice track 266 // - grouped by the async slice track in question 267 for (const node of nodes) { 268 const info = getInfo(node.trackId); 269 if (info !== null) { 270 info.sliceIds.push(node.sliceId); 271 info.nodes.push(node); 272 } 273 } 274 275 // Second pass, for each async track: 276 // - Query to find the layout_depth for each relevant sliceId 277 // - Iterate through the nodes updating the depth in place 278 for (const info of trackUriToInfo.values()) { 279 if (info === null) { 280 continue; 281 } 282 const r = await this.engine.query(` 283 SELECT 284 id, 285 layout_depth as depth 286 FROM 287 experimental_slice_layout 288 WHERE 289 filter_track_ids = '${info.siblingTrackIds.join(',')}' 290 AND id in (${info.sliceIds.join(', ')}) 291 `); 292 293 // Create the sliceId -> new depth map: 294 const it = r.iter({ 295 id: NUM, 296 depth: NUM, 297 }); 298 const sliceIdToDepth = new Map<number, number>(); 299 for (; it.valid(); it.next()) { 300 sliceIdToDepth.set(it.id, it.depth); 301 } 302 303 // For each begin/end from an async track update the depth: 304 for (const node of info.nodes) { 305 const newDepth = sliceIdToDepth.get(node.sliceId); 306 if (newDepth !== undefined) { 307 node.depth = newDepth; 308 } 309 } 310 } 311 312 // Fill in the track uris if available 313 flows.forEach((flow) => { 314 flow.begin.trackUri = trackIdToTrack.get(flow.begin.trackId)?.uri; 315 flow.end.trackUri = trackIdToTrack.get(flow.end.trackId)?.uri; 316 }); 317 318 return flows; 319 } 320 321 sliceSelected(sliceId: number) { 322 const connectedFlows = SHOW_INDIRECT_PRECEDING_FLOWS_FLAG.get() 323 ? `( 324 select * from directly_connected_flow(${sliceId}) 325 union 326 select * from preceding_flow(${sliceId}) 327 )` 328 : `directly_connected_flow(${sliceId})`; 329 330 const query = ` 331 -- Include slices.flow to initialise indexes on 'flow.slice_in' and 'flow.slice_out'. 332 INCLUDE PERFETTO MODULE slices.flow; 333 334 select 335 f.slice_out as beginSliceId, 336 t1.track_id as beginTrackId, 337 t1.name as beginSliceName, 338 CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName, 339 t1.category as beginSliceCategory, 340 t1.ts as beginSliceStartTs, 341 (t1.ts+t1.dur) as beginSliceEndTs, 342 t1.depth as beginDepth, 343 (thread_out.name || ' ' || thread_out.tid) as beginThreadName, 344 (process_out.name || ' ' || process_out.pid) as beginProcessName, 345 f.slice_in as endSliceId, 346 t2.track_id as endTrackId, 347 t2.name as endSliceName, 348 CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName, 349 t2.category as endSliceCategory, 350 t2.ts as endSliceStartTs, 351 (t2.ts+t2.dur) as endSliceEndTs, 352 t2.depth as endDepth, 353 (thread_in.name || ' ' || thread_in.tid) as endThreadName, 354 (process_in.name || ' ' || process_in.pid) as endProcessName, 355 extract_arg(f.arg_set_id, 'cat') as category, 356 extract_arg(f.arg_set_id, 'name') as name, 357 f.id as id, 358 slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant 359 from ${connectedFlows} f 360 join slice t1 on f.slice_out = t1.slice_id 361 join slice t2 on f.slice_in = t2.slice_id 362 left join thread_track track_out on track_out.id = t1.track_id 363 left join thread thread_out on thread_out.utid = track_out.utid 364 left join thread_track track_in on track_in.id = t2.track_id 365 left join thread thread_in on thread_in.utid = track_in.utid 366 left join process process_out on process_out.upid = thread_out.upid 367 left join process process_in on process_in.upid = thread_in.upid 368 `; 369 this.queryFlowEvents(query).then((flows) => this.setConnectedFlows(flows)); 370 } 371 372 private areaSelected(area: AreaSelection) { 373 const trackIds: number[] = []; 374 375 for (const trackInfo of area.tracks) { 376 const kind = trackInfo?.tags?.kind; 377 if ( 378 kind === SLICE_TRACK_KIND || 379 kind === ACTUAL_FRAMES_SLICE_TRACK_KIND 380 ) { 381 if (trackInfo?.tags?.trackIds) { 382 for (const trackId of trackInfo.tags.trackIds) { 383 trackIds.push(trackId); 384 } 385 } 386 } 387 } 388 389 const tracks = `(${trackIds.join(',')})`; 390 391 const startNs = area.start; 392 const endNs = area.end; 393 394 const query = ` 395 select 396 f.slice_out as beginSliceId, 397 t1.track_id as beginTrackId, 398 t1.name as beginSliceName, 399 CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName, 400 t1.category as beginSliceCategory, 401 t1.ts as beginSliceStartTs, 402 (t1.ts+t1.dur) as beginSliceEndTs, 403 t1.depth as beginDepth, 404 NULL as beginThreadName, 405 NULL as beginProcessName, 406 f.slice_in as endSliceId, 407 t2.track_id as endTrackId, 408 t2.name as endSliceName, 409 CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName, 410 t2.category as endSliceCategory, 411 t2.ts as endSliceStartTs, 412 (t2.ts+t2.dur) as endSliceEndTs, 413 t2.depth as endDepth, 414 NULL as endThreadName, 415 NULL as endProcessName, 416 extract_arg(f.arg_set_id, 'cat') as category, 417 extract_arg(f.arg_set_id, 'name') as name, 418 f.id as id, 419 slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant 420 from flow f 421 join slice t1 on f.slice_out = t1.slice_id 422 join slice t2 on f.slice_in = t2.slice_id 423 where 424 (t1.track_id in ${tracks} 425 and (t1.ts+t1.dur <= ${endNs} and t1.ts+t1.dur >= ${startNs})) 426 or 427 (t2.track_id in ${tracks} 428 and (t2.ts <= ${endNs} and t2.ts >= ${startNs})) 429 `; 430 this.queryFlowEvents(query).then((flows) => this.setSelectedFlows(flows)); 431 } 432 433 private setConnectedFlows(connectedFlows: Flow[]) { 434 this._connectedFlows = connectedFlows; 435 // If a chrome slice is selected and we have any flows in connectedFlows 436 // we will find the flows on the right and left of that slice to set a default 437 // focus. In all other cases the focusedFlowId(Left|Right) will be set to -1. 438 this._focusedFlowIdLeft = -1; 439 this._focusedFlowIdRight = -1; 440 if (this._curSelection?.kind === 'track_event') { 441 const sliceId = this._curSelection.eventId; 442 for (const flow of connectedFlows) { 443 if (flow.begin.sliceId === sliceId) { 444 this._focusedFlowIdRight = flow.id; 445 } 446 if (flow.end.sliceId === sliceId) { 447 this._focusedFlowIdLeft = flow.id; 448 } 449 } 450 } 451 raf.scheduleFullRedraw(); 452 } 453 454 private setSelectedFlows(selectedFlows: Flow[]) { 455 this._selectedFlows = selectedFlows; 456 raf.scheduleFullRedraw(); 457 } 458 459 updateFlows(selection: Selection) { 460 this.initialize(); 461 this._curSelection = selection; 462 463 if (selection.kind === 'empty') { 464 this.setConnectedFlows([]); 465 this.setSelectedFlows([]); 466 return; 467 } 468 469 // TODO(b/155483804): This is a hack as annotation slices don't contain 470 // flows. We should tidy this up when fixing this bug. 471 if (selection.kind === 'track_event' && selection.tableName === 'slice') { 472 this.sliceSelected(selection.eventId); 473 } else { 474 this.setConnectedFlows([]); 475 } 476 477 if (selection.kind === 'area') { 478 this.areaSelected(selection); 479 } else { 480 this.setConnectedFlows([]); 481 } 482 } 483 484 // Change focus to the next flow event (matching the direction) 485 focusOtherFlow(direction: FlowDirection) { 486 const currentSelection = this._curSelection; 487 if (!currentSelection || currentSelection.kind !== 'track_event') { 488 return; 489 } 490 const sliceId = currentSelection.eventId; 491 if (sliceId === -1) { 492 return; 493 } 494 495 const boundFlows = this._connectedFlows.filter( 496 (flow) => 497 (flow.begin.sliceId === sliceId && direction === 'Forward') || 498 (flow.end.sliceId === sliceId && direction === 'Backward'), 499 ); 500 501 if (direction === 'Backward') { 502 const nextFlowId = findAnotherFlowExcept( 503 boundFlows, 504 this._focusedFlowIdLeft, 505 ); 506 this._focusedFlowIdLeft = nextFlowId; 507 } else { 508 const nextFlowId = findAnotherFlowExcept( 509 boundFlows, 510 this._focusedFlowIdRight, 511 ); 512 this._focusedFlowIdRight = nextFlowId; 513 } 514 raf.scheduleFullRedraw(); 515 } 516 517 // Select the slice connected to the flow in focus 518 moveByFocusedFlow(direction: FlowDirection): void { 519 const currentSelection = this._curSelection; 520 if (!currentSelection || currentSelection.kind !== 'track_event') { 521 return; 522 } 523 524 const sliceId = currentSelection.eventId; 525 const flowId = 526 direction === 'Backward' 527 ? this._focusedFlowIdLeft 528 : this._focusedFlowIdRight; 529 530 if (sliceId === -1 || flowId === -1) { 531 return; 532 } 533 534 // Find flow that is in focus and select corresponding slice 535 for (const flow of this._connectedFlows) { 536 if (flow.id === flowId) { 537 const flowPoint = direction === 'Backward' ? flow.begin : flow.end; 538 this.selectionMgr.selectSqlEvent('slice', flowPoint.sliceId, { 539 scrollToSelection: true, 540 }); 541 } 542 } 543 } 544 545 get connectedFlows() { 546 return this._connectedFlows; 547 } 548 549 get selectedFlows() { 550 return this._selectedFlows; 551 } 552 553 get focusedFlowIdLeft() { 554 return this._focusedFlowIdLeft; 555 } 556 get focusedFlowIdRight() { 557 return this._focusedFlowIdRight; 558 } 559 560 get visibleCategories(): ReadonlyMap<string, boolean> { 561 return this._visibleCategories; 562 } 563 564 setCategoryVisible(name: string, value: boolean) { 565 this._visibleCategories.set(name, value); 566 raf.scheduleFullRedraw(); 567 } 568} 569 570// Search |boundFlows| for |flowId| and return the id following it. 571// Returns the first flow id if nothing was found or |flowId| was the last flow 572// in |boundFlows|, and -1 if |boundFlows| is empty 573function findAnotherFlowExcept(boundFlows: Flow[], flowId: number): number { 574 let selectedFlowFound = false; 575 576 if (boundFlows.length === 0) { 577 return -1; 578 } 579 580 for (const flow of boundFlows) { 581 if (selectedFlowFound) { 582 return flow.id; 583 } 584 585 if (flow.id === flowId) { 586 selectedFlowFound = true; 587 } 588 } 589 return boundFlows[0].id; 590} 591