• 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 {searchSegment} from '../../base/binary_search';
16import {assertTrue} from '../../base/logging';
17import {TrackState} from '../../common/state';
18import {checkerboardExcept} from '../../frontend/checkerboard';
19import {globals} from '../../frontend/globals';
20import {Track} from '../../frontend/track';
21import {trackRegistry} from '../../frontend/track_registry';
22
23import {
24  Config,
25  COUNTER_TRACK_KIND,
26  Data,
27} from './common';
28
29// 0.5 Makes the horizontal lines sharp.
30const MARGIN_TOP = 4.5;
31const RECT_HEIGHT = 30;
32
33class CounterTrack extends Track<Config, Data> {
34  static readonly kind = COUNTER_TRACK_KIND;
35  static create(trackState: TrackState): CounterTrack {
36    return new CounterTrack(trackState);
37  }
38
39  private mouseXpos = 0;
40  private hoveredValue: number|undefined = undefined;
41  private hoveredTs: number|undefined = undefined;
42  private hoveredTsEnd: number|undefined = undefined;
43
44  constructor(trackState: TrackState) {
45    super(trackState);
46  }
47
48  renderCanvas(ctx: CanvasRenderingContext2D): void {
49    // TODO: fonts and colors should come from the CSS and not hardcoded here.
50    const {timeScale, visibleWindowTime} = globals.frontendLocalState;
51    const data = this.data();
52
53    // If there aren't enough cached slices data in |data| request more to
54    // the controller.
55    const inRange = data !== undefined &&
56        (visibleWindowTime.start >= data.start &&
57         visibleWindowTime.end <= data.end);
58    if (!inRange || data === undefined ||
59        data.resolution !== globals.getCurResolution()) {
60      globals.requestTrackData(this.trackState.id);
61    }
62    if (data === undefined) return;  // Can't possibly draw anything.
63
64    assertTrue(data.timestamps.length === data.values.length);
65
66    const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start));
67    const endPx = Math.floor(timeScale.timeToPx(visibleWindowTime.end));
68    const zeroY = MARGIN_TOP + RECT_HEIGHT / (data.minimumValue < 0 ? 2 : 1);
69
70    let lastX = startPx;
71    let lastY = zeroY;
72
73    // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
74    const maxValue = Math.max(data.maximumValue, 0);
75
76    let yMax = Math.max(Math.abs(data.minimumValue), maxValue);
77    const kUnits = ['', 'K', 'M', 'G', 'T', 'E'];
78    const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
79    const pow10 = Math.pow(10, exp);
80    yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4);
81    const yRange = data.minimumValue < 0 ? yMax * 2 : yMax;
82    const unitGroup = Math.floor(exp / 3);
83    const yLabel = `${yMax / Math.pow(10, unitGroup * 3)} ${kUnits[unitGroup]}`;
84    // There are 360deg of hue. We want a scale that starts at green with
85    // exp <= 3 (<= 1KB), goes orange around exp = 6 (~1MB) and red/violet
86    // around exp >= 9 (1GB).
87    // The hue scale looks like this:
88    // 0                              180                                 360
89    // Red        orange         green | blue         purple          magenta
90    // So we want to start @ 180deg with pow=0, go down to 0deg and then wrap
91    // back from 360deg back to 180deg.
92    const expCapped = Math.min(Math.max(exp - 3), 9);
93    const hue = (180 - Math.floor(expCapped * (180 / 6)) + 360) % 360;
94
95    ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
96    ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
97    ctx.beginPath();
98    ctx.moveTo(lastX, lastY);
99    for (let i = 0; i < data.values.length; i++) {
100      const value = data.values[i];
101      const startTime = data.timestamps[i];
102      const nextY = zeroY - Math.round((value / yRange) * RECT_HEIGHT);
103      if (nextY === lastY) continue;
104
105      lastX = Math.floor(timeScale.timeToPx(startTime));
106      ctx.lineTo(lastX, lastY);
107      ctx.lineTo(lastX, nextY);
108      lastY = nextY;
109    }
110    ctx.lineTo(endPx, lastY);
111    ctx.lineTo(endPx, zeroY);
112    ctx.closePath();
113    ctx.fill();
114    ctx.stroke();
115
116    // Draw the Y=0 dashed line.
117    ctx.strokeStyle = `hsl(${hue}, 10%, 15%)`;
118    ctx.beginPath();
119    ctx.setLineDash([2, 4]);
120    ctx.moveTo(0, zeroY);
121    ctx.lineTo(endPx, zeroY);
122    ctx.closePath();
123    ctx.stroke();
124    ctx.setLineDash([]);
125
126    ctx.font = '10px Google Sans';
127
128    if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) {
129      // TODO(hjd): Add units.
130      let text = (data.isQuantized) ? 'max value: ' : 'value: ';
131      text += `${this.hoveredValue.toLocaleString()}`;
132      const width = ctx.measureText(text).width;
133
134      ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
135      ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
136
137      const xStart = Math.floor(timeScale.timeToPx(this.hoveredTs));
138      const xEnd = this.hoveredTsEnd === undefined ?
139          endPx :
140          Math.floor(timeScale.timeToPx(this.hoveredTsEnd));
141      const y = zeroY - Math.round((this.hoveredValue / yRange) * RECT_HEIGHT);
142
143      // Highlight line.
144      ctx.beginPath();
145      ctx.moveTo(xStart, y);
146      ctx.lineTo(xEnd, y);
147      ctx.lineWidth = 3;
148      ctx.stroke();
149      ctx.lineWidth = 1;
150
151      // Draw change marker.
152      ctx.beginPath();
153      ctx.arc(xStart, y, 3 /*r*/, 0 /*start angle*/, 2 * Math.PI /*end angle*/);
154      ctx.fill();
155      ctx.stroke();
156
157      // Draw the tooltip.
158      ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
159      ctx.fillRect(this.mouseXpos + 5, MARGIN_TOP, width + 16, RECT_HEIGHT);
160      ctx.fillStyle = 'hsl(200, 50%, 40%)';
161      ctx.textAlign = 'left';
162      ctx.textBaseline = 'middle';
163      ctx.fillText(text, this.mouseXpos + 8, MARGIN_TOP + RECT_HEIGHT / 2);
164    }
165
166    // Write the Y scale on the top left corner.
167    ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
168    ctx.fillRect(0, 0, 40, 16);
169    ctx.fillStyle = '#666';
170    ctx.textAlign = 'left';
171    ctx.textBaseline = 'alphabetic';
172    ctx.fillText(`${yLabel}`, 5, 14);
173
174    // If the cached trace slices don't fully cover the visible time range,
175    // show a gray rectangle with a "Loading..." label.
176    checkerboardExcept(
177        ctx,
178        timeScale.timeToPx(visibleWindowTime.start),
179        timeScale.timeToPx(visibleWindowTime.end),
180        timeScale.timeToPx(data.start),
181        timeScale.timeToPx(data.end));
182  }
183
184  onMouseMove({x}: {x: number, y: number}) {
185    const data = this.data();
186    if (data === undefined) return;
187    this.mouseXpos = x;
188    const {timeScale} = globals.frontendLocalState;
189    const time = timeScale.pxToTime(x);
190
191    const [left, right] = searchSegment(data.timestamps, time);
192    this.hoveredTs = left === -1 ? undefined : data.timestamps[left];
193    this.hoveredTsEnd = right === -1 ? undefined : data.timestamps[right];
194    this.hoveredValue = left === -1 ? undefined : data.values[left];
195
196    // for (let i = 0; i < data.values.length; i++) {
197    //  if (data.timestamps[i] > time) break;
198    //  this.hoveredTs = data.timestamps[i];
199    //  this.hoveredValue = data.values[i];
200    //}
201  }
202
203  onMouseOut() {
204    this.hoveredValue = undefined;
205    this.hoveredTs = undefined;
206  }
207}
208
209trackRegistry.register(CounterTrack);
210