1// Copyright (C) 2018 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 {Actions} from '../../common/actions';
18import {cropText, drawIncompleteSlice} from '../../common/canvas_utils';
19import {hslForSlice} from '../../common/colorizer';
20import {TRACE_MARGIN_TIME_S} from '../../common/constants';
21import {TrackState} from '../../common/state';
22import {checkerboardExcept} from '../../frontend/checkerboard';
23import {globals} from '../../frontend/globals';
24import {SliceRect, Track} from '../../frontend/track';
25import {trackRegistry} from '../../frontend/track_registry';
26
27import {Config, Data, SLICE_TRACK_KIND} from './common';
28
29const SLICE_HEIGHT = 18;
30const TRACK_PADDING = 4;
31const CHEVRON_WIDTH_PX = 10;
32const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2;
33const INNER_CHEVRON_OFFSET = -3;
34const INNER_CHEVRON_SCALE =
35    (SLICE_HEIGHT - 2 * INNER_CHEVRON_OFFSET) / SLICE_HEIGHT;
36
37export class ChromeSliceTrack extends Track<Config, Data> {
38  static readonly kind: string = SLICE_TRACK_KIND;
39  static create(trackState: TrackState): Track {
40    return new ChromeSliceTrack(trackState);
41  }
42
43  private hoveredTitleId = -1;
44
45  constructor(trackState: TrackState) {
46    super(trackState);
47  }
48
49  renderCanvas(ctx: CanvasRenderingContext2D): void {
50    // TODO: fonts and colors should come from the CSS and not hardcoded here.
51
52    const {timeScale, visibleWindowTime} = globals.frontendLocalState;
53    const data = this.data();
54
55    if (data === undefined) return;  // Can't possibly draw anything.
56
57    // If the cached trace slices don't fully cover the visible time range,
58    // show a gray rectangle with a "Loading..." label.
59    checkerboardExcept(
60        ctx,
61        this.getHeight(),
62        timeScale.timeToPx(visibleWindowTime.start),
63        timeScale.timeToPx(visibleWindowTime.end),
64        timeScale.timeToPx(data.start),
65        timeScale.timeToPx(data.end),
66    );
67
68    ctx.font = '12px Roboto Condensed';
69    ctx.textAlign = 'center';
70
71    // measuretext is expensive so we only use it once.
72    const charWidth = ctx.measureText('ACBDLqsdfg').width / 10;
73
74    // The draw of the rect on the selected slice must happen after the other
75    // drawings, otherwise it would result under another rect.
76    let drawRectOnSelected = () => {};
77
78    for (let i = 0; i < data.starts.length; i++) {
79      const tStart = data.starts[i];
80      let tEnd = data.ends[i];
81      const depth = data.depths[i];
82      const titleId = data.titles[i];
83      const sliceId = data.sliceIds[i];
84      const isInstant = data.isInstant[i];
85      const isIncomplete = data.isIncomplete[i];
86      const title = data.strings[titleId];
87      const colorOverride = data.colors && data.strings[data.colors[i]];
88      if (isIncomplete) {  // incomplete slice
89        tEnd = visibleWindowTime.end;
90      }
91
92      const rect = this.getSliceRect(tStart, tEnd, depth);
93      if (!rect || !rect.visible) {
94        continue;
95      }
96
97      const currentSelection = globals.state.currentSelection;
98      const isSelected = currentSelection &&
99          currentSelection.kind === 'CHROME_SLICE' &&
100          currentSelection.id !== undefined && currentSelection.id === sliceId;
101
102      const name = title.replace(/( )?\d+/g, '');
103      const highlighted = titleId === this.hoveredTitleId ||
104          globals.frontendLocalState.highlightedSliceId === sliceId;
105
106      const [hue, saturation, lightness] =
107          hslForSlice(name, highlighted || isSelected);
108
109      let color: string;
110      if (colorOverride === undefined) {
111        color = hsluvToHex([hue, saturation, lightness]);
112      } else {
113        color = colorOverride;
114      }
115      ctx.fillStyle = color;
116
117      // We draw instant events as upward facing chevrons starting at A:
118      //     A
119      //    ###
120      //   ##C##
121      //  ##   ##
122      // D       B
123      // Then B, C, D and back to A:
124      if (isInstant) {
125        if (isSelected) {
126          drawRectOnSelected = () => {
127            ctx.save();
128            ctx.translate(rect.left, rect.top);
129
130            // Draw outer chevron as dark border
131            ctx.save();
132            ctx.translate(0, INNER_CHEVRON_OFFSET);
133            ctx.scale(INNER_CHEVRON_SCALE, INNER_CHEVRON_SCALE);
134            ctx.fillStyle = hsluvToHex([hue, 100, 10]);
135
136            this.drawChevron(ctx);
137            ctx.restore();
138
139            // Draw inner chevron as interior
140            ctx.fillStyle = color;
141            this.drawChevron(ctx);
142
143            ctx.restore();
144          };
145        } else {
146          ctx.save();
147          ctx.translate(rect.left, rect.top);
148          this.drawChevron(ctx);
149          ctx.restore();
150        }
151        continue;
152      }
153      if (isIncomplete && rect.width > SLICE_HEIGHT / 4) {
154        drawIncompleteSlice(
155            ctx, rect.left, rect.top, rect.width, SLICE_HEIGHT, color);
156      } else {
157        ctx.fillRect(rect.left, rect.top, rect.width, SLICE_HEIGHT);
158      }
159      // Selected case
160      if (isSelected) {
161        drawRectOnSelected = () => {
162          ctx.strokeStyle = hsluvToHex([hue, 100, 10]);
163          ctx.beginPath();
164          ctx.lineWidth = 3;
165          ctx.strokeRect(
166              rect.left, rect.top - 1.5, rect.width, SLICE_HEIGHT + 3);
167          ctx.closePath();
168        };
169      }
170
171      ctx.fillStyle = lightness > 65 ? '#404040' : 'white';
172      const displayText = cropText(title, charWidth, rect.width);
173      const rectXCenter = rect.left + rect.width / 2;
174      ctx.textBaseline = "middle";
175      ctx.fillText(displayText, rectXCenter, rect.top + SLICE_HEIGHT / 2);
176    }
177    drawRectOnSelected();
178  }
179
180  drawChevron(ctx: CanvasRenderingContext2D) {
181    // Draw a chevron at a fixed location and size. Should be used with
182    // ctx.translate and ctx.scale to alter location and size.
183    ctx.beginPath();
184    ctx.moveTo(0, 0);
185    ctx.lineTo(HALF_CHEVRON_WIDTH_PX, SLICE_HEIGHT);
186    ctx.lineTo(0, SLICE_HEIGHT - HALF_CHEVRON_WIDTH_PX);
187    ctx.lineTo(-HALF_CHEVRON_WIDTH_PX, SLICE_HEIGHT);
188    ctx.lineTo(0, 0);
189    ctx.fill();
190  }
191
192  getSliceIndex({x, y}: {x: number, y: number}): number|void {
193    const data = this.data();
194    if (data === undefined) return;
195    const {timeScale} = globals.frontendLocalState;
196    if (y < TRACK_PADDING) return;
197    const instantWidthTime = timeScale.deltaPxToDuration(HALF_CHEVRON_WIDTH_PX);
198    const t = timeScale.pxToTime(x);
199    const depth = Math.floor((y - TRACK_PADDING) / SLICE_HEIGHT);
200    for (let i = 0; i < data.starts.length; i++) {
201      if (depth !== data.depths[i]) {
202        continue;
203      }
204      const tStart = data.starts[i];
205      if (data.isInstant[i]) {
206        if (Math.abs(tStart - t) < instantWidthTime) {
207          return i;
208        }
209      } else {
210        let tEnd = data.ends[i];
211        if (data.isIncomplete[i]) {
212          tEnd = globals.frontendLocalState.visibleWindowTime.end;
213        }
214        if (tStart <= t && t <= tEnd) {
215          return i;
216        }
217      }
218    }
219  }
220
221  onMouseMove({x, y}: {x: number, y: number}) {
222    this.hoveredTitleId = -1;
223    globals.frontendLocalState.setHighlightedSliceId(-1);
224    const sliceIndex = this.getSliceIndex({x, y});
225    if (sliceIndex === undefined) return;
226    const data = this.data();
227    if (data === undefined) return;
228    this.hoveredTitleId = data.titles[sliceIndex];
229    globals.frontendLocalState.setHighlightedSliceId(data.sliceIds[sliceIndex]);
230  }
231
232  onMouseOut() {
233    this.hoveredTitleId = -1;
234    globals.frontendLocalState.setHighlightedSliceId(-1);
235  }
236
237  onMouseClick({x, y}: {x: number, y: number}): boolean {
238    const sliceIndex = this.getSliceIndex({x, y});
239    if (sliceIndex === undefined) return false;
240    const data = this.data();
241    if (data === undefined) return false;
242    const sliceId = data.sliceIds[sliceIndex];
243    if (sliceId !== undefined && sliceId !== -1) {
244      globals.makeSelection(Actions.selectChromeSlice({
245        id: sliceId,
246        trackId: this.trackState.id,
247        table: this.config.namespace
248      }));
249      return true;
250    }
251    return false;
252  }
253
254  getHeight() {
255    return SLICE_HEIGHT * (this.config.maxDepth + 1) + 2 * TRACK_PADDING;
256  }
257
258  getSliceRect(tStart: number, tEnd: number, depth: number): SliceRect
259      |undefined {
260    const {timeScale, visibleWindowTime} = globals.frontendLocalState;
261    const pxEnd = timeScale.timeToPx(visibleWindowTime.end);
262    const left = Math.max(timeScale.timeToPx(tStart), -TRACE_MARGIN_TIME_S);
263    const right = Math.min(timeScale.timeToPx(tEnd), pxEnd);
264    return {
265      left,
266      width: Math.max(right - left, 1),
267      top: TRACK_PADDING + depth * SLICE_HEIGHT,
268      height: SLICE_HEIGHT,
269      visible:
270          !(tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end)
271    };
272  }
273}
274
275trackRegistry.register(ChromeSliceTrack);
276