1/* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {Component, ElementRef, Inject, Input} from '@angular/core'; 18import {TimeUtils} from 'common/time_utils'; 19import {Transition} from 'trace/flickerlib/common'; 20import {ElapsedTimestamp, TimestampType} from 'trace/timestamp'; 21import {Terminal} from 'viewers/common/ui_tree_utils'; 22import {Events} from './events'; 23import {UiData} from './ui_data'; 24 25@Component({ 26 selector: 'viewer-transitions', 27 template: ` 28 <div class="card-grid container"> 29 <div class="top-viewer"> 30 <div class="entries"> 31 <div class="table-header table-row"> 32 <div class="id">Id</div> 33 <div class="type">Type</div> 34 <div class="send-time">Send Time</div> 35 <div class="duration">Duration</div> 36 <div class="status">Status</div> 37 </div> 38 <cdk-virtual-scroll-viewport itemSize="53" class="scroll"> 39 <div 40 *cdkVirtualFor="let transition of uiData.entries; let i = index" 41 class="entry table-row" 42 [class.current]="isCurrentTransition(transition)" 43 (click)="onTransitionClicked(transition)"> 44 <div class="id"> 45 <span class="mat-body-1">{{ transition.id }}</span> 46 </div> 47 <div class="type"> 48 <span class="mat-body-1">{{ transition.type }}</span> 49 </div> 50 <div class="send-time"> 51 <span *ngIf="!transition.sendTime.isMin" class="mat-body-1">{{ 52 formattedTime(transition.sendTime, uiData.timestampType) 53 }}</span> 54 <span *ngIf="transition.sendTime.isMin"> n/a </span> 55 </div> 56 <div class="duration"> 57 <span 58 *ngIf="!transition.sendTime.isMin && !transition.finishTime.isMax" 59 class="mat-body-1" 60 >{{ 61 formattedTimeDiff( 62 transition.sendTime, 63 transition.finishTime, 64 uiData.timestampType 65 ) 66 }}</span 67 > 68 <span *ngIf="transition.sendTime.isMin || transition.finishTime.isMax">n/a</span> 69 </div> 70 <div class="status"> 71 <div *ngIf="transition.mergedInto"> 72 <span>MERGED</span> 73 <mat-icon aria-hidden="false" fontIcon="merge" matTooltip="merged" icon-gray> 74 </mat-icon> 75 </div> 76 77 <div *ngIf="transition.aborted && !transition.mergedInto"> 78 <span>ABORTED</span> 79 <mat-icon 80 aria-hidden="false" 81 fontIcon="close" 82 matTooltip="aborted" 83 style="color: red" 84 icon-red></mat-icon> 85 </div> 86 87 <div *ngIf="transition.played && !transition.aborted && !transition.mergedInto"> 88 <span>PLAYED</span> 89 <mat-icon 90 aria-hidden="false" 91 fontIcon="check" 92 matTooltip="played" 93 style="color: green" 94 *ngIf=" 95 transition.played && !transition.aborted && !transition.mergedInto 96 "></mat-icon> 97 </div> 98 </div> 99 </div> 100 </cdk-virtual-scroll-viewport> 101 </div> 102 103 <mat-divider [vertical]="true"></mat-divider> 104 105 <div class="container-properties"> 106 <h3 class="properties-title mat-title">Selected Transition</h3> 107 <tree-view 108 [item]="uiData.selectedTransitionPropertiesTree" 109 [showNode]="showNode" 110 [isLeaf]="isLeaf" 111 [isAlwaysCollapsed]="true"> 112 </tree-view> 113 <div *ngIf="!uiData.selectedTransitionPropertiesTree"> 114 No selected transition.<br /> 115 Select the tranitions below. 116 </div> 117 </div> 118 </div> 119 120 <div class="bottom-viewer"> 121 <div class="transition-timeline"> 122 <div *ngFor="let row of timelineRows()" class="row"> 123 <svg width="100%" [attr.height]="transitionHeight"> 124 <rect 125 *ngFor="let transition of transitionsOnRow(row)" 126 [attr.width]="widthOf(transition)" 127 [attr.height]="transitionHeight" 128 [attr.style]="transitionRectStyle(transition)" 129 rx="5" 130 [attr.x]="startOf(transition)" 131 (click)="onTransitionClicked(transition)" /> 132 <rect 133 *ngFor="let transition of transitionsOnRow(row)" 134 [attr.width]="transitionDividerWidth" 135 [attr.height]="transitionHeight" 136 [attr.style]="transitionDividerRectStyle(transition)" 137 [attr.x]="sendOf(transition)" /> 138 </svg> 139 </div> 140 </div> 141 </div> 142 </div> 143 `, 144 styles: [ 145 ` 146 .container { 147 display: flex; 148 flex-grow: 1; 149 flex-direction: column; 150 } 151 152 .top-viewer { 153 display: flex; 154 flex-grow: 1; 155 flex: 3; 156 border-bottom: solid 1px rgba(0, 0, 0, 0.12); 157 } 158 159 .bottom-viewer { 160 display: flex; 161 flex-shrink: 1; 162 } 163 164 .transition-timeline { 165 flex-grow: 1; 166 padding: 1.5rem 1rem; 167 } 168 169 .entries { 170 flex: 3; 171 display: flex; 172 flex-direction: column; 173 padding: 16px; 174 } 175 176 .container-properties { 177 flex: 1; 178 padding: 16px; 179 } 180 181 .entries .scroll { 182 height: 100%; 183 } 184 185 .entries .table-header { 186 flex: 1; 187 } 188 189 .table-row { 190 display: flex; 191 flex-direction: row; 192 cursor: pointer; 193 border-bottom: solid 1px rgba(0, 0, 0, 0.12); 194 } 195 196 .table-header.table-row { 197 font-weight: bold; 198 border-bottom: solid 1px rgba(0, 0, 0, 0.5); 199 } 200 201 .scroll .entry.current { 202 color: white; 203 background-color: #365179; 204 } 205 206 .table-row > div { 207 padding: 16px; 208 } 209 210 .table-row .id { 211 flex: 1; 212 } 213 214 .table-row .type { 215 flex: 2; 216 } 217 218 .table-row .send-time { 219 flex: 4; 220 } 221 222 .table-row .duration { 223 flex: 3; 224 } 225 226 .table-row .status { 227 flex: 2; 228 } 229 230 .status > div { 231 display: flex; 232 justify-content: center; 233 align-items: center; 234 gap: 5px; 235 } 236 237 .current .status mat-icon { 238 color: white !important; 239 } 240 241 .transition-timeline .row svg rect { 242 cursor: pointer; 243 } 244 245 .label { 246 width: 300px; 247 padding: 1rem; 248 } 249 250 .lines { 251 flex-grow: 1; 252 padding: 0.5rem; 253 } 254 255 .selected-transition { 256 padding: 1rem; 257 border-bottom: solid 1px rgba(0, 0, 0, 0.12); 258 flex-grow: 1; 259 } 260 `, 261 ], 262}) 263export class ViewerTransitionsComponent { 264 transitionHeight = '20px'; 265 transitionDividerWidth = '3px'; 266 267 constructor(@Inject(ElementRef) elementRef: ElementRef) { 268 this.elementRef = elementRef; 269 } 270 271 @Input() 272 set inputData(data: UiData) { 273 this.uiData = data; 274 } 275 276 getMinOfRanges(): bigint { 277 if (this.uiData.entries.length === 0) { 278 return 0n; 279 } 280 const minOfRange = bigIntMin( 281 ...this.uiData.entries 282 .filter((it) => !it.createTime.isMin) 283 .map((it) => BigInt(it.createTime.elapsedNanos.toString())) 284 ); 285 return minOfRange; 286 } 287 288 getMaxOfRanges(): bigint { 289 if (this.uiData.entries.length === 0) { 290 return 0n; 291 } 292 const maxOfRange = bigIntMax( 293 ...this.uiData.entries 294 .filter((it) => !it.finishTime.isMax) 295 .map((it) => BigInt(it.finishTime.elapsedNanos.toString())) 296 ); 297 return maxOfRange; 298 } 299 300 formattedTime(time: any, timestampType: TimestampType): string { 301 return TimeUtils.formattedKotlinTimestamp(time, timestampType); 302 } 303 304 formattedTimeDiff(time1: any, time2: any, timestampType: TimestampType): string { 305 const timeDiff = new ElapsedTimestamp( 306 BigInt(time2.elapsedNanos.toString()) - BigInt(time1.elapsedNanos.toString()) 307 ); 308 return TimeUtils.format(timeDiff); 309 } 310 311 widthOf(transition: Transition) { 312 const fullRange = this.getMaxOfRanges() - this.getMinOfRanges(); 313 314 let finish = BigInt(transition.finishTime.elapsedNanos.toString()); 315 if (transition.finishTime.elapsedNanos.isMax) { 316 finish = this.getMaxOfRanges(); 317 } 318 319 let start = BigInt(transition.createTime.elapsedNanos.toString()); 320 if (transition.createTime.elapsedNanos.isMin) { 321 start = this.getMinOfRanges(); 322 } 323 324 const minWidthPercent = 0.5; 325 return `${Math.max(minWidthPercent, Number((finish - start) * 100n) / Number(fullRange))}%`; 326 } 327 328 startOf(transition: Transition) { 329 const fullRange = this.getMaxOfRanges() - this.getMinOfRanges(); 330 return `${ 331 Number( 332 (BigInt(transition.createTime.elapsedNanos.toString()) - this.getMinOfRanges()) * 100n 333 ) / Number(fullRange) 334 }%`; 335 } 336 337 sendOf(transition: Transition) { 338 const fullRange = this.getMaxOfRanges() - this.getMinOfRanges(); 339 return `${ 340 Number((BigInt(transition.sendTime.elapsedNanos.toString()) - this.getMinOfRanges()) * 100n) / 341 Number(fullRange) 342 }%`; 343 } 344 345 onTransitionClicked(transition: Transition): void { 346 this.emitEvent(Events.TransitionSelected, transition); 347 } 348 349 transitionRectStyle(transition: Transition): string { 350 if (this.uiData.selectedTransition === transition) { 351 return 'fill:rgb(0, 0, 230)'; 352 } else if (transition.aborted) { 353 return 'fill:rgb(255, 0, 0)'; 354 } else { 355 return 'fill:rgb(78, 205, 230)'; 356 } 357 } 358 359 transitionDividerRectStyle(transition: Transition): string { 360 return 'fill:rgb(255, 0, 0)'; 361 } 362 363 showNode(item: any) { 364 return ( 365 !(item instanceof Terminal) && 366 !(item.name instanceof Terminal) && 367 !(item.propertyKey instanceof Terminal) 368 ); 369 } 370 371 isLeaf(item: any) { 372 return ( 373 !item.children || 374 item.children.length === 0 || 375 item.children.filter((c: any) => !(c instanceof Terminal)).length === 0 376 ); 377 } 378 379 isCurrentTransition(transition: Transition): boolean { 380 return this.uiData.selectedTransition === transition; 381 } 382 383 assignRowsToTransitions(): Map<Transition, number> { 384 const fullRange = this.getMaxOfRanges() - this.getMinOfRanges(); 385 const assignedRows = new Map<Transition, number>(); 386 387 const sortedTransitions = [...this.uiData.entries].sort((t1, t2) => { 388 const diff = 389 BigInt(t1.createTime.elapsedNanos.toString()) - 390 BigInt(t2.createTime.elapsedNanos.toString()); 391 if (diff < 0) { 392 return -1; 393 } 394 if (diff > 0) { 395 return 1; 396 } 397 return 0; 398 }); 399 400 const rowFirstAvailableTime = new Map<number, bigint>(); 401 let rowsUsed = 1; 402 rowFirstAvailableTime.set(0, 0n); 403 404 for (const transition of sortedTransitions) { 405 const start = BigInt(transition.createTime.elapsedNanos.toString()); 406 const end = BigInt(transition.finishTime.elapsedNanos.toString()); 407 408 let rowIndexWithSpace = undefined; 409 for (let rowIndex = 0; rowIndex < rowsUsed; rowIndex++) { 410 if (start > rowFirstAvailableTime.get(rowIndex)!) { 411 // current row has space 412 rowIndexWithSpace = rowIndex; 413 break; 414 } 415 } 416 417 if (rowIndexWithSpace === undefined) { 418 rowIndexWithSpace = rowsUsed; 419 rowsUsed++; 420 } 421 422 assignedRows.set(transition, rowIndexWithSpace); 423 424 const minimumPaddingBetweenEntries = fullRange / 100n; 425 426 rowFirstAvailableTime.set(rowIndexWithSpace, end + minimumPaddingBetweenEntries); 427 } 428 429 return assignedRows; 430 } 431 432 timelineRows(): number[] { 433 return [...new Set(this.assignRowsToTransitions().values())]; 434 } 435 436 transitionsOnRow(row: number): Transition[] { 437 const transitions = []; 438 const assignedRows = this.assignRowsToTransitions(); 439 440 for (const transition of assignedRows.keys()) { 441 if (row === assignedRows.get(transition)) { 442 transitions.push(transition); 443 } 444 } 445 446 return transitions; 447 } 448 449 rowsRequiredForTransitions(): number { 450 return Math.max(...this.assignRowsToTransitions().values()); 451 } 452 453 private emitEvent(event: string, data: any) { 454 const customEvent = new CustomEvent(event, { 455 bubbles: true, 456 detail: data, 457 }); 458 this.elementRef.nativeElement.dispatchEvent(customEvent); 459 } 460 461 uiData: UiData = UiData.EMPTY; 462 private elementRef: ElementRef; 463} 464 465const bigIntMax = (...args: Array<bigint>) => args.reduce((m, e) => (e > m ? e : m)); 466const bigIntMin = (...args: Array<bigint>) => args.reduce((m, e) => (e < m ? e : m)); 467