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 {AreaSelection, getLegacySelection} from '../common/state'; 17import {featureFlags} from '../core/feature_flags'; 18import {Flow, globals} from '../frontend/globals'; 19import {publishConnectedFlows, publishSelectedFlows} from '../frontend/publish'; 20import {asSliceSqlId} from '../frontend/sql_types'; 21import {Engine} from '../trace_processor/engine'; 22import {LONG, NUM, STR_NULL} from '../trace_processor/query_result'; 23 24import {Controller} from './controller'; 25import {Monitor} from '../base/monitor'; 26import { 27 ACTUAL_FRAMES_SLICE_TRACK_KIND, 28 THREAD_SLICE_TRACK_KIND, 29} from '../core/track_kinds'; 30 31export interface FlowEventsControllerArgs { 32 engine: Engine; 33} 34 35const SHOW_INDIRECT_PRECEDING_FLOWS_FLAG = featureFlags.register({ 36 id: 'showIndirectPrecedingFlows', 37 name: 'Show indirect preceding flows', 38 description: 39 'Show indirect preceding flows (connected through ancestor ' + 40 'slices) when a slice is selected.', 41 defaultValue: false, 42}); 43 44export class FlowEventsController extends Controller<'main'> { 45 private readonly monitor = new Monitor([() => globals.state.selection]); 46 47 constructor(private args: FlowEventsControllerArgs) { 48 super('main'); 49 50 // Create |CHROME_CUSTOME_SLICE_NAME| helper, which combines slice name 51 // and args for some slices (scheduler tasks and mojo messages) for more 52 // helpful messages. 53 // In the future, it should be replaced with this a more scalable and 54 // customisable solution. 55 // Note that a function here is significantly faster than a join. 56 this.args.engine.query(` 57 SELECT CREATE_FUNCTION( 58 'CHROME_CUSTOM_SLICE_NAME(slice_id LONG)', 59 'STRING', 60 'select case 61 when name="Receive mojo message" then 62 printf("Receive mojo message (interface=%s, hash=%s)", 63 EXTRACT_ARG(arg_set_id, 64 "chrome_mojo_event_info.mojo_interface_tag"), 65 EXTRACT_ARG(arg_set_id, "chrome_mojo_event_info.ipc_hash")) 66 when name="ThreadControllerImpl::RunTask" or 67 name="ThreadPool_RunTask" then 68 printf("RunTask(posted_from=%s:%s)", 69 EXTRACT_ARG(arg_set_id, "task.posted_from.file_name"), 70 EXTRACT_ARG(arg_set_id, "task.posted_from.function_name")) 71 end 72 from slice where id=$slice_id' 73 );`); 74 } 75 76 async queryFlowEvents(query: string, callback: (flows: Flow[]) => void) { 77 const result = await this.args.engine.query(query); 78 const flows: Flow[] = []; 79 80 const it = result.iter({ 81 beginSliceId: NUM, 82 beginTrackId: NUM, 83 beginSliceName: STR_NULL, 84 beginSliceChromeCustomName: STR_NULL, 85 beginSliceCategory: STR_NULL, 86 beginSliceStartTs: LONG, 87 beginSliceEndTs: LONG, 88 beginDepth: NUM, 89 beginThreadName: STR_NULL, 90 beginProcessName: STR_NULL, 91 endSliceId: NUM, 92 endTrackId: NUM, 93 endSliceName: STR_NULL, 94 endSliceChromeCustomName: STR_NULL, 95 endSliceCategory: STR_NULL, 96 endSliceStartTs: LONG, 97 endSliceEndTs: LONG, 98 endDepth: NUM, 99 endThreadName: STR_NULL, 100 endProcessName: STR_NULL, 101 name: STR_NULL, 102 category: STR_NULL, 103 id: NUM, 104 flowToDescendant: NUM, 105 }); 106 107 const nullToStr = (s: null | string): string => { 108 return s === null ? 'NULL' : s; 109 }; 110 111 const nullToUndefined = (s: null | string): undefined | string => { 112 return s === null ? undefined : s; 113 }; 114 115 const nodes = []; 116 117 for (; it.valid(); it.next()) { 118 // Category and name present only in version 1 flow events 119 // It is most likelly NULL for all other versions 120 const category = nullToUndefined(it.category); 121 const name = nullToUndefined(it.name); 122 const id = it.id; 123 124 const begin = { 125 trackId: it.beginTrackId, 126 sliceId: asSliceSqlId(it.beginSliceId), 127 sliceName: nullToStr(it.beginSliceName), 128 sliceChromeCustomName: nullToUndefined(it.beginSliceChromeCustomName), 129 sliceCategory: nullToStr(it.beginSliceCategory), 130 sliceStartTs: Time.fromRaw(it.beginSliceStartTs), 131 sliceEndTs: Time.fromRaw(it.beginSliceEndTs), 132 depth: it.beginDepth, 133 threadName: nullToStr(it.beginThreadName), 134 processName: nullToStr(it.beginProcessName), 135 }; 136 137 const end = { 138 trackId: it.endTrackId, 139 sliceId: asSliceSqlId(it.endSliceId), 140 sliceName: nullToStr(it.endSliceName), 141 sliceChromeCustomName: nullToUndefined(it.endSliceChromeCustomName), 142 sliceCategory: nullToStr(it.endSliceCategory), 143 sliceStartTs: Time.fromRaw(it.endSliceStartTs), 144 sliceEndTs: Time.fromRaw(it.endSliceEndTs), 145 depth: it.endDepth, 146 threadName: nullToStr(it.endThreadName), 147 processName: nullToStr(it.endProcessName), 148 }; 149 150 nodes.push(begin); 151 nodes.push(end); 152 153 flows.push({ 154 id, 155 begin, 156 end, 157 dur: it.endSliceStartTs - it.beginSliceEndTs, 158 category, 159 name, 160 flowToDescendant: !!it.flowToDescendant, 161 }); 162 } 163 164 // Everything below here is a horrible hack to support flows for 165 // async slice tracks. 166 // In short the issue is this: 167 // - For most slice tracks there is a one-to-one mapping between 168 // the track in the UI and the track in the TP. n.b. Even in this 169 // case the UI 'trackId' and the TP 'track.id' may not be the 170 // same. In this case 'depth' in the TP is the exact depth in the 171 // UI. 172 // - In the case of aysnc tracks however the mapping is 173 // one-to-many. Each async slice track in the UI is 'backed' but 174 // multiple TP tracks. In order to render this track we need 175 // to adjust depth to avoid overlapping slices. In the render 176 // path we use experimental_slice_layout for this purpose. This 177 // is a virtual table in the TP which, for an arbitrary collection 178 // of TP trackIds, computes for each slice a 'layout_depth'. 179 // - Everything above in this function and its callers doesn't 180 // know anything about layout_depth. 181 // 182 // So if we stopped here we would have incorrect rendering for 183 // async slice tracks. Instead we want to 'fix' depth for these 184 // cases. We do this in two passes. 185 // - First we collect all the information we need in 'Info' POJOs 186 // - Secondly we loop over those Infos querying 187 // the database to find the layout_depth for each sliceId 188 // TODO(hjd): This should not be needed after TracksV2 lands. 189 190 // We end up with one Info POJOs for each UI async slice track 191 // which has at least one flow {begin,end}ing in one of its slices. 192 interface Info { 193 uiTrackId: string; 194 siblingTrackIds: number[]; 195 sliceIds: number[]; 196 nodes: Array<{ 197 sliceId: number; 198 depth: number; 199 }>; 200 } 201 202 const uiTrackIdToInfo = new Map<string, null | Info>(); 203 const trackIdToInfo = new Map<number, null | Info>(); 204 205 const trackIdToUiTrackId = globals.trackManager.trackKeyByTrackId; 206 const tracks = globals.state.tracks; 207 208 const getInfo = (trackId: number): null | Info => { 209 let info = trackIdToInfo.get(trackId); 210 if (info !== undefined) { 211 return info; 212 } 213 214 const uiTrackId = trackIdToUiTrackId.get(trackId); 215 if (uiTrackId === undefined) { 216 trackIdToInfo.set(trackId, null); 217 return null; 218 } 219 220 const track = tracks[uiTrackId]; 221 if (track === undefined) { 222 trackIdToInfo.set(trackId, null); 223 return null; 224 } 225 226 info = uiTrackIdToInfo.get(uiTrackId); 227 if (info !== undefined) { 228 return info; 229 } 230 231 // If 'trackIds' is undefined this is not an async slice track so 232 // we don't need to do anything. We also don't need to do 233 // anything if there is only one TP track in this async track. In 234 // that case experimental_slice_layout is just an expensive way 235 // to find out depth === layout_depth. 236 const trackInfo = globals.trackManager.resolveTrackInfo(track.uri); 237 const trackIds = trackInfo?.trackIds; 238 if (trackIds === undefined || trackIds.length <= 1) { 239 uiTrackIdToInfo.set(uiTrackId, null); 240 trackIdToInfo.set(trackId, null); 241 return null; 242 } 243 244 const newInfo = { 245 uiTrackId, 246 siblingTrackIds: trackIds, 247 sliceIds: [], 248 nodes: [], 249 }; 250 251 uiTrackIdToInfo.set(uiTrackId, newInfo); 252 trackIdToInfo.set(trackId, newInfo); 253 254 return newInfo; 255 }; 256 257 // First pass, collect: 258 // - all slices that belong to async slice track 259 // - grouped by the async slice track in question 260 for (const node of nodes) { 261 const info = getInfo(node.trackId); 262 if (info !== null) { 263 info.sliceIds.push(node.sliceId); 264 info.nodes.push(node); 265 } 266 } 267 268 // Second pass, for each async track: 269 // - Query to find the layout_depth for each relevant sliceId 270 // - Iterate through the nodes updating the depth in place 271 for (const info of uiTrackIdToInfo.values()) { 272 if (info === null) { 273 continue; 274 } 275 const r = await this.args.engine.query(` 276 SELECT 277 id, 278 layout_depth as depth 279 FROM 280 experimental_slice_layout 281 WHERE 282 filter_track_ids = '${info.siblingTrackIds.join(',')}' 283 AND id in (${info.sliceIds.join(', ')}) 284 `); 285 286 // Create the sliceId -> new depth map: 287 const it = r.iter({ 288 id: NUM, 289 depth: NUM, 290 }); 291 const sliceIdToDepth = new Map<number, number>(); 292 for (; it.valid(); it.next()) { 293 sliceIdToDepth.set(it.id, it.depth); 294 } 295 296 // For each begin/end from an async track update the depth: 297 for (const node of info.nodes) { 298 const newDepth = sliceIdToDepth.get(node.sliceId); 299 if (newDepth !== undefined) { 300 node.depth = newDepth; 301 } 302 } 303 } 304 305 callback(flows); 306 } 307 308 sliceSelected(sliceId: number) { 309 const connectedFlows = SHOW_INDIRECT_PRECEDING_FLOWS_FLAG.get() 310 ? `( 311 select * from directly_connected_flow(${sliceId}) 312 union 313 select * from preceding_flow(${sliceId}) 314 )` 315 : `directly_connected_flow(${sliceId})`; 316 317 const query = ` 318 select 319 f.slice_out as beginSliceId, 320 t1.track_id as beginTrackId, 321 t1.name as beginSliceName, 322 CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName, 323 t1.category as beginSliceCategory, 324 t1.ts as beginSliceStartTs, 325 (t1.ts+t1.dur) as beginSliceEndTs, 326 t1.depth as beginDepth, 327 (thread_out.name || ' ' || thread_out.tid) as beginThreadName, 328 (process_out.name || ' ' || process_out.pid) as beginProcessName, 329 f.slice_in as endSliceId, 330 t2.track_id as endTrackId, 331 t2.name as endSliceName, 332 CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName, 333 t2.category as endSliceCategory, 334 t2.ts as endSliceStartTs, 335 (t2.ts+t2.dur) as endSliceEndTs, 336 t2.depth as endDepth, 337 (thread_in.name || ' ' || thread_in.tid) as endThreadName, 338 (process_in.name || ' ' || process_in.pid) as endProcessName, 339 extract_arg(f.arg_set_id, 'cat') as category, 340 extract_arg(f.arg_set_id, 'name') as name, 341 f.id as id, 342 slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant 343 from ${connectedFlows} f 344 join slice t1 on f.slice_out = t1.slice_id 345 join slice t2 on f.slice_in = t2.slice_id 346 left join thread_track track_out on track_out.id = t1.track_id 347 left join thread thread_out on thread_out.utid = track_out.utid 348 left join thread_track track_in on track_in.id = t2.track_id 349 left join thread thread_in on thread_in.utid = track_in.utid 350 left join process process_out on process_out.upid = thread_out.upid 351 left join process process_in on process_in.upid = thread_in.upid 352 `; 353 this.queryFlowEvents(query, (flows: Flow[]) => 354 publishConnectedFlows(flows), 355 ); 356 } 357 358 private areaSelected(area: AreaSelection) { 359 const trackIds: number[] = []; 360 361 for (const uiTrackId of area.tracks) { 362 const track = globals.state.tracks[uiTrackId]; 363 if (track?.uri !== undefined) { 364 const trackInfo = globals.trackManager.resolveTrackInfo(track.uri); 365 const kind = trackInfo?.kind; 366 if ( 367 kind === THREAD_SLICE_TRACK_KIND || 368 kind === ACTUAL_FRAMES_SLICE_TRACK_KIND 369 ) { 370 if (trackInfo?.trackIds) { 371 for (const trackId of trackInfo.trackIds) { 372 trackIds.push(trackId); 373 } 374 } 375 } 376 } 377 } 378 379 const tracks = `(${trackIds.join(',')})`; 380 381 const startNs = area.start; 382 const endNs = area.end; 383 384 const query = ` 385 select 386 f.slice_out as beginSliceId, 387 t1.track_id as beginTrackId, 388 t1.name as beginSliceName, 389 CHROME_CUSTOM_SLICE_NAME(t1.slice_id) as beginSliceChromeCustomName, 390 t1.category as beginSliceCategory, 391 t1.ts as beginSliceStartTs, 392 (t1.ts+t1.dur) as beginSliceEndTs, 393 t1.depth as beginDepth, 394 NULL as beginThreadName, 395 NULL as beginProcessName, 396 f.slice_in as endSliceId, 397 t2.track_id as endTrackId, 398 t2.name as endSliceName, 399 CHROME_CUSTOM_SLICE_NAME(t2.slice_id) as endSliceChromeCustomName, 400 t2.category as endSliceCategory, 401 t2.ts as endSliceStartTs, 402 (t2.ts+t2.dur) as endSliceEndTs, 403 t2.depth as endDepth, 404 NULL as endThreadName, 405 NULL as endProcessName, 406 extract_arg(f.arg_set_id, 'cat') as category, 407 extract_arg(f.arg_set_id, 'name') as name, 408 f.id as id, 409 slice_is_ancestor(t1.slice_id, t2.slice_id) as flowToDescendant 410 from flow f 411 join slice t1 on f.slice_out = t1.slice_id 412 join slice t2 on f.slice_in = t2.slice_id 413 where 414 (t1.track_id in ${tracks} 415 and (t1.ts+t1.dur <= ${endNs} and t1.ts+t1.dur >= ${startNs})) 416 or 417 (t2.track_id in ${tracks} 418 and (t2.ts <= ${endNs} and t2.ts >= ${startNs})) 419 `; 420 this.queryFlowEvents(query, (flows: Flow[]) => publishSelectedFlows(flows)); 421 } 422 423 refreshVisibleFlows() { 424 if (!this.monitor.ifStateChanged()) { 425 return; 426 } 427 428 const selection = globals.state.selection; 429 if (selection.kind === 'empty') { 430 publishConnectedFlows([]); 431 publishSelectedFlows([]); 432 return; 433 } 434 435 const legacySelection = getLegacySelection(globals.state); 436 // TODO(b/155483804): This is a hack as annotation slices don't contain 437 // flows. We should tidy this up when fixing this bug. 438 if ( 439 legacySelection && 440 legacySelection.kind === 'SLICE' && 441 legacySelection.table !== 'annotation' 442 ) { 443 this.sliceSelected(legacySelection.id); 444 } else { 445 publishConnectedFlows([]); 446 } 447 448 if (selection.kind === 'area') { 449 this.areaSelected(selection); 450 } else { 451 publishSelectedFlows([]); 452 } 453 } 454 455 run() { 456 this.refreshVisibleFlows(); 457 } 458} 459