• 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
15import {BigintMath} from '../../base/bigint_math';
16import {search, searchEq, searchSegment} from '../../base/binary_search';
17import {assertTrue} from '../../base/logging';
18import {Actions} from '../../common/actions';
19import {
20  cropText,
21  drawDoubleHeadedArrow,
22  drawIncompleteSlice,
23} from '../../common/canvas_utils';
24import {colorForThread} from '../../common/colorizer';
25import {PluginContext} from '../../common/plugin_api';
26import {NUM} from '../../common/query_result';
27import {
28  fromNs,
29  toNs,
30  TPDuration,
31  TPTime,
32  tpTimeFromSeconds,
33  tpTimeToNanos,
34  tpTimeToString,
35} from '../../common/time';
36import {TrackData} from '../../common/track_data';
37import {
38  TrackController,
39} from '../../controller/track_controller';
40import {checkerboardExcept} from '../../frontend/checkerboard';
41import {globals} from '../../frontend/globals';
42import {NewTrackArgs, Track} from '../../frontend/track';
43
44export const CPU_SLICE_TRACK_KIND = 'CpuSliceTrack';
45
46export interface Data extends TrackData {
47  // Slices are stored in a columnar fashion. All fields have the same length.
48  ids: Float64Array;
49  starts: Float64Array;
50  ends: Float64Array;
51  utids: Uint32Array;
52  isIncomplete: Uint8Array;
53  lastRowId: number;
54}
55
56export interface Config {
57  cpu: number;
58}
59
60class CpuSliceTrackController extends TrackController<Config, Data> {
61  static readonly kind = CPU_SLICE_TRACK_KIND;
62
63  private cachedBucketNs = Number.MAX_SAFE_INTEGER;
64  private maxDurNs = 0;
65  private lastRowId = -1;
66
67  async onSetup() {
68    await this.query(`
69      create view ${this.tableName('sched')} as
70      select
71        ts,
72        dur,
73        utid,
74        id,
75        dur = -1 as isIncomplete
76      from sched
77      where cpu = ${this.config.cpu} and utid != 0
78    `);
79
80    const queryRes = await this.query(`
81      select ifnull(max(dur), 0) as maxDur, count(1) as rowCount
82      from ${this.tableName('sched')}
83    `);
84
85    const queryLastSlice = await this.query(`
86    select max(id) as lastSliceId from ${this.tableName('sched')}
87    `);
88    this.lastRowId = queryLastSlice.firstRow({lastSliceId: NUM}).lastSliceId;
89
90    const row = queryRes.firstRow({maxDur: NUM, rowCount: NUM});
91    this.maxDurNs = row.maxDur;
92    const rowCount = row.rowCount;
93    const bucketNs = this.cachedBucketSizeNs(rowCount);
94    if (bucketNs === undefined) {
95      return;
96    }
97
98    await this.query(`
99      create table ${this.tableName('sched_cached')} as
100      select
101        (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cached_tsq,
102        ts,
103        max(dur) as dur,
104        utid,
105        id,
106        isIncomplete
107      from ${this.tableName('sched')}
108      group by cached_tsq, isIncomplete
109      order by cached_tsq
110    `);
111    this.cachedBucketNs = bucketNs;
112  }
113
114  async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
115      Promise<Data> {
116    assertTrue(
117        BigintMath.popcount(resolution) === 1,
118        `${resolution} is not a power of 2`);
119    const resolutionNs = Number(resolution);
120
121    // The resolution should always be a power of two for the logic of this
122    // function to make sense.
123    assertTrue(Math.log2(resolutionNs) % 1 === 0);
124
125    const boundStartNs = tpTimeToNanos(start);
126    const boundEndNs = tpTimeToNanos(end);
127
128    // ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
129    // be an even number, so we can snap in the middle.
130    const bucketNs =
131        Math.max(Math.round(resolutionNs * this.pxSize() / 2) * 2, 1);
132
133    const isCached = this.cachedBucketNs <= bucketNs;
134    const queryTsq = isCached ?
135        `cached_tsq / ${bucketNs} * ${bucketNs}` :
136        `(ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs}`;
137    const queryTable =
138        isCached ? this.tableName('sched_cached') : this.tableName('sched');
139    const constraintColumn = isCached ? 'cached_tsq' : 'ts';
140
141    const queryRes = await this.query(`
142      select
143        ${queryTsq} as tsq,
144        ts,
145        max(dur) as dur,
146        utid,
147        id,
148        isIncomplete
149      from ${queryTable}
150      where
151        ${constraintColumn} >= ${boundStartNs - this.maxDurNs} and
152        ${constraintColumn} <= ${boundEndNs}
153      group by tsq, isIncomplete
154      order by tsq
155    `);
156
157    const numRows = queryRes.numRows();
158    const slices: Data = {
159      start,
160      end,
161      resolution,
162      length: numRows,
163      lastRowId: this.lastRowId,
164      ids: new Float64Array(numRows),
165      starts: new Float64Array(numRows),
166      ends: new Float64Array(numRows),
167      utids: new Uint32Array(numRows),
168      isIncomplete: new Uint8Array(numRows),
169    };
170
171    const it = queryRes.iter(
172        {tsq: NUM, ts: NUM, dur: NUM, utid: NUM, id: NUM, isIncomplete: NUM});
173    for (let row = 0; it.valid(); it.next(), row++) {
174      const startNsQ = it.tsq;
175      const startNs = it.ts;
176      const durNs = it.dur;
177      const endNs = startNs + durNs;
178
179      // If the slice is incomplete, the end calculated later.
180      if (!it.isIncomplete) {
181        let endNsQ =
182            Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
183        endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
184        slices.ends[row] = fromNs(endNsQ);
185      }
186
187      slices.starts[row] = fromNs(startNsQ);
188      slices.utids[row] = it.utid;
189      slices.ids[row] = it.id;
190      slices.isIncomplete[row] = it.isIncomplete;
191    }
192
193    // If the slice is incomplete and it is the last slice in the track, the end
194    // of the slice would be the end of the visible window. Otherwise we end the
195    // slice with the beginning the next one.
196    for (let row = 0; row < slices.length; row++) {
197      if (!slices.isIncomplete[row]) {
198        continue;
199      }
200      const endNs =
201          row === slices.length - 1 ? boundEndNs : toNs(slices.starts[row + 1]);
202
203      let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
204      endNsQ = Math.max(endNsQ, toNs(slices.starts[row]) + bucketNs);
205      slices.ends[row] = fromNs(endNsQ);
206    }
207    return slices;
208  }
209
210  async onDestroy() {
211    await this.query(`drop table if exists ${this.tableName('sched_cached')}`);
212  }
213}
214
215const MARGIN_TOP = 3;
216const RECT_HEIGHT = 24;
217const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
218
219class CpuSliceTrack extends Track<Config, Data> {
220  static readonly kind = CPU_SLICE_TRACK_KIND;
221  static create(args: NewTrackArgs): CpuSliceTrack {
222    return new CpuSliceTrack(args);
223  }
224
225  private mousePos?: {x: number, y: number};
226  private utidHoveredInThisTrack = -1;
227
228  constructor(args: NewTrackArgs) {
229    super(args);
230  }
231
232  getHeight(): number {
233    return TRACK_HEIGHT;
234  }
235
236  renderCanvas(ctx: CanvasRenderingContext2D): void {
237    // TODO: fonts and colors should come from the CSS and not hardcoded here.
238    const {visibleTimeScale, windowSpan} = globals.frontendLocalState;
239    const data = this.data();
240
241    if (data === undefined) return;  // Can't possibly draw anything.
242
243    // If the cached trace slices don't fully cover the visible time range,
244    // show a gray rectangle with a "Loading..." label.
245    checkerboardExcept(
246        ctx,
247        this.getHeight(),
248        windowSpan.start,
249        windowSpan.end,
250        visibleTimeScale.tpTimeToPx(data.start),
251        visibleTimeScale.tpTimeToPx(data.end));
252
253    this.renderSlices(ctx, data);
254  }
255
256  renderSlices(ctx: CanvasRenderingContext2D, data: Data): void {
257    const {
258      visibleTimeScale,
259      visibleWindowTime,
260    } = globals.frontendLocalState;
261    assertTrue(data.starts.length === data.ends.length);
262    assertTrue(data.starts.length === data.utids.length);
263
264    const visWindowEndPx = visibleTimeScale.hpTimeToPx(visibleWindowTime.end);
265
266    ctx.textAlign = 'center';
267    ctx.font = '12px Roboto Condensed';
268    const charWidth = ctx.measureText('dbpqaouk').width / 8;
269
270    const startSec = visibleWindowTime.start.seconds;
271    const endSec = visibleWindowTime.end.seconds;
272
273    const rawStartIdx = data.ends.findIndex((end) => end >= startSec);
274    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
275
276    const [, rawEndIdx] = searchSegment(data.starts, endSec);
277    const endIdx = rawEndIdx === -1 ? data.starts.length : rawEndIdx;
278
279    for (let i = startIdx; i < endIdx; i++) {
280      const tStart = data.starts[i];
281      let tEnd = data.ends[i];
282      const utid = data.utids[i];
283
284      // If the last slice is incomplete, it should end with the end of the
285      // window, else it might spill over the window and the end would not be
286      // visible as a zigzag line.
287      if (data.ids[i] === data.lastRowId && data.isIncomplete[i]) {
288        tEnd = visibleWindowTime.end.seconds;
289      }
290      const rectStart = visibleTimeScale.secondsToPx(tStart);
291      const rectEnd = visibleTimeScale.secondsToPx(tEnd);
292      const rectWidth = Math.max(1, rectEnd - rectStart);
293
294      const threadInfo = globals.threads.get(utid);
295      const pid = threadInfo && threadInfo.pid ? threadInfo.pid : -1;
296
297      const isHovering = globals.state.hoveredUtid !== -1;
298      const isThreadHovered = globals.state.hoveredUtid === utid;
299      const isProcessHovered = globals.state.hoveredPid === pid;
300      const color = colorForThread(threadInfo);
301      if (isHovering && !isThreadHovered) {
302        if (!isProcessHovered) {
303          color.l = 90;
304          color.s = 0;
305        } else {
306          color.l = Math.min(color.l + 30, 80);
307          color.s -= 20;
308        }
309      } else {
310        color.l = Math.min(color.l + 10, 60);
311        color.s -= 20;
312      }
313      ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
314      if (data.isIncomplete[i]) {
315        drawIncompleteSlice(ctx, rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
316      } else {
317        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
318      }
319
320      // Don't render text when we have less than 5px to play with.
321      if (rectWidth < 5) continue;
322
323      // TODO: consider de-duplicating this code with the copied one from
324      // chrome_slices/frontend.ts.
325      let title = `[utid:${utid}]`;
326      let subTitle = '';
327      if (threadInfo) {
328        if (threadInfo.pid) {
329          let procName = threadInfo.procName || '';
330          if (procName.startsWith('/')) {  // Remove folder paths from name
331            procName = procName.substring(procName.lastIndexOf('/') + 1);
332          }
333          title = `${procName} [${threadInfo.pid}]`;
334          subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`;
335        } else {
336          title = `${threadInfo.threadName} [${threadInfo.tid}]`;
337        }
338      }
339      const right = Math.min(visWindowEndPx, rectEnd);
340      const left = Math.max(rectStart, 0);
341      const visibleWidth = Math.max(right - left, 1);
342      title = cropText(title, charWidth, visibleWidth);
343      subTitle = cropText(subTitle, charWidth, visibleWidth);
344      const rectXCenter = left + visibleWidth / 2;
345      ctx.fillStyle = '#fff';
346      ctx.font = '12px Roboto Condensed';
347      ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 1);
348      ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
349      ctx.font = '10px Roboto Condensed';
350      ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 9);
351    }
352
353    const selection = globals.state.currentSelection;
354    const details = globals.sliceDetails;
355    if (selection !== null && selection.kind === 'SLICE') {
356      const [startIndex, endIndex] = searchEq(data.ids, selection.id);
357      if (startIndex !== endIndex) {
358        const tStart = data.starts[startIndex];
359        const tEnd = data.ends[startIndex];
360        const utid = data.utids[startIndex];
361        const color = colorForThread(globals.threads.get(utid));
362        const rectStart = visibleTimeScale.secondsToPx(tStart);
363        const rectEnd = visibleTimeScale.secondsToPx(tEnd);
364        const rectWidth = Math.max(1, rectEnd - rectStart);
365
366        // Draw a rectangle around the slice that is currently selected.
367        ctx.strokeStyle = `hsl(${color.h}, ${color.s}%, 30%)`;
368        ctx.beginPath();
369        ctx.lineWidth = 3;
370        ctx.strokeRect(rectStart, MARGIN_TOP - 1.5, rectWidth, RECT_HEIGHT + 3);
371        ctx.closePath();
372        // Draw arrow from wakeup time of current slice.
373        if (details.wakeupTs) {
374          const wakeupPos = visibleTimeScale.tpTimeToPx(details.wakeupTs);
375          const latencyWidth = rectStart - wakeupPos;
376          drawDoubleHeadedArrow(
377              ctx,
378              wakeupPos,
379              MARGIN_TOP + RECT_HEIGHT,
380              latencyWidth,
381              latencyWidth >= 20);
382          // Latency time with a white semi-transparent background.
383          const latency = tpTimeFromSeconds(tStart) - details.wakeupTs;
384          const displayText = tpTimeToString(latency);
385          const measured = ctx.measureText(displayText);
386          if (latencyWidth >= measured.width + 2) {
387            ctx.fillStyle = 'rgba(255,255,255,0.7)';
388            ctx.fillRect(
389                wakeupPos + latencyWidth / 2 - measured.width / 2 - 1,
390                MARGIN_TOP + RECT_HEIGHT - 12,
391                measured.width + 2,
392                11);
393            ctx.textBaseline = 'bottom';
394            ctx.fillStyle = 'black';
395            ctx.fillText(
396                displayText,
397                wakeupPos + (latencyWidth) / 2,
398                MARGIN_TOP + RECT_HEIGHT - 1);
399          }
400        }
401      }
402
403      // Draw diamond if the track being drawn is the cpu of the waker.
404      if (this.config.cpu === details.wakerCpu && details.wakeupTs) {
405        const wakeupPos =
406            Math.floor(visibleTimeScale.tpTimeToPx(details.wakeupTs));
407        ctx.beginPath();
408        ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
409        ctx.fillStyle = 'black';
410        ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2);
411        ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8);
412        ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2);
413        ctx.fill();
414        ctx.closePath();
415      }
416    }
417
418    const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
419    if (hoveredThread !== undefined && this.mousePos !== undefined) {
420      const tidText = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
421      if (hoveredThread.pid) {
422        const pidText = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`;
423        this.drawTrackHoverTooltip(ctx, this.mousePos, pidText, tidText);
424      } else {
425        this.drawTrackHoverTooltip(ctx, this.mousePos, tidText);
426      }
427    }
428  }
429
430  onMouseMove(pos: {x: number, y: number}) {
431    const data = this.data();
432    this.mousePos = pos;
433    if (data === undefined) return;
434    const {visibleTimeScale} = globals.frontendLocalState;
435    if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
436      this.utidHoveredInThisTrack = -1;
437      globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
438      return;
439    }
440    const t = visibleTimeScale.pxToHpTime(pos.x).seconds;
441    let hoveredUtid = -1;
442
443    for (let i = 0; i < data.starts.length; i++) {
444      const tStart = data.starts[i];
445      const tEnd = data.ends[i];
446      const utid = data.utids[i];
447      if (tStart <= t && t <= tEnd) {
448        hoveredUtid = utid;
449        break;
450      }
451    }
452    this.utidHoveredInThisTrack = hoveredUtid;
453    const threadInfo = globals.threads.get(hoveredUtid);
454    const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
455    globals.dispatch(
456        Actions.setHoveredUtidAndPid({utid: hoveredUtid, pid: hoveredPid}));
457  }
458
459  onMouseOut() {
460    this.utidHoveredInThisTrack = -1;
461    globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
462    this.mousePos = undefined;
463  }
464
465  onMouseClick({x}: {x: number}) {
466    const data = this.data();
467    if (data === undefined) return false;
468    const {visibleTimeScale} = globals.frontendLocalState;
469    const time = visibleTimeScale.pxToHpTime(x).seconds;
470    const index = search(data.starts, time);
471    const id = index === -1 ? undefined : data.ids[index];
472    if (!id || this.utidHoveredInThisTrack === -1) return false;
473    globals.makeSelection(
474        Actions.selectSlice({id, trackId: this.trackState.id}));
475    return true;
476  }
477}
478
479function activate(ctx: PluginContext) {
480  ctx.registerTrackController(CpuSliceTrackController);
481  ctx.registerTrack(CpuSliceTrack);
482}
483
484export const plugin = {
485  pluginId: 'perfetto.CpuSlices',
486  activate,
487};
488