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 {TrackState} from '../../common/state'; 21import {fromNs, toNs} from '../../common/time'; 22import {globals} from '../../frontend/globals'; 23import {TimeScale} from '../../frontend/time_scale'; 24import {Track} from '../../frontend/track'; 25import {trackRegistry} from '../../frontend/track_registry'; 26 27import {Config, CPU_PROFILE_TRACK_KIND, Data} from './common'; 28 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(trackState: TrackState): CpuProfileTrack { 39 return new CpuProfileTrack(trackState); 40 } 41 42 private centerY = this.getHeight() / 2; 43 private markerWidth = (this.getHeight() - MARGIN_TOP) / 2; 44 private hoveredTs: number|undefined = undefined; 45 46 constructor(trackState: TrackState) { 47 super(trackState); 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 79 drawMarker( 80 ctx: CanvasRenderingContext2D, x: number, y: number, isHovered: boolean, 81 strokeWidth: number, callsiteId: number): void { 82 ctx.beginPath(); 83 ctx.moveTo(x - this.markerWidth, y - this.markerWidth); 84 ctx.lineTo(x, y + this.markerWidth); 85 ctx.lineTo(x + this.markerWidth, y - this.markerWidth); 86 ctx.lineTo(x - this.markerWidth, y - this.markerWidth); 87 ctx.closePath(); 88 ctx.fillStyle = colorForSample(callsiteId, isHovered); 89 ctx.fill(); 90 if (strokeWidth > 0) { 91 ctx.strokeStyle = colorForSample(callsiteId, false); 92 ctx.lineWidth = strokeWidth; 93 ctx.stroke(); 94 } 95 } 96 97 onMouseMove({x, y}: {x: number, y: number}) { 98 const data = this.data(); 99 if (data === undefined) return; 100 const {timeScale} = globals.frontendLocalState; 101 const time = toNs(timeScale.pxToTime(x)); 102 const [left, right] = searchSegment(data.tsStarts, time); 103 const index = this.findTimestampIndex(left, timeScale, data, x, y, right); 104 this.hoveredTs = index === -1 ? undefined : data.tsStarts[index]; 105 } 106 107 onMouseOut() { 108 this.hoveredTs = undefined; 109 } 110 111 onMouseClick({x, y}: {x: number, y: number}) { 112 const data = this.data(); 113 if (data === undefined) return false; 114 const {timeScale} = globals.frontendLocalState; 115 116 const time = toNs(timeScale.pxToTime(x)); 117 const [left, right] = searchSegment(data.tsStarts, time); 118 119 const index = this.findTimestampIndex(left, timeScale, data, x, y, right); 120 121 if (index !== -1) { 122 const id = data.ids[index]; 123 const ts = data.tsStarts[index]; 124 125 globals.makeSelection( 126 Actions.selectCpuProfileSample({id, utid: this.config.utid, ts})); 127 return true; 128 } 129 return false; 130 } 131 132 // If the markers overlap the rightmost one will be selected. 133 findTimestampIndex( 134 left: number, timeScale: TimeScale, data: Data, x: number, y: number, 135 right: number): number { 136 let index = -1; 137 if (left !== -1) { 138 const centerX = timeScale.timeToPx(fromNs(data.tsStarts[left])); 139 if (this.isInMarker(x, y, centerX)) { 140 index = left; 141 } 142 } 143 if (right !== -1) { 144 const centerX = timeScale.timeToPx(fromNs(data.tsStarts[right])); 145 if (this.isInMarker(x, y, centerX)) { 146 index = right; 147 } 148 } 149 return index; 150 } 151 152 isInMarker(x: number, y: number, centerX: number) { 153 return Math.abs(x - centerX) + Math.abs(y - this.centerY) <= 154 this.markerWidth; 155 } 156} 157 158trackRegistry.register(CpuProfileTrack); 159