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