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