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 15 16import {searchSegment} from '../../base/binary_search'; 17import {Actions} from '../../common/actions'; 18import {hslForSlice} from '../../common/colorizer'; 19import {PluginContext} from '../../common/plugin_api'; 20import {NUM} from '../../common/query_result'; 21import {fromNs, TPDuration, TPTime} from '../../common/time'; 22import {TrackData} from '../../common/track_data'; 23import { 24 TrackController, 25} from '../../controller/track_controller'; 26import {globals} from '../../frontend/globals'; 27import {cachedHsluvToHex} from '../../frontend/hsluv_cache'; 28import {TimeScale} from '../../frontend/time_scale'; 29import {NewTrackArgs, Track} from '../../frontend/track'; 30 31const BAR_HEIGHT = 3; 32const MARGIN_TOP = 4.5; 33const RECT_HEIGHT = 30.5; 34 35export const CPU_PROFILE_TRACK_KIND = 'CpuProfileTrack'; 36 37export interface Data extends TrackData { 38 ids: Float64Array; 39 tsStarts: Float64Array; 40 callsiteId: Uint32Array; 41} 42 43export interface Config { 44 utid: number; 45} 46 47class CpuProfileTrackController extends TrackController<Config, Data> { 48 static readonly kind = CPU_PROFILE_TRACK_KIND; 49 async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration): 50 Promise<Data> { 51 const query = `select 52 id, 53 ts, 54 callsite_id as callsiteId 55 from cpu_profile_stack_sample 56 where utid = ${this.config.utid} 57 order by ts`; 58 59 const result = await this.query(query); 60 const numRows = result.numRows(); 61 const data: Data = { 62 start, 63 end, 64 resolution, 65 length: numRows, 66 ids: new Float64Array(numRows), 67 tsStarts: new Float64Array(numRows), 68 callsiteId: new Uint32Array(numRows), 69 }; 70 71 const it = result.iter({id: NUM, ts: NUM, callsiteId: NUM}); 72 for (let row = 0; it.valid(); it.next(), ++row) { 73 data.ids[row] = it.id; 74 data.tsStarts[row] = it.ts; 75 data.callsiteId[row] = it.callsiteId; 76 } 77 78 return data; 79 } 80} 81 82function colorForSample(callsiteId: number, isHovered: boolean): string { 83 const [hue, saturation, lightness] = 84 hslForSlice(String(callsiteId), isHovered); 85 return cachedHsluvToHex(hue, saturation, lightness); 86} 87 88class CpuProfileTrack extends Track<Config, Data> { 89 static readonly kind = CPU_PROFILE_TRACK_KIND; 90 static create(args: NewTrackArgs): CpuProfileTrack { 91 return new CpuProfileTrack(args); 92 } 93 94 private centerY = this.getHeight() / 2 + BAR_HEIGHT; 95 private markerWidth = (this.getHeight() - MARGIN_TOP - BAR_HEIGHT) / 2; 96 private hoveredTs: number|undefined = undefined; 97 98 constructor(args: NewTrackArgs) { 99 super(args); 100 } 101 102 getHeight() { 103 return MARGIN_TOP + RECT_HEIGHT - 1; 104 } 105 106 renderCanvas(ctx: CanvasRenderingContext2D): void { 107 const { 108 visibleTimeScale: timeScale, 109 } = globals.frontendLocalState; 110 const data = this.data(); 111 112 if (data === undefined) return; 113 114 for (let i = 0; i < data.tsStarts.length; i++) { 115 const centerX = data.tsStarts[i]; 116 const selection = globals.state.currentSelection; 117 const isHovered = this.hoveredTs === centerX; 118 const isSelected = selection !== null && 119 selection.kind === 'CPU_PROFILE_SAMPLE' && selection.ts === centerX; 120 const strokeWidth = isSelected ? 3 : 0; 121 this.drawMarker( 122 ctx, 123 timeScale.secondsToPx(fromNs(centerX)), 124 this.centerY, 125 isHovered, 126 strokeWidth, 127 data.callsiteId[i]); 128 } 129 130 // Group together identical identical CPU profile samples by connecting them 131 // with an horizontal bar. 132 let clusterStartIndex = 0; 133 while (clusterStartIndex < data.tsStarts.length) { 134 const callsiteId = data.callsiteId[clusterStartIndex]; 135 136 // Find the end of the cluster by searching for the next different CPU 137 // sample. The resulting range [clusterStartIndex, clusterEndIndex] is 138 // inclusive and within array bounds. 139 let clusterEndIndex = clusterStartIndex; 140 while (clusterEndIndex + 1 < data.tsStarts.length && 141 data.callsiteId[clusterEndIndex + 1] === callsiteId) { 142 clusterEndIndex++; 143 } 144 145 // If there are multiple CPU samples in the cluster, draw a line. 146 if (clusterStartIndex !== clusterEndIndex) { 147 const startX = data.tsStarts[clusterStartIndex]; 148 const endX = data.tsStarts[clusterEndIndex]; 149 const leftPx = timeScale.secondsToPx(fromNs(startX)) - this.markerWidth; 150 const rightPx = timeScale.secondsToPx(fromNs(endX)) + this.markerWidth; 151 const width = rightPx - leftPx; 152 ctx.fillStyle = colorForSample(callsiteId, false); 153 ctx.fillRect(leftPx, MARGIN_TOP, width, BAR_HEIGHT); 154 } 155 156 // Move to the next cluster. 157 clusterStartIndex = clusterEndIndex + 1; 158 } 159 } 160 161 drawMarker( 162 ctx: CanvasRenderingContext2D, x: number, y: number, isHovered: boolean, 163 strokeWidth: number, callsiteId: number): void { 164 ctx.beginPath(); 165 ctx.moveTo(x - this.markerWidth, y - this.markerWidth); 166 ctx.lineTo(x, y + this.markerWidth); 167 ctx.lineTo(x + this.markerWidth, y - this.markerWidth); 168 ctx.lineTo(x - this.markerWidth, y - this.markerWidth); 169 ctx.closePath(); 170 ctx.fillStyle = colorForSample(callsiteId, isHovered); 171 ctx.fill(); 172 if (strokeWidth > 0) { 173 ctx.strokeStyle = colorForSample(callsiteId, false); 174 ctx.lineWidth = strokeWidth; 175 ctx.stroke(); 176 } 177 } 178 179 onMouseMove({x, y}: {x: number, y: number}) { 180 const data = this.data(); 181 if (data === undefined) return; 182 const { 183 visibleTimeScale: timeScale, 184 } = globals.frontendLocalState; 185 const time = timeScale.pxToHpTime(x).nanos; 186 const [left, right] = searchSegment(data.tsStarts, time); 187 const index = this.findTimestampIndex(left, timeScale, data, x, y, right); 188 this.hoveredTs = index === -1 ? undefined : data.tsStarts[index]; 189 } 190 191 onMouseOut() { 192 this.hoveredTs = undefined; 193 } 194 195 onMouseClick({x, y}: {x: number, y: number}) { 196 const data = this.data(); 197 if (data === undefined) return false; 198 const { 199 visibleTimeScale: timeScale, 200 } = globals.frontendLocalState; 201 202 const time = timeScale.pxToHpTime(x).nanos; 203 const [left, right] = searchSegment(data.tsStarts, time); 204 205 const index = this.findTimestampIndex(left, timeScale, data, x, y, right); 206 207 if (index !== -1) { 208 const id = data.ids[index]; 209 const ts = data.tsStarts[index]; 210 211 globals.makeSelection( 212 Actions.selectCpuProfileSample({id, utid: this.config.utid, ts})); 213 return true; 214 } 215 return false; 216 } 217 218 // If the markers overlap the rightmost one will be selected. 219 findTimestampIndex( 220 left: number, timeScale: TimeScale, data: Data, x: number, y: number, 221 right: number): number { 222 let index = -1; 223 if (left !== -1) { 224 const centerX = timeScale.secondsToPx(fromNs(data.tsStarts[left])); 225 if (this.isInMarker(x, y, centerX)) { 226 index = left; 227 } 228 } 229 if (right !== -1) { 230 const centerX = timeScale.secondsToPx(fromNs(data.tsStarts[right])); 231 if (this.isInMarker(x, y, centerX)) { 232 index = right; 233 } 234 } 235 return index; 236 } 237 238 isInMarker(x: number, y: number, centerX: number) { 239 return Math.abs(x - centerX) + Math.abs(y - this.centerY) <= 240 this.markerWidth; 241 } 242} 243 244function activate(ctx: PluginContext) { 245 ctx.registerTrackController(CpuProfileTrackController); 246 ctx.registerTrack(CpuProfileTrack); 247} 248 249export const plugin = { 250 pluginId: 'perfetto.CpuProfile', 251 activate, 252}; 253