• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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