• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 * as m from 'mithril';
16
17import {searchSegment} from '../../base/binary_search';
18import {assertTrue} from '../../base/logging';
19import {Actions} from '../../common/actions';
20import {toNs} from '../../common/time';
21import {checkerboardExcept} from '../../frontend/checkerboard';
22import {globals} from '../../frontend/globals';
23import {NewTrackArgs, Track} from '../../frontend/track';
24import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
25import {trackRegistry} from '../../frontend/track_registry';
26
27import {
28  Config,
29  COUNTER_TRACK_KIND,
30  CounterScaleOptions,
31  Data,
32} from './common';
33
34// 0.5 Makes the horizontal lines sharp.
35const MARGIN_TOP = 3.5;
36const RECT_HEIGHT = 24.5;
37
38interface CounterScaleAttribute {
39  follower: CounterScaleOptions;
40  tooltip: string;
41  icon: string;
42}
43
44function scaleTooltip(scale?: CounterScaleOptions): string {
45  const description: CounterScaleAttribute = getCounterScaleAttribute(scale);
46  const source: string = description.tooltip;
47  const destination: string =
48      getCounterScaleAttribute(description.follower).tooltip;
49  return `Toggle scale from ${source} to ${destination}`;
50}
51
52function scaleIcon(scale?: CounterScaleOptions): string {
53  return getCounterScaleAttribute(scale).icon;
54}
55
56function nextScale(scale?: CounterScaleOptions): CounterScaleOptions {
57  return getCounterScaleAttribute(scale).follower;
58}
59
60function getCounterScaleAttribute(scale?: CounterScaleOptions):
61    CounterScaleAttribute {
62  switch (scale) {
63    case 'MIN_MAX':
64      return {
65        follower: 'DELTA_FROM_PREVIOUS',
66        tooltip: 'min/max',
67        icon: 'show_chart'
68      };
69    case 'DELTA_FROM_PREVIOUS':
70      return {follower: 'ZERO_BASED', tooltip: 'delta', icon: 'bar_chart'};
71    case 'ZERO_BASED':
72    default:
73      return {
74        follower: 'MIN_MAX',
75        tooltip: 'zero based',
76        icon: 'waterfall_chart'
77      };
78  }
79}
80
81class CounterTrack extends Track<Config, Data> {
82  static readonly kind = COUNTER_TRACK_KIND;
83  static create(args: NewTrackArgs): CounterTrack {
84    return new CounterTrack(args);
85  }
86
87  private mousePos = {x: 0, y: 0};
88  private hoveredValue: number|undefined = undefined;
89  private hoveredTs: number|undefined = undefined;
90  private hoveredTsEnd: number|undefined = undefined;
91
92  constructor(args: NewTrackArgs) {
93    super(args);
94  }
95
96  getHeight() {
97    return MARGIN_TOP + RECT_HEIGHT;
98  }
99
100  getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
101    const buttons: Array<m.Vnode<TrackButtonAttrs>> = [];
102    buttons.push(m(TrackButton, {
103      action: () => {
104        this.config.scale = nextScale(this.config.scale);
105        Actions.updateTrackConfig(
106            {id: this.trackState.id, config: this.config});
107        globals.rafScheduler.scheduleFullRedraw();
108      },
109      i: scaleIcon(this.config.scale),
110      tooltip: scaleTooltip(this.config.scale),
111      showButton: !!this.config.scale && this.config.scale !== 'ZERO_BASED',
112    }));
113    return buttons;
114  }
115
116  renderCanvas(ctx: CanvasRenderingContext2D): void {
117    // TODO: fonts and colors should come from the CSS and not hardcoded here.
118    const {timeScale, visibleWindowTime} = globals.frontendLocalState;
119    const data = this.data();
120
121    // Can't possibly draw anything.
122    if (data === undefined || data.timestamps.length === 0) {
123      return;
124    }
125
126    assertTrue(data.timestamps.length === data.minValues.length);
127    assertTrue(data.timestamps.length === data.maxValues.length);
128    assertTrue(data.timestamps.length === data.lastValues.length);
129    assertTrue(data.timestamps.length === data.totalDeltas.length);
130
131    const scale: CounterScaleOptions = this.config.scale || 'ZERO_BASED';
132
133    let minValues = data.minValues;
134    let maxValues = data.maxValues;
135    let lastValues = data.lastValues;
136    let maximumValue = data.maximumValue;
137    let minimumValue = data.minimumValue;
138    if (scale === 'DELTA_FROM_PREVIOUS') {
139      lastValues = data.totalDeltas;
140      minValues = data.totalDeltas;
141      maxValues = data.totalDeltas;
142      maximumValue = data.maximumDelta;
143      minimumValue = data.minimumDelta;
144    }
145
146    const endPx = Math.floor(timeScale.timeToPx(visibleWindowTime.end));
147    const zeroY = MARGIN_TOP + RECT_HEIGHT / (minimumValue < 0 ? 2 : 1);
148
149    // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
150    const maxValue = Math.max(maximumValue, 0);
151
152    let yMax = Math.max(Math.abs(minimumValue), maxValue);
153    const kUnits = ['', 'K', 'M', 'G', 'T', 'E'];
154    const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
155    const pow10 = Math.pow(10, exp);
156    yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4);
157    let yRange = 0;
158    const unitGroup = Math.floor(exp / 3);
159    let yMin = 0;
160    let yLabel = '';
161    if (scale === 'MIN_MAX') {
162      yRange = maximumValue - minimumValue;
163      yMin = minimumValue;
164      yLabel = 'min - max';
165    } else {
166      yRange = minimumValue < 0 ? yMax * 2 : yMax;
167      yMin = minimumValue < 0 ? -yMax : 0;
168      yLabel = `${yMax / Math.pow(10, unitGroup * 3)} ${kUnits[unitGroup]}`;
169      if (scale === 'DELTA_FROM_PREVIOUS') {
170        yLabel += '\u0394';
171      }
172    }
173
174    // There are 360deg of hue. We want a scale that starts at green with
175    // exp <= 3 (<= 1KB), goes orange around exp = 6 (~1MB) and red/violet
176    // around exp >= 9 (1GB).
177    // The hue scale looks like this:
178    // 0                              180                                 360
179    // Red        orange         green | blue         purple          magenta
180    // So we want to start @ 180deg with pow=0, go down to 0deg and then wrap
181    // back from 360deg back to 180deg.
182    const expCapped = Math.min(Math.max(exp - 3), 9);
183    const hue = (180 - Math.floor(expCapped * (180 / 6)) + 360) % 360;
184
185    ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
186    ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
187
188    const calculateX = (ts: number) => {
189      return Math.floor(timeScale.timeToPx(ts));
190    };
191    const calculateY = (value: number) => {
192      return MARGIN_TOP + RECT_HEIGHT -
193          Math.round(((value - yMin) / yRange) * RECT_HEIGHT);
194    };
195
196    ctx.beginPath();
197    ctx.moveTo(calculateX(data.timestamps[0]), zeroY);
198    let lastDrawnY = zeroY;
199    for (let i = 0; i < data.timestamps.length; i++) {
200      const x = calculateX(data.timestamps[i]);
201      const minY = calculateY(minValues[i]);
202      const maxY = calculateY(maxValues[i]);
203      const lastY = calculateY(lastValues[i]);
204
205      ctx.lineTo(x, lastDrawnY);
206      if (minY === maxY) {
207        assertTrue(lastY === minY);
208        ctx.lineTo(x, lastY);
209      } else {
210        ctx.lineTo(x, minY);
211        ctx.lineTo(x, maxY);
212        ctx.lineTo(x, lastY);
213      }
214      lastDrawnY = lastY;
215    }
216    ctx.lineTo(endPx, lastDrawnY);
217    ctx.lineTo(endPx, zeroY);
218    ctx.closePath();
219    ctx.fill();
220    ctx.stroke();
221
222    // Draw the Y=0 dashed line.
223    ctx.strokeStyle = `hsl(${hue}, 10%, 71%)`;
224    ctx.beginPath();
225    ctx.setLineDash([2, 4]);
226    ctx.moveTo(0, zeroY);
227    ctx.lineTo(endPx, zeroY);
228    ctx.closePath();
229    ctx.stroke();
230    ctx.setLineDash([]);
231
232    ctx.font = '10px Roboto Condensed';
233
234    if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) {
235      // TODO(hjd): Add units.
236      let text = scale === 'DELTA_FROM_PREVIOUS' ? 'delta: ' : 'value: ';
237      text += `${this.hoveredValue.toLocaleString()}`;
238
239      ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
240      ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
241
242      const xStart = Math.floor(timeScale.timeToPx(this.hoveredTs));
243      const xEnd = this.hoveredTsEnd === undefined ?
244          endPx :
245          Math.floor(timeScale.timeToPx(this.hoveredTsEnd));
246      const y = MARGIN_TOP + RECT_HEIGHT -
247          Math.round(((this.hoveredValue - yMin) / yRange) * RECT_HEIGHT);
248
249      // Highlight line.
250      ctx.beginPath();
251      ctx.moveTo(xStart, y);
252      ctx.lineTo(xEnd, y);
253      ctx.lineWidth = 3;
254      ctx.stroke();
255      ctx.lineWidth = 1;
256
257      // Draw change marker.
258      ctx.beginPath();
259      ctx.arc(xStart, y, 3 /*r*/, 0 /*start angle*/, 2 * Math.PI /*end angle*/);
260      ctx.fill();
261      ctx.stroke();
262
263      // Draw the tooltip.
264      this.drawTrackHoverTooltip(ctx, this.mousePos, text);
265    }
266
267    // Write the Y scale on the top left corner.
268    ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
269    ctx.fillRect(0, 0, 42, 16);
270    ctx.fillStyle = '#666';
271    ctx.textAlign = 'left';
272    ctx.textBaseline = 'alphabetic';
273    ctx.fillText(`${yLabel}`, 5, 14);
274
275    // TODO(hjd): Refactor this into checkerboardExcept
276    {
277      const endPx = timeScale.timeToPx(visibleWindowTime.end);
278      const counterEndPx =
279          Math.min(timeScale.timeToPx(this.config.endTs || Infinity), endPx);
280
281      // Grey out RHS.
282      if (counterEndPx < endPx) {
283        ctx.fillStyle = '#0000001f';
284        ctx.fillRect(counterEndPx, 0, endPx - counterEndPx, this.getHeight());
285      }
286    }
287
288    // If the cached trace slices don't fully cover the visible time range,
289    // show a gray rectangle with a "Loading..." label.
290    checkerboardExcept(
291        ctx,
292        this.getHeight(),
293        timeScale.timeToPx(visibleWindowTime.start),
294        timeScale.timeToPx(visibleWindowTime.end),
295        timeScale.timeToPx(data.start),
296        timeScale.timeToPx(data.end));
297  }
298
299  onMouseMove(pos: {x: number, y: number}) {
300    const data = this.data();
301    if (data === undefined) return;
302    this.mousePos = pos;
303    const {timeScale} = globals.frontendLocalState;
304    const time = timeScale.pxToTime(pos.x);
305
306    const values = this.config.scale === 'DELTA_FROM_PREVIOUS' ?
307        data.totalDeltas :
308        data.lastValues;
309    const [left, right] = searchSegment(data.timestamps, time);
310    this.hoveredTs = left === -1 ? undefined : data.timestamps[left];
311    this.hoveredTsEnd = right === -1 ? undefined : data.timestamps[right];
312    this.hoveredValue = left === -1 ? undefined : values[left];
313  }
314
315  onMouseOut() {
316    this.hoveredValue = undefined;
317    this.hoveredTs = undefined;
318  }
319
320  onMouseClick({x}: {x: number}) {
321    const data = this.data();
322    if (data === undefined) return false;
323    const {timeScale} = globals.frontendLocalState;
324    const time = timeScale.pxToTime(x);
325    const [left, right] = searchSegment(data.timestamps, time);
326    if (left === -1) {
327      return false;
328    } else {
329      const counterId = data.lastIds[left];
330      if (counterId === -1) return true;
331      globals.makeSelection(Actions.selectCounter({
332        leftTs: toNs(data.timestamps[left]),
333        rightTs: right !== -1 ? toNs(data.timestamps[right]) : -1,
334        id: counterId,
335        trackId: this.trackState.id
336      }));
337      return true;
338    }
339  }
340}
341
342trackRegistry.register(CounterTrack);
343