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 {Actions} from '../../common/actions'; 17import {TrackState} from '../../common/state'; 18import {fromNs, toNs} from '../../common/time'; 19import {HEAP_PROFILE_HOVERED_COLOR} from '../../frontend/flamegraph'; 20import {globals} from '../../frontend/globals'; 21import {TimeScale} from '../../frontend/time_scale'; 22import {Track} from '../../frontend/track'; 23import {trackRegistry} from '../../frontend/track_registry'; 24 25import {Config, Data, HEAP_PROFILE_TRACK_KIND} from './common'; 26 27const HEAP_PROFILE_COLOR = 'hsl(224, 45%, 70%)'; 28 29// 0.5 Makes the horizontal lines sharp. 30const MARGIN_TOP = 4.5; 31const RECT_HEIGHT = 30.5; 32 33class HeapProfileTrack extends Track<Config, Data> { 34 static readonly kind = HEAP_PROFILE_TRACK_KIND; 35 static create(trackState: TrackState): HeapProfileTrack { 36 return new HeapProfileTrack(trackState); 37 } 38 39 private centerY = this.getHeight() / 2; 40 private markerWidth = (this.getHeight() - MARGIN_TOP) / 2; 41 private hoveredTs: number|undefined = undefined; 42 43 constructor(trackState: TrackState) { 44 super(trackState); 45 } 46 47 getHeight() { 48 return MARGIN_TOP + RECT_HEIGHT - 1; 49 } 50 51 renderCanvas(ctx: CanvasRenderingContext2D): void { 52 const { 53 timeScale, 54 } = globals.frontendLocalState; 55 const data = this.data(); 56 57 if (data === undefined) return; 58 59 for (let i = 0; i < data.tsStarts.length; i++) { 60 const centerX = data.tsStarts[i]; 61 const selection = globals.state.currentSelection; 62 const isHovered = this.hoveredTs === centerX; 63 const isSelected = selection !== null && 64 selection.kind === 'HEAP_PROFILE' && selection.ts === centerX; 65 const strokeWidth = isSelected ? 3 : 0; 66 this.drawMarker( 67 ctx, 68 timeScale.timeToPx(fromNs(centerX)), 69 this.centerY, 70 isHovered, 71 strokeWidth); 72 } 73 } 74 75 drawMarker( 76 ctx: CanvasRenderingContext2D, x: number, y: number, isHovered: boolean, 77 strokeWidth: number): void { 78 ctx.beginPath(); 79 ctx.moveTo(x, y - this.markerWidth); 80 ctx.lineTo(x - this.markerWidth, y); 81 ctx.lineTo(x, y + this.markerWidth); 82 ctx.lineTo(x + this.markerWidth, y); 83 ctx.lineTo(x, y - this.markerWidth); 84 ctx.closePath(); 85 ctx.fillStyle = isHovered ? HEAP_PROFILE_HOVERED_COLOR : HEAP_PROFILE_COLOR; 86 ctx.fill(); 87 if (strokeWidth > 0) { 88 ctx.strokeStyle = HEAP_PROFILE_HOVERED_COLOR; 89 ctx.lineWidth = strokeWidth; 90 ctx.stroke(); 91 } 92 } 93 94 onMouseMove({x, y}: {x: number, y: number}) { 95 const data = this.data(); 96 if (data === undefined) return; 97 const {timeScale} = globals.frontendLocalState; 98 const time = toNs(timeScale.pxToTime(x)); 99 const [left, right] = searchSegment(data.tsStarts, time); 100 const index = this.findTimestampIndex(left, timeScale, data, x, y, right); 101 this.hoveredTs = index === -1 ? undefined : data.tsStarts[index]; 102 } 103 104 onMouseOut() { 105 this.hoveredTs = undefined; 106 } 107 108 onMouseClick({x, y}: {x: number, y: number}) { 109 const data = this.data(); 110 if (data === undefined) return false; 111 const {timeScale} = globals.frontendLocalState; 112 113 const time = toNs(timeScale.pxToTime(x)); 114 const [left, right] = searchSegment(data.tsStarts, time); 115 116 const index = this.findTimestampIndex(left, timeScale, data, x, y, right); 117 118 if (index !== -1) { 119 const ts = data.tsStarts[index]; 120 const type = data.types[index]; 121 globals.makeSelection(Actions.selectHeapProfile( 122 {id: index, upid: this.config.upid, ts, type})); 123 return true; 124 } 125 return false; 126 } 127 128 // If the markers overlap the rightmost one will be selected. 129 findTimestampIndex( 130 left: number, timeScale: TimeScale, data: Data, x: number, y: number, 131 right: number): number { 132 let index = -1; 133 if (left !== -1) { 134 const centerX = timeScale.timeToPx(fromNs(data.tsStarts[left])); 135 if (this.isInMarker(x, y, centerX)) { 136 index = left; 137 } 138 } 139 if (right !== -1) { 140 const centerX = timeScale.timeToPx(fromNs(data.tsStarts[right])); 141 if (this.isInMarker(x, y, centerX)) { 142 index = right; 143 } 144 } 145 return index; 146 } 147 148 isInMarker(x: number, y: number, centerX: number) { 149 return Math.abs(x - centerX) + Math.abs(y - this.centerY) <= 150 this.markerWidth; 151 } 152} 153 154trackRegistry.register(HeapProfileTrack); 155