1/*
2 * Copyright (C) 2022 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, Input} from '@angular/core';
18import {TimelineUtils} from 'app/components/timeline/timeline_utils';
19import {assertDefined, assertTrue} from 'common/assert_utils';
20import {Point} from 'common/geometry/point';
21import {Rect} from 'common/geometry/rect';
22import {TimeRange, Timestamp} from 'common/time';
23import {AbsoluteEntryIndex, Trace, TraceEntry} from 'trace/trace';
24import {TraceType} from 'trace/trace_type';
25import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
26import {AbstractTimelineRowComponent} from './abstract_timeline_row_component';
27
28@Component({
29  selector: 'transition-timeline',
30  template: `
31    <div
32      class="transition-timeline"
33      matTooltip="Some or all transitions will not be rendered in timeline due to unknown dispatch and finish or abort time"
34      [matTooltipDisabled]="shouldNotRenderEntries.length === 0"
35      [style.background-color]="getBackgroundColor()"
36      (click)="onTimelineClick($event)"
37      #wrapper>
38      <canvas
39        id="canvas"
40        (mousemove)="trackMousePos($event)"
41        (mouseleave)="onMouseLeave($event)" #canvas></canvas>
42    </div>
43  `,
44  styles: [
45    `
46      .transition-timeline {
47        height: 4rem;
48      }
49      .transition-timeline:hover {
50        background-color: var(--hover-element-color);
51        cursor: pointer;
52      }
53    `,
54  ],
55})
56export class TransitionTimelineComponent extends AbstractTimelineRowComponent<PropertyTreeNode> {
57  @Input() selectedEntry: TraceEntry<PropertyTreeNode> | undefined;
58  @Input() trace: Trace<PropertyTreeNode> | undefined;
59  @Input() transitionEntries: Array<PropertyTreeNode | undefined> | undefined;
60  @Input() fullRange: TimeRange | undefined;
61
62  hoveringEntry?: TraceEntry<PropertyTreeNode>;
63  rowsToUse = new Map<number, number>();
64  maxRowsRequires = 0;
65  shouldNotRenderEntries: number[] = [];
66
67  ngOnInit() {
68    assertDefined(this.trace);
69    assertTrue(this.trace?.type === TraceType.TRANSITION);
70    assertDefined(this.selectionRange);
71    assertDefined(this.transitionEntries);
72    assertDefined(this.fullRange);
73    this.computeRowsToUse();
74  }
75
76  getAvailableWidth() {
77    return this.canvasDrawer.getScaledCanvasWidth();
78  }
79
80  override onHover(mousePoint: Point) {
81    this.drawSegmentHover(mousePoint);
82  }
83
84  override handleMouseOut(e: MouseEvent) {
85    if (this.hoveringEntry) {
86      // If undefined there is no current hover effect so no need to clear
87      this.redraw();
88    }
89    this.hoveringEntry = undefined;
90  }
91
92  override drawTimeline() {
93    let selectedRect: Rect | undefined;
94    assertDefined(this.trace).forEachEntry((entry) => {
95      const index = entry.getIndex();
96      const rect = this.getRectFromIndex(entry.getIndex());
97      if (!rect) {
98        return;
99      }
100      const transition = assertDefined(this.transitionEntries?.at(index));
101      this.drawSegment(rect, transition);
102      if (index === this.selectedEntry?.getIndex()) {
103        selectedRect = rect;
104      }
105    });
106    if (selectedRect) {
107      this.canvasDrawer.drawRectBorder(selectedRect);
108    }
109  }
110
111  private getRectFromIndex(entryIndex: AbsoluteEntryIndex): Rect | undefined {
112    if (this.shouldNotRenderEntries.includes(entryIndex)) {
113      return undefined;
114    }
115    const transition = this.transitionEntries?.at(entryIndex);
116    if (!transition) {
117      return undefined;
118    }
119    const timeRange = TimelineUtils.getTimeRangeForTransition(
120      transition,
121      assertDefined(this.selectionRange),
122      assertDefined(this.timestampConverter),
123    );
124    if (!timeRange) {
125      return undefined;
126    }
127    const row = this.getRowToUseFor(entryIndex);
128    return this.getSegmentRect(timeRange.from, timeRange.to, row);
129  }
130
131  protected override getEntryAt(
132    mousePoint: Point,
133  ): TraceEntry<PropertyTreeNode> | undefined {
134    const transitionEntries = assertDefined(this.trace).mapEntry(
135      (entry) => entry,
136    );
137
138    for (const entry of transitionEntries) {
139      const rect = this.getRectFromIndex(entry.getIndex());
140      if (rect?.containsPoint(mousePoint)) {
141        return entry;
142      }
143    }
144    return undefined;
145  }
146
147  private drawSegmentHover(mousePoint: Point) {
148    const currentHoverEntry = this.getEntryAt(mousePoint);
149
150    if (this.hoveringEntry) {
151      this.redraw();
152    }
153
154    this.hoveringEntry = currentHoverEntry;
155
156    if (!this.hoveringEntry) {
157      return;
158    }
159
160    const rect = this.getRectFromIndex(this.hoveringEntry.getIndex());
161    if (rect) {
162      this.canvasDrawer.drawRectBorder(rect);
163    }
164  }
165
166  private getXPosOf(entry: Timestamp): number {
167    const start = assertDefined(this.selectionRange).from.getValueNs();
168    const end = assertDefined(this.selectionRange).to.getValueNs();
169
170    return Number(
171      (BigInt(this.getAvailableWidth()) * (entry.getValueNs() - start)) /
172        (end - start),
173    );
174  }
175
176  private getSegmentRect(
177    start: Timestamp,
178    end: Timestamp,
179    rowToUse: number,
180  ): Rect {
181    const xPosStart = this.getXPosOf(start);
182    const selectionStart = assertDefined(this.selectionRange).from.getValueNs();
183    const selectionEnd = assertDefined(this.selectionRange).to.getValueNs();
184
185    const borderPadding = 5;
186    let totalRowHeight =
187      (this.canvasDrawer.getScaledCanvasHeight() - 2 * borderPadding) /
188      this.maxRowsRequires;
189    if (totalRowHeight < 10) {
190      totalRowHeight = 10;
191    }
192    if (this.maxRowsRequires === 1) {
193      totalRowHeight = 30;
194    }
195
196    const padding = 5;
197    const rowHeight = totalRowHeight - padding;
198
199    const width = Math.max(
200      Number(
201        (BigInt(this.getAvailableWidth()) *
202          (end.getValueNs() - start.getValueNs())) /
203          (selectionEnd - selectionStart),
204      ),
205      rowHeight,
206    );
207
208    return new Rect(
209      xPosStart,
210      borderPadding + rowToUse * totalRowHeight,
211      width,
212      rowHeight,
213    );
214  }
215
216  private drawSegment(rect: Rect, transition: PropertyTreeNode) {
217    const aborted = assertDefined(
218      transition.getChildByName('aborted'),
219    ).getValue();
220    const alpha = aborted ? 0.25 : 1.0;
221
222    const hasUnknownStart =
223      TimelineUtils.isTransitionWithUnknownStart(transition);
224    const hasUnknownEnd = TimelineUtils.isTransitionWithUnknownEnd(transition);
225    this.canvasDrawer.drawRect(
226      rect,
227      this.color,
228      alpha,
229      hasUnknownStart,
230      hasUnknownEnd,
231    );
232  }
233
234  private getRowToUseFor(entryIndex: AbsoluteEntryIndex): number {
235    const rowToUse = this.rowsToUse.get(entryIndex);
236    if (rowToUse === undefined) {
237      throw new Error(`Could not find entry ${entryIndex} in rowsToUse`);
238    }
239    return rowToUse;
240  }
241
242  private computeRowsToUse(): void {
243    const rowAvailableFrom: Array<bigint | undefined> = [];
244    assertDefined(this.trace).forEachEntry((entry) => {
245      const index = entry.getIndex();
246      const transition = this.transitionEntries?.at(entry.getIndex());
247      if (!transition) {
248        return;
249      }
250
251      const timeRange = TimelineUtils.getTimeRangeForTransition(
252        transition,
253        assertDefined(this.fullRange),
254        assertDefined(this.timestampConverter),
255      );
256
257      if (timeRange === undefined) {
258        this.shouldNotRenderEntries.push(index);
259        return;
260      }
261
262      let rowToUse = 0;
263      while ((rowAvailableFrom[rowToUse] ?? 0n) > timeRange.from.getValueNs()) {
264        rowToUse++;
265      }
266
267      rowAvailableFrom[rowToUse] = timeRange.to.getValueNs();
268
269      if (rowToUse + 1 > this.maxRowsRequires) {
270        this.maxRowsRequires = rowToUse + 1;
271      }
272      this.rowsToUse.set(index, rowToUse);
273    });
274  }
275}
276