1// Copyright (C) 2020 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 {searchSegment} from '../../base/binary_search'; 18import {Actions} from '../../common/actions'; 19import {hslForSlice} from '../../common/colorizer'; 20import {fromNs, toNs} from '../../common/time'; 21import {globals} from '../../frontend/globals'; 22import {TimeScale} from '../../frontend/time_scale'; 23import {NewTrackArgs, Track} from '../../frontend/track'; 24import {trackRegistry} from '../../frontend/track_registry'; 25 26import {Config, CPU_PROFILE_TRACK_KIND, Data} from './common'; 27 28const BAR_HEIGHT = 3; 29const MARGIN_TOP = 4.5; 30const RECT_HEIGHT = 30.5; 31 32function colorForSample(callsiteId: number, isHovered: boolean): string { 33 return hsluvToHex(hslForSlice(String(callsiteId), isHovered)); 34} 35 36class CpuProfileTrack extends Track<Config, Data> { 37 static readonly kind = CPU_PROFILE_TRACK_KIND; 38 static create(args: NewTrackArgs): CpuProfileTrack { 39 return new CpuProfileTrack(args); 40 } 41 42 private centerY = this.getHeight() / 2 + BAR_HEIGHT; 43 private markerWidth = (this.getHeight() - MARGIN_TOP - BAR_HEIGHT) / 2; 44 private hoveredTs: number|undefined = undefined; 45 46 constructor(args: NewTrackArgs) { 47 super(args); 48 } 49 50 getHeight() { 51 return MARGIN_TOP + RECT_HEIGHT - 1; 52 } 53 54 renderCanvas(ctx: CanvasRenderingContext2D): void { 55 const { 56 timeScale, 57 } = globals.frontendLocalState; 58 const data = this.data(); 59 60 if (data === undefined) return; 61 62 for (let i = 0; i < data.tsStarts.length; i++) { 63 const centerX = data.tsStarts[i]; 64 const selection = globals.state.currentSelection; 65 const isHovered = this.hoveredTs === centerX; 66 const isSelected = selection !== null && 67 selection.kind === 'CPU_PROFILE_SAMPLE' && selection.ts === centerX; 68 const strokeWidth = isSelected ? 3 : 0; 69 this.drawMarker( 70 ctx, 71 timeScale.timeToPx(fromNs(centerX)), 72 this.centerY, 73 isHovered, 74 strokeWidth, 75 data.callsiteId[i]); 76 } 77 78 let startX = data.tsStarts.length ? data.tsStarts[0] : -1; 79 let endX = data.tsStarts.length ? data.tsStarts[0] : -1; 80 let lastCallsiteId = data.callsiteId.length ? data.callsiteId[0] : -1; 81 for (let i = 0; i < data.tsStarts.length; i++) { 82 const centerX = data.tsStarts[i]; 83 const callsiteId = data.callsiteId[i]; 84 if (lastCallsiteId !== callsiteId) { 85 if (startX !== endX) { 86 const leftPx = timeScale.timeToPx(fromNs(startX)) - this.markerWidth; 87 const rightPx = timeScale.timeToPx(fromNs(endX)) + this.markerWidth; 88 const width = rightPx - leftPx; 89 ctx.fillStyle = colorForSample(lastCallsiteId, false); 90 ctx.fillRect(leftPx, MARGIN_TOP, width, BAR_HEIGHT); 91 } 92 startX = centerX; 93 } 94 endX = centerX; 95 lastCallsiteId = callsiteId; 96 } 97 } 98 99 drawMarker( 100 ctx: CanvasRenderingContext2D, x: number, y: number, isHovered: boolean, 101 strokeWidth: number, callsiteId: number): void { 102 ctx.beginPath(); 103 ctx.moveTo(x - this.markerWidth, y - this.markerWidth); 104 ctx.lineTo(x, y + this.markerWidth); 105 ctx.lineTo(x + this.markerWidth, y - this.markerWidth); 106 ctx.lineTo(x - this.markerWidth, y - this.markerWidth); 107 ctx.closePath(); 108 ctx.fillStyle = colorForSample(callsiteId, isHovered); 109 ctx.fill(); 110 if (strokeWidth > 0) { 111 ctx.strokeStyle = colorForSample(callsiteId, false); 112 ctx.lineWidth = strokeWidth; 113 ctx.stroke(); 114 } 115 } 116 117 onMouseMove({x, y}: {x: number, y: number}) { 118 const data = this.data(); 119 if (data === undefined) return; 120 const {timeScale} = globals.frontendLocalState; 121 const time = toNs(timeScale.pxToTime(x)); 122 const [left, right] = searchSegment(data.tsStarts, time); 123 const index = this.findTimestampIndex(left, timeScale, data, x, y, right); 124 this.hoveredTs = index === -1 ? undefined : data.tsStarts[index]; 125 } 126 127 onMouseOut() { 128 this.hoveredTs = undefined; 129 } 130 131 onMouseClick({x, y}: {x: number, y: number}) { 132 const data = this.data(); 133 if (data === undefined) return false; 134 const {timeScale} = globals.frontendLocalState; 135 136 const time = toNs(timeScale.pxToTime(x)); 137 const [left, right] = searchSegment(data.tsStarts, time); 138 139 const index = this.findTimestampIndex(left, timeScale, data, x, y, right); 140 141 if (index !== -1) { 142 const id = data.ids[index]; 143 const ts = data.tsStarts[index]; 144 145 globals.makeSelection( 146 Actions.selectCpuProfileSample({id, utid: this.config.utid, ts})); 147 return true; 148 } 149 return false; 150 } 151 152 // If the markers overlap the rightmost one will be selected. 153 findTimestampIndex( 154 left: number, timeScale: TimeScale, data: Data, x: number, y: number, 155 right: number): number { 156 let index = -1; 157 if (left !== -1) { 158 const centerX = timeScale.timeToPx(fromNs(data.tsStarts[left])); 159 if (this.isInMarker(x, y, centerX)) { 160 index = left; 161 } 162 } 163 if (right !== -1) { 164 const centerX = timeScale.timeToPx(fromNs(data.tsStarts[right])); 165 if (this.isInMarker(x, y, centerX)) { 166 index = right; 167 } 168 } 169 return index; 170 } 171 172 isInMarker(x: number, y: number, centerX: number) { 173 return Math.abs(x - centerX) + Math.abs(y - this.centerY) <= 174 this.markerWidth; 175 } 176} 177 178trackRegistry.register(CpuProfileTrack); 179