• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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