1// Copyright (C) 2021 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 {PluginContext} from '../../common/plugin_api'; 18import {NUM} from '../../common/query_result'; 19import {ProfileType} from '../../common/state'; 20import { 21 fromNs, 22 TPDuration, 23 TPTime, 24 tpTimeFromSeconds, 25} from '../../common/time'; 26import {TrackData} from '../../common/track_data'; 27import { 28 TrackController, 29} from '../../controller/track_controller'; 30import {FLAMEGRAPH_HOVERED_COLOR} from '../../frontend/flamegraph'; 31import {globals} from '../../frontend/globals'; 32import {TimeScale} from '../../frontend/time_scale'; 33import {NewTrackArgs, Track} from '../../frontend/track'; 34 35export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack'; 36 37export interface Data extends TrackData { 38 tsStartsNs: Float64Array; 39} 40 41export interface Config { 42 upid: number; 43} 44 45class PerfSamplesProfileTrackController extends TrackController<Config, Data> { 46 static readonly kind = PERF_SAMPLES_PROFILE_TRACK_KIND; 47 async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration): 48 Promise<Data> { 49 if (this.config.upid === undefined) { 50 return { 51 start, 52 end, 53 resolution, 54 length: 0, 55 tsStartsNs: new Float64Array(), 56 }; 57 } 58 const queryRes = await this.query(` 59 select ts, upid from perf_sample 60 join thread using (utid) 61 where upid = ${this.config.upid} 62 and callsite_id is not null 63 order by ts`); 64 const numRows = queryRes.numRows(); 65 const data: Data = { 66 start, 67 end, 68 resolution, 69 length: numRows, 70 tsStartsNs: new Float64Array(numRows), 71 }; 72 73 const it = queryRes.iter({ts: NUM}); 74 for (let row = 0; it.valid(); it.next(), row++) { 75 data.tsStartsNs[row] = it.ts; 76 } 77 return data; 78 } 79} 80 81const PERP_SAMPLE_COLOR = 'hsl(224, 45%, 70%)'; 82 83// 0.5 Makes the horizontal lines sharp. 84const MARGIN_TOP = 4.5; 85const RECT_HEIGHT = 30.5; 86 87class PerfSamplesProfileTrack extends Track<Config, Data> { 88 static readonly kind = PERF_SAMPLES_PROFILE_TRACK_KIND; 89 static create(args: NewTrackArgs): PerfSamplesProfileTrack { 90 return new PerfSamplesProfileTrack(args); 91 } 92 93 private centerY = this.getHeight() / 2; 94 private markerWidth = (this.getHeight() - MARGIN_TOP) / 2; 95 private hoveredTs: number|undefined = undefined; 96 97 constructor(args: NewTrackArgs) { 98 super(args); 99 } 100 101 getHeight() { 102 return MARGIN_TOP + RECT_HEIGHT - 1; 103 } 104 105 renderCanvas(ctx: CanvasRenderingContext2D): void { 106 const { 107 visibleTimeScale, 108 } = globals.frontendLocalState; 109 const data = this.data(); 110 111 if (data === undefined) return; 112 113 for (let i = 0; i < data.tsStartsNs.length; i++) { 114 const centerX = data.tsStartsNs[i]; 115 const selection = globals.state.currentSelection; 116 const isHovered = this.hoveredTs === centerX; 117 const isSelected = selection !== null && 118 selection.kind === 'PERF_SAMPLES' && 119 selection.leftTs <= centerX && selection.rightTs >= centerX; 120 const strokeWidth = isSelected ? 3 : 0; 121 this.drawMarker( 122 ctx, 123 visibleTimeScale.secondsToPx(fromNs(centerX)), 124 this.centerY, 125 isHovered, 126 strokeWidth); 127 } 128 } 129 130 drawMarker( 131 ctx: CanvasRenderingContext2D, x: number, y: number, isHovered: boolean, 132 strokeWidth: number): void { 133 ctx.beginPath(); 134 ctx.moveTo(x, y - this.markerWidth); 135 ctx.lineTo(x - this.markerWidth, y); 136 ctx.lineTo(x, y + this.markerWidth); 137 ctx.lineTo(x + this.markerWidth, y); 138 ctx.lineTo(x, y - this.markerWidth); 139 ctx.closePath(); 140 ctx.fillStyle = isHovered ? FLAMEGRAPH_HOVERED_COLOR : PERP_SAMPLE_COLOR; 141 ctx.fill(); 142 if (strokeWidth > 0) { 143 ctx.strokeStyle = FLAMEGRAPH_HOVERED_COLOR; 144 ctx.lineWidth = strokeWidth; 145 ctx.stroke(); 146 } 147 } 148 149 onMouseMove({x, y}: {x: number, y: number}) { 150 const data = this.data(); 151 if (data === undefined) return; 152 const {visibleTimeScale} = globals.frontendLocalState; 153 const time = visibleTimeScale.pxToHpTime(x).nanos; 154 const [left, right] = searchSegment(data.tsStartsNs, time); 155 const index = 156 this.findTimestampIndex(left, visibleTimeScale, data, x, y, right); 157 this.hoveredTs = index === -1 ? undefined : data.tsStartsNs[index]; 158 } 159 160 onMouseOut() { 161 this.hoveredTs = undefined; 162 } 163 164 onMouseClick({x, y}: {x: number, y: number}) { 165 const data = this.data(); 166 if (data === undefined) return false; 167 const { 168 visibleTimeScale: timeScale, 169 } = globals.frontendLocalState; 170 171 const time = timeScale.pxToHpTime(x).nanos; 172 const [left, right] = searchSegment(data.tsStartsNs, time); 173 174 const index = this.findTimestampIndex(left, timeScale, data, x, y, right); 175 176 if (index !== -1) { 177 const ts = data.tsStartsNs[index]; 178 globals.makeSelection(Actions.selectPerfSamples({ 179 id: index, 180 upid: this.config.upid, 181 leftTs: tpTimeFromSeconds(ts), 182 rightTs: tpTimeFromSeconds(ts), 183 type: ProfileType.PERF_SAMPLE, 184 })); 185 return true; 186 } 187 return false; 188 } 189 190 // If the markers overlap the rightmost one will be selected. 191 findTimestampIndex( 192 left: number, timeScale: TimeScale, data: Data, x: number, y: number, 193 right: number): number { 194 let index = -1; 195 if (left !== -1) { 196 const centerX = timeScale.secondsToPx(fromNs(data.tsStartsNs[left])); 197 if (this.isInMarker(x, y, centerX)) { 198 index = left; 199 } 200 } 201 if (right !== -1) { 202 const centerX = timeScale.secondsToPx(fromNs(data.tsStartsNs[right])); 203 if (this.isInMarker(x, y, centerX)) { 204 index = right; 205 } 206 } 207 return index; 208 } 209 210 isInMarker(x: number, y: number, centerX: number) { 211 return Math.abs(x - centerX) + Math.abs(y - this.centerY) <= 212 this.markerWidth; 213 } 214} 215 216export function activate(ctx: PluginContext) { 217 ctx.registerTrackController(PerfSamplesProfileTrackController); 218 ctx.registerTrack(PerfSamplesProfileTrack); 219} 220 221export const plugin = { 222 pluginId: 'perfetto.PerfSamplesProfile', 223 activate, 224}; 225