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} from 'common/assert_utils';
20import {Point} from 'common/geometry_types';
21import {Rect} from 'common/rect';
22import {Timestamp} from 'common/time';
23import {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 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() traceEntries: PropertyTreeNode[] | undefined;
60
61  hoveringEntry?: TraceEntry<PropertyTreeNode>;
62  rowsToUse = new Map<number, number>();
63  maxRowsRequires = 0;
64  shouldNotRenderEntries: number[] = [];
65
66  ngOnInit() {
67    assertDefined(this.trace);
68    assertDefined(this.selectionRange);
69    assertDefined(this.traceEntries);
70    this.processTraceEntries();
71  }
72
73  getAvailableWidth() {
74    return this.canvasDrawer.getScaledCanvasWidth();
75  }
76
77  override onHover(mousePoint: Point) {
78    this.drawSegmentHover(mousePoint);
79  }
80
81  override handleMouseOut(e: MouseEvent) {
82    if (this.hoveringEntry) {
83      // If undefined there is no current hover effect so no need to clear
84      this.redraw();
85    }
86    this.hoveringEntry = undefined;
87  }
88
89  override drawTimeline() {
90    (this.trace as Trace<PropertyTreeNode>).mapEntry((entry) => {
91      const transition = this.traceEntries?.at(entry.getIndex());
92      if (!transition) {
93        return;
94      }
95      const timeRange = TimelineUtils.getTimeRangeForTransition(
96        transition,
97        assertDefined(this.selectionRange),
98        assertDefined(this.timestampConverter),
99      );
100      if (!timeRange) {
101        return;
102      }
103      const rowToUse = this.getRowToUseFor(entry);
104      const aborted = assertDefined(
105        transition.getChildByName('aborted'),
106      ).getValue();
107      this.drawSegment(timeRange.from, timeRange.to, rowToUse, aborted);
108    });
109    this.drawSelectedTransitionEntry();
110  }
111
112  protected override getEntryAt(
113    mousePoint: Point,
114  ): TraceEntry<PropertyTreeNode> | undefined {
115    if (assertDefined(this.trace).type !== TraceType.TRANSITION) {
116      return undefined;
117    }
118
119    const transitions = assertDefined(this.trace).mapEntry((entry) => {
120      const transition = this.traceEntries?.at(entry.getIndex());
121      if (!transition) {
122        return;
123      }
124      const timeRange = TimelineUtils.getTimeRangeForTransition(
125        transition,
126        assertDefined(this.selectionRange),
127        assertDefined(this.timestampConverter),
128      );
129
130      if (!timeRange) {
131        return undefined;
132      }
133      const rowToUse = this.getRowToUseFor(entry);
134      const rect = this.getSegmentRect(timeRange.from, timeRange.to, rowToUse);
135      if (rect.containsPoint(mousePoint)) {
136        return entry;
137      }
138      return undefined;
139    });
140
141    return transitions.find((entry) => entry !== undefined);
142  }
143
144  private drawSegmentHover(mousePoint: Point) {
145    const currentHoverEntry = this.getEntryAt(mousePoint);
146
147    if (this.hoveringEntry) {
148      this.redraw();
149    }
150
151    this.hoveringEntry = currentHoverEntry;
152
153    if (!this.hoveringEntry) {
154      return;
155    }
156
157    const transition = this.traceEntries?.at(this.hoveringEntry.getIndex());
158    if (!transition) {
159      return;
160    }
161    const timeRange = TimelineUtils.getTimeRangeForTransition(
162      transition,
163      assertDefined(this.selectionRange),
164      assertDefined(this.timestampConverter),
165    );
166
167    if (!timeRange) {
168      return;
169    }
170
171    const rowToUse = this.getRowToUseFor(this.hoveringEntry);
172    const rect = this.getSegmentRect(timeRange.from, timeRange.to, rowToUse);
173    this.canvasDrawer.drawRectBorder(rect);
174  }
175
176  private getXPosOf(entry: Timestamp): number {
177    const start = assertDefined(this.selectionRange).from.getValueNs();
178    const end = assertDefined(this.selectionRange).to.getValueNs();
179
180    return Number(
181      (BigInt(this.getAvailableWidth()) * (entry.getValueNs() - start)) /
182        (end - start),
183    );
184  }
185
186  private getSegmentRect(
187    start: Timestamp,
188    end: Timestamp,
189    rowToUse: number,
190  ): Rect {
191    const xPosStart = this.getXPosOf(start);
192    const selectionStart = assertDefined(this.selectionRange).from.getValueNs();
193    const selectionEnd = assertDefined(this.selectionRange).to.getValueNs();
194
195    const width = Number(
196      (BigInt(this.getAvailableWidth()) *
197        (end.getValueNs() - start.getValueNs())) /
198        (selectionEnd - selectionStart),
199    );
200
201    const borderPadding = 5;
202    let totalRowHeight =
203      (this.canvasDrawer.getScaledCanvasHeight() - 2 * borderPadding) /
204      this.maxRowsRequires;
205    if (totalRowHeight < 10) {
206      totalRowHeight = 10;
207    }
208    if (this.maxRowsRequires === 1) {
209      totalRowHeight = 30;
210    }
211
212    const padding = 5;
213    const rowHeight = totalRowHeight - padding;
214
215    return new Rect(
216      xPosStart,
217      borderPadding + rowToUse * totalRowHeight,
218      width,
219      rowHeight,
220    );
221  }
222
223  private drawSegment(
224    start: Timestamp,
225    end: Timestamp,
226    rowToUse: number,
227    aborted: boolean,
228  ) {
229    const rect = this.getSegmentRect(start, end, rowToUse);
230    const alpha = aborted ? 0.25 : 1.0;
231    this.canvasDrawer.drawRect(rect, this.color, alpha);
232  }
233
234  private drawSelectedTransitionEntry() {
235    if (this.selectedEntry === undefined) {
236      return;
237    }
238
239    const transition = this.traceEntries?.at(this.selectedEntry.getIndex());
240    if (!transition) {
241      return;
242    }
243    const timeRange = TimelineUtils.getTimeRangeForTransition(
244      transition,
245      assertDefined(this.selectionRange),
246      assertDefined(this.timestampConverter),
247    );
248    if (!timeRange) {
249      return;
250    }
251
252    const rowIndex = this.getRowToUseFor(this.selectedEntry);
253    const rect = this.getSegmentRect(timeRange.from, timeRange.to, rowIndex);
254    const alpha = transition.getChildByName('aborted') ? 0.25 : 1.0;
255    this.canvasDrawer.drawRect(rect, this.color, alpha);
256    this.canvasDrawer.drawRectBorder(rect);
257  }
258
259  private getRowToUseFor(entry: TraceEntry<PropertyTreeNode>): number {
260    const rowToUse = this.rowsToUse.get(entry.getIndex());
261    if (rowToUse === undefined) {
262      console.error('Failed to find', entry, 'in', this.rowsToUse);
263      throw new Error('Could not find entry in rowsToUse');
264    }
265    return rowToUse;
266  }
267
268  private processTraceEntries(): void {
269    const rowAvailableFrom: Array<bigint | undefined> = [];
270    assertDefined(this.trace).mapEntry((entry) => {
271      const index = entry.getIndex();
272      const transition = this.traceEntries?.at(entry.getIndex());
273      if (!transition) {
274        return;
275      }
276
277      const timeRange = TimelineUtils.getTimeRangeForTransition(
278        transition,
279        assertDefined(this.selectionRange),
280        assertDefined(this.timestampConverter),
281      );
282
283      if (!timeRange) {
284        this.shouldNotRenderEntries.push(index);
285      }
286
287      let rowToUse = 0;
288      while (
289        (rowAvailableFrom[rowToUse] ?? 0n) >
290        (timeRange?.from.getValueNs() ??
291          assertDefined(this.selectionRange).from.getValueNs())
292      ) {
293        rowToUse++;
294      }
295
296      rowAvailableFrom[rowToUse] =
297        timeRange?.to.getValueNs() ??
298        assertDefined(this.selectionRange).to.getValueNs();
299
300      if (rowToUse + 1 > this.maxRowsRequires) {
301        this.maxRowsRequires = rowToUse + 1;
302      }
303      this.rowsToUse.set(index, rowToUse);
304    });
305  }
306}
307