1// Copyright (C) 2020 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 {hsluvToHex} from 'hsluv';
16
17import {searchSegment} from '../../base/binary_search';
18import {Actions} from '../../common/actions';
19import {hslForSlice} from '../../common/colorizer';
20import {TrackState} from '../../common/state';
21import {fromNs, toNs} from '../../common/time';
22import {globals} from '../../frontend/globals';
23import {TimeScale} from '../../frontend/time_scale';
24import {Track} from '../../frontend/track';
25import {trackRegistry} from '../../frontend/track_registry';
26
27import {Config, CPU_PROFILE_TRACK_KIND, Data} from './common';
28
29const MARGIN_TOP = 4.5;
30const RECT_HEIGHT = 30.5;
31
32function colorForSample(callsiteId: number, isHovered: boolean): string {
33  return hsluvToHex(hslForSlice(String(callsiteId), isHovered));
34}
35
36class CpuProfileTrack extends Track<Config, Data> {
37  static readonly kind = CPU_PROFILE_TRACK_KIND;
38  static create(trackState: TrackState): CpuProfileTrack {
39    return new CpuProfileTrack(trackState);
40  }
41
42  private centerY = this.getHeight() / 2;
43  private markerWidth = (this.getHeight() - MARGIN_TOP) / 2;
44  private hoveredTs: number|undefined = undefined;
45
46  constructor(trackState: TrackState) {
47    super(trackState);
48  }
49
50  getHeight() {
51    return MARGIN_TOP + RECT_HEIGHT - 1;
52  }
53
54  renderCanvas(ctx: CanvasRenderingContext2D): void {
55    const {
56      timeScale,
57    } = globals.frontendLocalState;
58    const data = this.data();
59
60    if (data === undefined) return;
61
62    for (let i = 0; i < data.tsStarts.length; i++) {
63      const centerX = data.tsStarts[i];
64      const selection = globals.state.currentSelection;
65      const isHovered = this.hoveredTs === centerX;
66      const isSelected = selection !== null &&
67          selection.kind === 'CPU_PROFILE_SAMPLE' && selection.ts === centerX;
68      const strokeWidth = isSelected ? 3 : 0;
69      this.drawMarker(
70          ctx,
71          timeScale.timeToPx(fromNs(centerX)),
72          this.centerY,
73          isHovered,
74          strokeWidth,
75          data.callsiteId[i]);
76    }
77  }
78
79  drawMarker(
80      ctx: CanvasRenderingContext2D, x: number, y: number, isHovered: boolean,
81      strokeWidth: number, callsiteId: number): void {
82    ctx.beginPath();
83    ctx.moveTo(x - this.markerWidth, y - this.markerWidth);
84    ctx.lineTo(x, y + this.markerWidth);
85    ctx.lineTo(x + this.markerWidth, y - this.markerWidth);
86    ctx.lineTo(x - this.markerWidth, y - this.markerWidth);
87    ctx.closePath();
88    ctx.fillStyle = colorForSample(callsiteId, isHovered);
89    ctx.fill();
90    if (strokeWidth > 0) {
91      ctx.strokeStyle = colorForSample(callsiteId, false);
92      ctx.lineWidth = strokeWidth;
93      ctx.stroke();
94    }
95  }
96
97  onMouseMove({x, y}: {x: number, y: number}) {
98    const data = this.data();
99    if (data === undefined) return;
100    const {timeScale} = globals.frontendLocalState;
101    const time = toNs(timeScale.pxToTime(x));
102    const [left, right] = searchSegment(data.tsStarts, time);
103    const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
104    this.hoveredTs = index === -1 ? undefined : data.tsStarts[index];
105  }
106
107  onMouseOut() {
108    this.hoveredTs = undefined;
109  }
110
111  onMouseClick({x, y}: {x: number, y: number}) {
112    const data = this.data();
113    if (data === undefined) return false;
114    const {timeScale} = globals.frontendLocalState;
115
116    const time = toNs(timeScale.pxToTime(x));
117    const [left, right] = searchSegment(data.tsStarts, time);
118
119    const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
120
121    if (index !== -1) {
122      const id = data.ids[index];
123      const ts = data.tsStarts[index];
124
125      globals.makeSelection(
126          Actions.selectCpuProfileSample({id, utid: this.config.utid, ts}));
127      return true;
128    }
129    return false;
130  }
131
132  // If the markers overlap the rightmost one will be selected.
133  findTimestampIndex(
134      left: number, timeScale: TimeScale, data: Data, x: number, y: number,
135      right: number): number {
136    let index = -1;
137    if (left !== -1) {
138      const centerX = timeScale.timeToPx(fromNs(data.tsStarts[left]));
139      if (this.isInMarker(x, y, centerX)) {
140        index = left;
141      }
142    }
143    if (right !== -1) {
144      const centerX = timeScale.timeToPx(fromNs(data.tsStarts[right]));
145      if (this.isInMarker(x, y, centerX)) {
146        index = right;
147      }
148    }
149    return index;
150  }
151
152  isInMarker(x: number, y: number, centerX: number) {
153    return Math.abs(x - centerX) + Math.abs(y - this.centerY) <=
154        this.markerWidth;
155  }
156}
157
158trackRegistry.register(CpuProfileTrack);
159