1// Copyright (C) 2019 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 {hueForCpu} from '../../common/colorizer';
18import {TrackState} from '../../common/state';
19import {checkerboardExcept} from '../../frontend/checkerboard';
20import {globals} from '../../frontend/globals';
21import {Track} from '../../frontend/track';
22import {trackRegistry} from '../../frontend/track_registry';
23
24import {
25  Config,
26  CPU_FREQ_TRACK_KIND,
27  Data,
28} from './common';
29
30// 0.5 Makes the horizontal lines sharp.
31const MARGIN_TOP = 4.5;
32const RECT_HEIGHT = 20;
33
34class CpuFreqTrack extends Track<Config, Data> {
35  static readonly kind = CPU_FREQ_TRACK_KIND;
36  static create(trackState: TrackState): CpuFreqTrack {
37    return new CpuFreqTrack(trackState);
38  }
39
40  private mouseXpos = 0;
41  private hoveredValue: number|undefined = undefined;
42  private hoveredTs: number|undefined = undefined;
43  private hoveredTsEnd: number|undefined = undefined;
44  private hoveredIdle: number|undefined = undefined;
45
46  constructor(trackState: TrackState) {
47    super(trackState);
48  }
49
50  getHeight() {
51    return MARGIN_TOP + RECT_HEIGHT;
52  }
53
54  renderCanvas(ctx: CanvasRenderingContext2D): void {
55    // TODO: fonts and colors should come from the CSS and not hardcoded here.
56    const {timeScale, visibleWindowTime} = globals.frontendLocalState;
57    const data = this.data();
58
59    if (data === undefined || data.timestamps.length === 0) {
60      // Can't possibly draw anything.
61      return;
62    }
63
64    assertTrue(data.timestamps.length === data.lastFreqKHz.length);
65    assertTrue(data.timestamps.length === data.minFreqKHz.length);
66    assertTrue(data.timestamps.length === data.maxFreqKHz.length);
67    assertTrue(data.timestamps.length === data.lastIdleValues.length);
68
69    const endPx = timeScale.timeToPx(visibleWindowTime.end);
70    const zeroY = MARGIN_TOP + RECT_HEIGHT;
71
72    // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
73    let yMax = data.maximumValue;
74    const kUnits = ['', 'K', 'M', 'G', 'T', 'E'];
75    const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
76    const pow10 = Math.pow(10, exp);
77    yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4);
78    const unitGroup = Math.floor(exp / 3);
79    const num = yMax / Math.pow(10, unitGroup * 3);
80    // The values we have for cpufreq are in kHz so +1 to unitGroup.
81    const yLabel = `${num} ${kUnits[unitGroup + 1]}Hz`;
82
83    // Draw the CPU frequency graph.
84    const hue = hueForCpu(this.config.cpu);
85    let saturation = 45;
86    if (globals.frontendLocalState.hoveredUtid !== -1) {
87      saturation = 0;
88    }
89    ctx.fillStyle = `hsl(${hue}, ${saturation}%, 70%)`;
90    ctx.strokeStyle = `hsl(${hue}, ${saturation}%, 55%)`;
91
92    const calculateX = (timestamp: number) => {
93      return Math.floor(timeScale.timeToPx(timestamp));
94    };
95    const calculateY = (value: number) => {
96      return zeroY - Math.round((value / yMax) * RECT_HEIGHT);
97    };
98
99    const [rawStartIdx,] =
100      searchSegment(data.timestamps, visibleWindowTime.start);
101    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
102
103    const [, rawEndIdx] = searchSegment(data.timestamps, visibleWindowTime.end);
104    const endIdx = rawEndIdx === -1 ? data.timestamps.length : rawEndIdx;
105
106    ctx.beginPath();
107    ctx.moveTo(Math.max(calculateX(data.timestamps[startIdx]), 0), zeroY);
108
109    let lastDrawnY = zeroY;
110    for (let i = startIdx; i < endIdx; i++) {
111      const x = calculateX(data.timestamps[i]);
112
113      const minY = calculateY(data.minFreqKHz[i]);
114      const maxY = calculateY(data.maxFreqKHz[i]);
115      const lastY = calculateY(data.lastFreqKHz[i]);
116
117      ctx.lineTo(x, lastDrawnY);
118      if (minY === maxY) {
119        assertTrue(lastY === minY);
120        ctx.lineTo(x, lastY);
121      } else {
122        ctx.lineTo(x, minY);
123        ctx.lineTo(x, maxY);
124        ctx.lineTo(x, lastY);
125      }
126      lastDrawnY = lastY;
127    }
128    // Find the end time for the last frequency event and then draw
129    // down to zero to show that we do not have data after that point.
130    const finalX = Math.min(calculateX(data.maxTsEnd), endPx);
131    ctx.lineTo(finalX, lastDrawnY);
132    ctx.lineTo(finalX, zeroY);
133    ctx.lineTo(endPx, zeroY);
134    ctx.closePath();
135    ctx.fill();
136    ctx.stroke();
137
138    // Draw CPU idle rectangles that overlay the CPU freq graph.
139    ctx.fillStyle = `rgba(240, 240, 240, 1)`;
140
141    for (let i = 0; i < data.lastIdleValues.length; i++) {
142      if (data.lastIdleValues[i] < 0) {
143        continue;
144      }
145
146      // We intentionally don't use the floor function here when computing x
147      // coordinates. Instead we use floating point which prevents flickering as
148      // we pan and zoom; this relies on the browser anti-aliasing pixels
149      // correctly.
150      const x = timeScale.timeToPx(data.timestamps[i]);
151      const xEnd = i === data.lastIdleValues.length - 1 ?
152          finalX :
153          timeScale.timeToPx(data.timestamps[i + 1]);
154
155      const width = xEnd - x;
156      const height = calculateY(data.lastFreqKHz[i]) - zeroY;
157
158      ctx.fillRect(x, zeroY, width, height);
159    }
160
161    ctx.font = '10px Roboto Condensed';
162
163    if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) {
164      let text = `${this.hoveredValue.toLocaleString()}kHz`;
165
166      ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
167      ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
168
169      const xStart = Math.floor(timeScale.timeToPx(this.hoveredTs));
170      const xEnd = this.hoveredTsEnd === undefined ?
171          endPx :
172          Math.floor(timeScale.timeToPx(this.hoveredTsEnd));
173      const y = zeroY - Math.round((this.hoveredValue / yMax) * RECT_HEIGHT);
174
175      // Highlight line.
176      ctx.beginPath();
177      ctx.moveTo(xStart, y);
178      ctx.lineTo(xEnd, y);
179      ctx.lineWidth = 3;
180      ctx.stroke();
181      ctx.lineWidth = 1;
182
183      // Draw change marker.
184      ctx.beginPath();
185      ctx.arc(xStart, y, 3 /*r*/, 0 /*start angle*/, 2 * Math.PI /*end angle*/);
186      ctx.fill();
187      ctx.stroke();
188
189      // Display idle value if current hover is idle.
190      if (this.hoveredIdle !== undefined && this.hoveredIdle !== -1) {
191        // Display the idle value +1 to be consistent with catapult.
192        text += ` (Idle: ${(this.hoveredIdle + 1).toLocaleString()})`;
193      }
194
195      // Draw the tooltip.
196      this.drawTrackHoverTooltip(ctx, this.mouseXpos, text);
197    }
198
199    // Write the Y scale on the top left corner.
200    ctx.textBaseline = 'alphabetic';
201    ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
202    ctx.fillRect(0, 0, 42, 18);
203    ctx.fillStyle = '#666';
204    ctx.textAlign = 'left';
205    ctx.fillText(`${yLabel}`, 4, 14);
206
207    // If the cached trace slices don't fully cover the visible time range,
208    // show a gray rectangle with a "Loading..." label.
209    checkerboardExcept(
210        ctx,
211        this.getHeight(),
212        timeScale.timeToPx(visibleWindowTime.start),
213        timeScale.timeToPx(visibleWindowTime.end),
214        timeScale.timeToPx(data.start),
215        timeScale.timeToPx(data.end));
216  }
217
218  onMouseMove({x}: {x: number, y: number}) {
219    const data = this.data();
220    if (data === undefined) return;
221    this.mouseXpos = x;
222    const {timeScale} = globals.frontendLocalState;
223    const time = timeScale.pxToTime(x);
224
225    const [left, right] = searchSegment(data.timestamps, time);
226    this.hoveredTs = left === -1 ? undefined : data.timestamps[left];
227    this.hoveredTsEnd = right === -1 ? undefined : data.timestamps[right];
228    this.hoveredValue = left === -1 ? undefined : data.lastFreqKHz[left];
229    this.hoveredIdle = left === -1 ? undefined : data.lastIdleValues[left];
230  }
231
232  onMouseOut() {
233    this.hoveredValue = undefined;
234    this.hoveredTs = undefined;
235    this.hoveredTsEnd = undefined;
236    this.hoveredIdle = undefined;
237  }
238}
239
240trackRegistry.register(CpuFreqTrack);
241