1// Copyright (C) 2023 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 m from 'mithril'; 16import {Duration, duration, Time, time} from '../../base/time'; 17import {hasArgs, renderArguments} from '../../components/details/slice_args'; 18import {renderDetails} from '../../components/details/slice_details'; 19import { 20 getDescendantSliceTree, 21 getSlice, 22 SliceDetails, 23 SliceTreeNode, 24} from '../../components/sql_utils/slice'; 25import {asSliceSqlId, SliceSqlId} from '../../components/sql_utils/core_types'; 26import { 27 ColumnDescriptor, 28 Table, 29 TableData, 30 widgetColumn, 31} from '../../widgets/table'; 32import {TreeTable, TreeTableAttrs} from '../../components/widgets/treetable'; 33import {LONG, NUM, STR} from '../../trace_processor/query_result'; 34import {DetailsShell} from '../../widgets/details_shell'; 35import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout'; 36import {Section} from '../../widgets/section'; 37import {MultiParagraphText, TextParagraph} from '../../widgets/text_paragraph'; 38import {Tree, TreeNode} from '../../widgets/tree'; 39import { 40 EventLatencyCauseThreadTracks, 41 EventLatencyStage, 42 getCauseLink, 43 getEventLatencyCauseTracks, 44 getScrollJankCauseStage, 45} from './scroll_jank_cause_link_utils'; 46import {ScrollJankCauseMap} from './scroll_jank_cause_map'; 47import {sliceRef} from '../../components/widgets/slice'; 48import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils'; 49import {TrackEventDetailsPanel} from '../../public/details_panel'; 50import {Trace} from '../../public/trace'; 51 52// Given a node in the slice tree, return a path from root to it. 53function getPath(slice: SliceTreeNode): string[] { 54 const result: string[] = []; 55 let node: SliceTreeNode | undefined = slice; 56 while (node.parent !== undefined) { 57 result.push(node.name); 58 node = node.parent; 59 } 60 return result.reverse(); 61} 62 63// Given a slice tree node and a path, find the node following the path from 64// the given slice, or `undefined` if not found. 65function findSliceInTreeByPath( 66 slice: SliceTreeNode | undefined, 67 path: string[], 68): SliceTreeNode | undefined { 69 if (slice === undefined) { 70 return undefined; 71 } 72 let result = slice; 73 for (const segment of path) { 74 let found = false; 75 for (const child of result.children) { 76 if (child.name === segment) { 77 found = true; 78 result = child; 79 break; 80 } 81 } 82 if (!found) { 83 return undefined; 84 } 85 } 86 return result; 87} 88 89function durationDelta(value: duration, base?: duration): string { 90 if (base === undefined) { 91 return 'NULL'; 92 } 93 const delta = value - base; 94 return `${delta > 0 ? '+' : ''}${Duration.humanise(delta)}`; 95} 96 97export class EventLatencySliceDetailsPanel implements TrackEventDetailsPanel { 98 private name = ''; 99 private topEventLatencyId: SliceSqlId | undefined = undefined; 100 101 private sliceDetails?: SliceDetails; 102 private jankySlice?: { 103 ts: time; 104 dur: duration; 105 id: number; 106 causeOfJank: string; 107 }; 108 109 // Whether this stage has caused jank. This is also true for top level 110 // EventLatency slices where a descendant is a cause of jank. 111 private isJankStage = false; 112 113 // For top level EventLatency slices - if any descendant is a cause of jank, 114 // this field stores information about that descendant slice. Otherwise, this 115 // is stores information about the current stage; 116 private relevantThreadStage: EventLatencyStage | undefined; 117 private relevantThreadTracks: EventLatencyCauseThreadTracks[] = []; 118 // Stages tree for the current EventLatency. 119 private eventLatencyBreakdown?: SliceTreeNode; 120 // Stages tree for the next EventLatency. 121 private nextEventLatencyBreakdown?: SliceTreeNode; 122 // Stages tree for the prev EventLatency. 123 private prevEventLatencyBreakdown?: SliceTreeNode; 124 125 private tracksByTrackId: Map<number, string>; 126 127 constructor( 128 private readonly trace: Trace, 129 private readonly id: number, 130 ) { 131 this.tracksByTrackId = new Map<number, string>(); 132 this.trace.tracks.getAllTracks().forEach((td) => { 133 td.tags?.trackIds?.forEach((trackId) => { 134 this.tracksByTrackId.set(trackId, td.uri); 135 }); 136 }); 137 } 138 139 async load() { 140 const queryResult = await this.trace.engine.query(` 141 SELECT 142 name 143 FROM slice 144 WHERE id = ${this.id} 145 `); 146 147 const iter = queryResult.firstRow({ 148 name: STR, 149 }); 150 151 this.name = iter.name; 152 153 await this.loadSlice(); 154 await this.loadJankSlice(); 155 await this.loadRelevantThreads(); 156 await this.loadEventLatencyBreakdown(); 157 } 158 159 async loadSlice() { 160 this.sliceDetails = await getSlice( 161 this.trace.engine, 162 asSliceSqlId(this.id), 163 ); 164 this.trace.scheduleFullRedraw(); 165 } 166 167 async loadJankSlice() { 168 if (!this.sliceDetails) return; 169 // Get the id for the top-level EventLatency slice (this or parent), as 170 // this id is used in the ScrollJankV3 track to identify the corresponding 171 // janky interval. 172 if (this.sliceDetails.name === 'EventLatency') { 173 this.topEventLatencyId = this.sliceDetails.id; 174 } else { 175 this.topEventLatencyId = asSliceSqlId( 176 await this.getOldestAncestorSliceId(), 177 ); 178 } 179 180 const it = ( 181 await this.trace.engine.query(` 182 SELECT ts, dur, id, cause_of_jank as causeOfJank 183 FROM chrome_janky_frame_presentation_intervals 184 WHERE event_latency_id = ${this.topEventLatencyId}`) 185 ).iter({ 186 id: NUM, 187 ts: LONG, 188 dur: LONG, 189 causeOfJank: STR, 190 }); 191 192 if (it.valid()) { 193 this.jankySlice = { 194 id: it.id, 195 ts: Time.fromRaw(it.ts), 196 dur: Duration.fromRaw(it.dur), 197 causeOfJank: it.causeOfJank, 198 }; 199 } 200 } 201 202 async loadRelevantThreads() { 203 if (!this.sliceDetails) return; 204 if (!this.topEventLatencyId) return; 205 206 // Relevant threads should only be available on a "Janky" EventLatency 207 // slice to allow the user to jump to the possible cause of jank. 208 if (this.sliceDetails.name === 'EventLatency' && !this.jankySlice) return; 209 210 const possibleScrollJankStage = await getScrollJankCauseStage( 211 this.trace.engine, 212 this.topEventLatencyId, 213 ); 214 if (this.sliceDetails.name === 'EventLatency') { 215 this.isJankStage = true; 216 this.relevantThreadStage = possibleScrollJankStage; 217 } else { 218 if ( 219 possibleScrollJankStage && 220 this.sliceDetails.name === possibleScrollJankStage.name 221 ) { 222 this.isJankStage = true; 223 } 224 this.relevantThreadStage = { 225 name: this.sliceDetails.name, 226 eventLatencyId: this.topEventLatencyId, 227 ts: this.sliceDetails.ts, 228 dur: this.sliceDetails.dur, 229 }; 230 } 231 232 if (this.relevantThreadStage) { 233 this.relevantThreadTracks = await getEventLatencyCauseTracks( 234 this.trace.engine, 235 this.relevantThreadStage, 236 ); 237 } 238 } 239 240 async loadEventLatencyBreakdown() { 241 if (this.topEventLatencyId === undefined) { 242 return; 243 } 244 this.eventLatencyBreakdown = await getDescendantSliceTree( 245 this.trace.engine, 246 this.topEventLatencyId, 247 ); 248 249 // TODO(altimin): this should only consider EventLatencies within the same scroll. 250 const prevEventLatency = ( 251 await this.trace.engine.query(` 252 INCLUDE PERFETTO MODULE chrome.event_latency; 253 SELECT 254 id 255 FROM chrome_event_latencies 256 WHERE event_type IN ( 257 'FIRST_GESTURE_SCROLL_UPDATE', 258 'GESTURE_SCROLL_UPDATE', 259 'INERTIAL_GESTURE_SCROLL_UPDATE') 260 AND is_presented 261 AND id < ${this.topEventLatencyId} 262 ORDER BY id DESC 263 LIMIT 1 264 ; 265 `) 266 ).maybeFirstRow({id: NUM}); 267 if (prevEventLatency !== undefined) { 268 this.prevEventLatencyBreakdown = await getDescendantSliceTree( 269 this.trace.engine, 270 asSliceSqlId(prevEventLatency.id), 271 ); 272 } 273 274 const nextEventLatency = ( 275 await this.trace.engine.query(` 276 INCLUDE PERFETTO MODULE chrome.event_latency; 277 SELECT 278 id 279 FROM chrome_event_latencies 280 WHERE event_type IN ( 281 'FIRST_GESTURE_SCROLL_UPDATE', 282 'GESTURE_SCROLL_UPDATE', 283 'INERTIAL_GESTURE_SCROLL_UPDATE') 284 AND is_presented 285 AND id > ${this.topEventLatencyId} 286 ORDER BY id DESC 287 LIMIT 1; 288 `) 289 ).maybeFirstRow({id: NUM}); 290 if (nextEventLatency !== undefined) { 291 this.nextEventLatencyBreakdown = await getDescendantSliceTree( 292 this.trace.engine, 293 asSliceSqlId(nextEventLatency.id), 294 ); 295 } 296 } 297 298 private getRelevantLinks(): m.Child { 299 if (!this.sliceDetails) return undefined; 300 301 // Relevant threads should only be available on a "Janky" EventLatency 302 // slice to allow the user to jump to the possible cause of jank. 303 if ( 304 this.sliceDetails.name === 'EventLatency' && 305 !this.relevantThreadStage 306 ) { 307 return undefined; 308 } 309 310 const name = this.relevantThreadStage 311 ? this.relevantThreadStage.name 312 : this.sliceDetails.name; 313 const ts = this.relevantThreadStage 314 ? this.relevantThreadStage.ts 315 : this.sliceDetails.ts; 316 const dur = this.relevantThreadStage 317 ? this.relevantThreadStage.dur 318 : this.sliceDetails.dur; 319 const stageDetails = ScrollJankCauseMap.getEventLatencyDetails(name); 320 if (stageDetails === undefined) return undefined; 321 322 const childWidgets: m.Child[] = []; 323 childWidgets.push(m(TextParagraph, {text: stageDetails.description})); 324 325 interface RelevantThreadRow { 326 description: string; 327 tracks: EventLatencyCauseThreadTracks; 328 ts: time; 329 dur: duration; 330 } 331 332 const columns: ColumnDescriptor<RelevantThreadRow>[] = [ 333 widgetColumn<RelevantThreadRow>('Relevant Thread', (x) => 334 getCauseLink(this.trace, x.tracks, this.tracksByTrackId, x.ts, x.dur), 335 ), 336 widgetColumn<RelevantThreadRow>('Description', (x) => { 337 if (x.description === '') { 338 return x.description; 339 } else { 340 return m(TextParagraph, {text: x.description}); 341 } 342 }), 343 ]; 344 345 const trackLinks: RelevantThreadRow[] = []; 346 347 for (let i = 0; i < this.relevantThreadTracks.length; i++) { 348 const track = this.relevantThreadTracks[i]; 349 let description = ''; 350 if (i == 0 || track.thread != this.relevantThreadTracks[i - 1].thread) { 351 description = track.causeDescription; 352 } 353 trackLinks.push({ 354 description: description, 355 tracks: this.relevantThreadTracks[i], 356 ts: ts, 357 dur: dur, 358 }); 359 } 360 361 const tableData = new TableData(trackLinks); 362 363 if (trackLinks.length > 0) { 364 childWidgets.push( 365 m(Table, { 366 data: tableData, 367 columns: columns, 368 }), 369 ); 370 } 371 372 return m( 373 Section, 374 {title: this.isJankStage ? `Jank Cause: ${name}` : name}, 375 childWidgets, 376 ); 377 } 378 379 private async getOldestAncestorSliceId(): Promise<number> { 380 let eventLatencyId = -1; 381 if (!this.sliceDetails) return eventLatencyId; 382 const queryResult = await this.trace.engine.query(` 383 SELECT 384 id 385 FROM ancestor_slice(${this.sliceDetails.id}) 386 WHERE name = 'EventLatency' 387 `); 388 389 const it = queryResult.iter({ 390 id: NUM, 391 }); 392 393 for (; it.valid(); it.next()) { 394 eventLatencyId = it.id; 395 break; 396 } 397 398 return eventLatencyId; 399 } 400 401 private getLinksSection(): m.Child { 402 return m( 403 Section, 404 {title: 'Quick links'}, 405 m( 406 Tree, 407 m(TreeNode, { 408 left: this.sliceDetails 409 ? sliceRef( 410 this.sliceDetails, 411 'EventLatency in context of other Input events', 412 ) 413 : 'EventLatency in context of other Input events', 414 right: this.sliceDetails ? '' : 'N/A', 415 }), 416 this.jankySlice && 417 m(TreeNode, { 418 left: renderSliceRef({ 419 trace: this.trace, 420 id: this.jankySlice.id, 421 trackUri: JANKS_TRACK_URI, 422 title: this.jankySlice.causeOfJank, 423 }), 424 }), 425 ), 426 ); 427 } 428 429 private getBreakdownSection(): m.Child { 430 if (this.eventLatencyBreakdown === undefined) { 431 return undefined; 432 } 433 434 const attrs: TreeTableAttrs<SliceTreeNode> = { 435 rows: [this.eventLatencyBreakdown], 436 getChildren: (slice) => slice.children, 437 columns: [ 438 {name: 'Name', getData: (slice) => slice.name}, 439 {name: 'Duration', getData: (slice) => Duration.humanise(slice.dur)}, 440 { 441 name: 'vs prev', 442 getData: (slice) => 443 durationDelta( 444 slice.dur, 445 findSliceInTreeByPath( 446 this.prevEventLatencyBreakdown, 447 getPath(slice), 448 )?.dur, 449 ), 450 }, 451 { 452 name: 'vs next', 453 getData: (slice) => 454 durationDelta( 455 slice.dur, 456 findSliceInTreeByPath( 457 this.nextEventLatencyBreakdown, 458 getPath(slice), 459 )?.dur, 460 ), 461 }, 462 ], 463 }; 464 465 return m( 466 Section, 467 { 468 title: 'EventLatency Stage Breakdown', 469 }, 470 m(TreeTable<SliceTreeNode>, attrs), 471 ); 472 } 473 474 private getDescriptionText(): m.Child { 475 return m( 476 MultiParagraphText, 477 m(TextParagraph, { 478 text: `EventLatency tracks the latency of handling a given input event 479 (Scrolls, Touches, Taps, etc). Ideally from when the input was 480 read by the hardware to when it was reflected on the screen.`, 481 }), 482 m(TextParagraph, { 483 text: `Note however the concept of coalescing or terminating early. This 484 occurs when we receive multiple events or handle them quickly by 485 converting them into a different event. Such as a TOUCH_MOVE 486 being converted into a GESTURE_SCROLL_UPDATE type, or a multiple 487 GESTURE_SCROLL_UPDATE events being formed into a single frame at 488 the end of the RendererCompositorQueuingDelay.`, 489 }), 490 m(TextParagraph, { 491 text: `*Important:* On some platforms (MacOS) we do not get feedback on 492 when something is presented on the screen so the timings are only 493 accurate for what we know on a given platform.`, 494 }), 495 ); 496 } 497 498 render() { 499 if (this.sliceDetails) { 500 const slice = this.sliceDetails; 501 502 const rightSideWidgets: m.Child[] = []; 503 rightSideWidgets.push( 504 m( 505 Section, 506 {title: 'Description'}, 507 m('.div', this.getDescriptionText()), 508 ), 509 ); 510 511 const stageWidget = this.getRelevantLinks(); 512 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 513 if (stageWidget) { 514 rightSideWidgets.push(stageWidget); 515 } 516 rightSideWidgets.push(this.getLinksSection()); 517 rightSideWidgets.push(this.getBreakdownSection()); 518 519 return m( 520 DetailsShell, 521 { 522 title: 'Slice', 523 description: this.name, 524 }, 525 m( 526 GridLayout, 527 m( 528 GridLayoutColumn, 529 renderDetails(this.trace, slice), 530 hasArgs(slice.args) && 531 m( 532 Section, 533 {title: 'Arguments'}, 534 m(Tree, renderArguments(this.trace, slice.args)), 535 ), 536 ), 537 m(GridLayoutColumn, rightSideWidgets), 538 ), 539 ); 540 } else { 541 return m(DetailsShell, {title: 'Slice', description: 'Loading...'}); 542 } 543 } 544} 545