xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2024 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 as BIMath} from '../../base/bigint_math';
16import {search, searchEq, searchSegment} from '../../base/binary_search';
17import {assertExists, assertTrue} from '../../base/logging';
18import {Duration, duration, Time, time} from '../../base/time';
19import {
20  drawDoubleHeadedArrow,
21  drawIncompleteSlice,
22  drawTrackHoverTooltip,
23} from '../../base/canvas_utils';
24import {cropText} from '../../base/string_utils';
25import {Color} from '../../public/color';
26import {colorForThread} from '../../components/colorizer';
27import {TrackData} from '../../components/tracks/track_data';
28import {TimelineFetcher} from '../../components/tracks/track_helper';
29import {checkerboardExcept} from '../../components/checkerboard';
30import {Point2D} from '../../base/geom';
31import {Track} from '../../public/track';
32import {LONG, NUM} from '../../trace_processor/query_result';
33import {uuidv4Sql} from '../../base/uuid';
34import {TrackMouseEvent, TrackRenderContext} from '../../public/track';
35import {TrackEventDetails} from '../../public/selection';
36import {asSchedSqlId} from '../../components/sql_utils/core_types';
37import {getSched, getSchedWakeupInfo} from '../../components/sql_utils/sched';
38import {SchedSliceDetailsPanel} from './sched_details_tab';
39import {Trace} from '../../public/trace';
40import {exists} from '../../base/utils';
41import {ThreadMap} from '../dev.perfetto.Thread/threads';
42import {Dataset, SourceDataset} from '../../trace_processor/dataset';
43
44export interface Data extends TrackData {
45  // Slices are stored in a columnar fashion. All fields have the same length.
46  ids: Float64Array;
47  startQs: BigInt64Array;
48  endQs: BigInt64Array;
49  utids: Uint32Array;
50  flags: Uint8Array;
51  lastRowId: number;
52}
53
54const MARGIN_TOP = 3;
55const RECT_HEIGHT = 24;
56const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
57
58const CPU_SLICE_FLAGS_INCOMPLETE = 1;
59const CPU_SLICE_FLAGS_REALTIME = 2;
60
61export class CpuSliceTrack implements Track {
62  private mousePos?: Point2D;
63  private utidHoveredInThisTrack?: number;
64  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
65
66  private lastRowId = -1;
67  private trackUuid = uuidv4Sql();
68
69  constructor(
70    private readonly trace: Trace,
71    private readonly uri: string,
72    private readonly cpu: number,
73    private readonly threads: ThreadMap,
74  ) {}
75
76  async onCreate() {
77    await this.trace.engine.query(`
78      create virtual table cpu_slice_${this.trackUuid}
79      using __intrinsic_slice_mipmap((
80        select
81          id,
82          ts,
83          iif(dur = -1, lead(ts, 1, trace_end()) over (order by ts) - ts, dur),
84          0 as depth
85        from sched
86        where cpu = ${this.cpu} and utid != 0
87      ));
88    `);
89    const it = await this.trace.engine.query(`
90      select coalesce(max(id), -1) as lastRowId
91      from sched
92      where cpu = ${this.cpu} and utid != 0
93    `);
94    this.lastRowId = it.firstRow({lastRowId: NUM}).lastRowId;
95  }
96
97  getDataset(): Dataset | undefined {
98    return new SourceDataset({
99      src: 'select id, ts, dur, cpu from sched where utid != 0',
100      schema: {
101        id: NUM,
102        ts: LONG,
103        dur: LONG,
104      },
105      filter: {
106        col: 'cpu',
107        eq: this.cpu,
108      },
109    });
110  }
111
112  async onUpdate({
113    visibleWindow,
114    resolution,
115  }: TrackRenderContext): Promise<void> {
116    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
117  }
118
119  async onBoundsChange(
120    start: time,
121    end: time,
122    resolution: duration,
123  ): Promise<Data> {
124    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
125
126    const queryRes = await this.trace.engine.query(`
127      select
128        (z.ts / ${resolution}) * ${resolution} as tsQ,
129        (((z.ts + z.dur) / ${resolution}) + 1) * ${resolution} as tsEndQ,
130        s.utid,
131        s.id,
132        s.dur = -1 as isIncomplete,
133        ifnull(s.priority < 100, 0) as isRealtime
134      from cpu_slice_${this.trackUuid}(${start}, ${end}, ${resolution}) z
135      cross join sched s using (id)
136    `);
137
138    const numRows = queryRes.numRows();
139    const slices: Data = {
140      start,
141      end,
142      resolution,
143      length: numRows,
144      lastRowId: this.lastRowId,
145      ids: new Float64Array(numRows),
146      startQs: new BigInt64Array(numRows),
147      endQs: new BigInt64Array(numRows),
148      utids: new Uint32Array(numRows),
149      flags: new Uint8Array(numRows),
150    };
151
152    const it = queryRes.iter({
153      tsQ: LONG,
154      tsEndQ: LONG,
155      utid: NUM,
156      id: NUM,
157      isIncomplete: NUM,
158      isRealtime: NUM,
159    });
160    for (let row = 0; it.valid(); it.next(), row++) {
161      slices.startQs[row] = it.tsQ;
162      slices.endQs[row] = it.tsEndQ;
163      slices.utids[row] = it.utid;
164      slices.ids[row] = it.id;
165
166      slices.flags[row] = 0;
167      if (it.isIncomplete) {
168        slices.flags[row] |= CPU_SLICE_FLAGS_INCOMPLETE;
169      }
170      if (it.isRealtime) {
171        slices.flags[row] |= CPU_SLICE_FLAGS_REALTIME;
172      }
173    }
174    return slices;
175  }
176
177  async onDestroy() {
178    await this.trace.engine.tryQuery(
179      `drop table if exists cpu_slice_${this.trackUuid}`,
180    );
181    this.fetcher[Symbol.dispose]();
182  }
183
184  getHeight(): number {
185    return TRACK_HEIGHT;
186  }
187
188  render(trackCtx: TrackRenderContext): void {
189    const {ctx, size, timescale} = trackCtx;
190
191    // TODO: fonts and colors should come from the CSS and not hardcoded here.
192    const data = this.fetcher.data;
193
194    if (data === undefined) return; // Can't possibly draw anything.
195
196    // If the cached trace slices don't fully cover the visible time range,
197    // show a gray rectangle with a "Loading..." label.
198    checkerboardExcept(
199      ctx,
200      this.getHeight(),
201      0,
202      size.width,
203      timescale.timeToPx(data.start),
204      timescale.timeToPx(data.end),
205    );
206
207    this.renderSlices(trackCtx, data);
208  }
209
210  renderSlices(
211    {ctx, timescale, size, visibleWindow}: TrackRenderContext,
212    data: Data,
213  ): void {
214    assertTrue(data.startQs.length === data.endQs.length);
215    assertTrue(data.startQs.length === data.utids.length);
216
217    const visWindowEndPx = size.width;
218
219    ctx.textAlign = 'center';
220    ctx.font = '12px Roboto Condensed';
221    const charWidth = ctx.measureText('dbpqaouk').width / 8;
222
223    const timespan = visibleWindow.toTimeSpan();
224
225    const startTime = timespan.start;
226    const endTime = timespan.end;
227
228    const rawStartIdx = data.endQs.findIndex((end) => end >= startTime);
229    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
230
231    const [, rawEndIdx] = searchSegment(data.startQs, endTime);
232    const endIdx = rawEndIdx === -1 ? data.startQs.length : rawEndIdx;
233
234    for (let i = startIdx; i < endIdx; i++) {
235      const tStart = Time.fromRaw(data.startQs[i]);
236      let tEnd = Time.fromRaw(data.endQs[i]);
237      const utid = data.utids[i];
238
239      // If the last slice is incomplete, it should end with the end of the
240      // window, else it might spill over the window and the end would not be
241      // visible as a zigzag line.
242      if (
243        data.ids[i] === data.lastRowId &&
244        data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE
245      ) {
246        tEnd = endTime;
247      }
248      const rectStart = timescale.timeToPx(tStart);
249      const rectEnd = timescale.timeToPx(tEnd);
250      const rectWidth = Math.max(1, rectEnd - rectStart);
251
252      const threadInfo = this.threads.get(utid);
253      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
254      const pid = threadInfo && threadInfo.pid ? threadInfo.pid : -1;
255
256      const isHovering = this.trace.timeline.hoveredUtid !== undefined;
257      const isThreadHovered = this.trace.timeline.hoveredUtid === utid;
258      const isProcessHovered = this.trace.timeline.hoveredPid === pid;
259      const colorScheme = colorForThread(threadInfo);
260      let color: Color;
261      let textColor: Color;
262      if (isHovering && !isThreadHovered) {
263        if (!isProcessHovered) {
264          color = colorScheme.disabled;
265          textColor = colorScheme.textDisabled;
266        } else {
267          color = colorScheme.variant;
268          textColor = colorScheme.textVariant;
269        }
270      } else {
271        color = colorScheme.base;
272        textColor = colorScheme.textBase;
273      }
274      ctx.fillStyle = color.cssString;
275
276      if (data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE) {
277        drawIncompleteSlice(ctx, rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
278      } else {
279        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
280      }
281
282      // Don't render text when we have less than 5px to play with.
283      if (rectWidth < 5) continue;
284
285      // Stylize real-time threads. We don't do it when zoomed out as the
286      // fillRect is expensive.
287      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
288        ctx.fillStyle = getHatchedPattern(ctx);
289        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
290      }
291
292      // TODO: consider de-duplicating this code with the copied one from
293      // chrome_slices/frontend.ts.
294      let title = `[utid:${utid}]`;
295      let subTitle = '';
296      if (threadInfo) {
297        if (threadInfo.pid !== undefined && threadInfo.pid !== 0) {
298          let procName = threadInfo.procName ?? '';
299          if (procName.startsWith('/')) {
300            // Remove folder paths from name
301            procName = procName.substring(procName.lastIndexOf('/') + 1);
302          }
303          title = `${procName} [${threadInfo.pid}]`;
304          subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`;
305        } else {
306          title = `${threadInfo.threadName} [${threadInfo.tid}]`;
307        }
308      }
309
310      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
311        subTitle = subTitle + ' (RT)';
312      }
313
314      const right = Math.min(visWindowEndPx, rectEnd);
315      const left = Math.max(rectStart, 0);
316      const visibleWidth = Math.max(right - left, 1);
317      title = cropText(title, charWidth, visibleWidth);
318      subTitle = cropText(subTitle, charWidth, visibleWidth);
319      const rectXCenter = left + visibleWidth / 2;
320      ctx.fillStyle = textColor.cssString;
321      ctx.font = '12px Roboto Condensed';
322      ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 1);
323      ctx.fillStyle = textColor.setAlpha(0.6).cssString;
324      ctx.font = '10px Roboto Condensed';
325      ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 9);
326    }
327
328    const selection = this.trace.selection.selection;
329    if (selection.kind === 'track_event') {
330      if (selection.trackUri === this.uri) {
331        const [startIndex, endIndex] = searchEq(data.ids, selection.eventId);
332        if (startIndex !== endIndex) {
333          const tStart = Time.fromRaw(data.startQs[startIndex]);
334          const tEnd = Time.fromRaw(data.endQs[startIndex]);
335          const utid = data.utids[startIndex];
336          const color = colorForThread(this.threads.get(utid));
337          const rectStart = timescale.timeToPx(tStart);
338          const rectEnd = timescale.timeToPx(tEnd);
339          const rectWidth = Math.max(1, rectEnd - rectStart);
340
341          // Draw a rectangle around the slice that is currently selected.
342          ctx.strokeStyle = color.base.setHSL({l: 30}).cssString;
343          ctx.beginPath();
344          ctx.lineWidth = 3;
345          ctx.strokeRect(
346            rectStart,
347            MARGIN_TOP - 1.5,
348            rectWidth,
349            RECT_HEIGHT + 3,
350          );
351          ctx.closePath();
352          // Draw arrow from wakeup time of current slice.
353          if (selection.wakeupTs) {
354            const wakeupPos = timescale.timeToPx(selection.wakeupTs);
355            const latencyWidth = rectStart - wakeupPos;
356            drawDoubleHeadedArrow(
357              ctx,
358              wakeupPos,
359              MARGIN_TOP + RECT_HEIGHT,
360              latencyWidth,
361              latencyWidth >= 20,
362            );
363            // Latency time with a white semi-transparent background.
364            const latency = tStart - selection.wakeupTs;
365            const displayText = Duration.humanise(latency);
366            const measured = ctx.measureText(displayText);
367            if (latencyWidth >= measured.width + 2) {
368              ctx.fillStyle = 'rgba(255,255,255,0.7)';
369              ctx.fillRect(
370                wakeupPos + latencyWidth / 2 - measured.width / 2 - 1,
371                MARGIN_TOP + RECT_HEIGHT - 12,
372                measured.width + 2,
373                11,
374              );
375              ctx.textBaseline = 'bottom';
376              ctx.fillStyle = 'black';
377              ctx.fillText(
378                displayText,
379                wakeupPos + latencyWidth / 2,
380                MARGIN_TOP + RECT_HEIGHT - 1,
381              );
382            }
383          }
384        }
385      }
386
387      // Draw diamond if the track being drawn is the cpu of the waker.
388      if (this.cpu === selection.wakerCpu && selection.wakeupTs) {
389        const wakeupPos = Math.floor(timescale.timeToPx(selection.wakeupTs));
390        ctx.beginPath();
391        ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
392        ctx.fillStyle = 'black';
393        ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2);
394        ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8);
395        ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2);
396        ctx.fill();
397        ctx.closePath();
398      }
399
400      if (this.utidHoveredInThisTrack !== undefined) {
401        const hoveredThread = this.threads.get(this.utidHoveredInThisTrack);
402        if (hoveredThread && this.mousePos !== undefined) {
403          const tidText = `T: ${hoveredThread.threadName}
404          [${hoveredThread.tid}]`;
405          // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
406          if (hoveredThread.pid) {
407            const pidText = `P: ${hoveredThread.procName}
408            [${hoveredThread.pid}]`;
409            drawTrackHoverTooltip(ctx, this.mousePos, size, pidText, tidText);
410          } else {
411            drawTrackHoverTooltip(ctx, this.mousePos, size, tidText);
412          }
413        }
414      }
415    }
416  }
417
418  onMouseMove({x, y, timescale}: TrackMouseEvent) {
419    const data = this.fetcher.data;
420    this.mousePos = {x, y};
421    if (data === undefined) return;
422    if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
423      this.utidHoveredInThisTrack = undefined;
424      this.trace.timeline.hoveredUtid = undefined;
425      this.trace.timeline.hoveredPid = undefined;
426      return;
427    }
428    const t = timescale.pxToHpTime(x);
429    let hoveredUtid = undefined;
430
431    for (let i = 0; i < data.startQs.length; i++) {
432      const tStart = Time.fromRaw(data.startQs[i]);
433      const tEnd = Time.fromRaw(data.endQs[i]);
434      const utid = data.utids[i];
435      if (t.gte(tStart) && t.lt(tEnd)) {
436        hoveredUtid = utid;
437        break;
438      }
439    }
440    this.utidHoveredInThisTrack = hoveredUtid;
441    const threadInfo = exists(hoveredUtid) && this.threads.get(hoveredUtid);
442    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
443    const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
444    this.trace.timeline.hoveredUtid = hoveredUtid;
445    this.trace.timeline.hoveredPid = hoveredPid;
446  }
447
448  onMouseOut() {
449    this.utidHoveredInThisTrack = -1;
450    this.trace.timeline.hoveredUtid = undefined;
451    this.trace.timeline.hoveredPid = undefined;
452    this.mousePos = undefined;
453  }
454
455  onMouseClick({x, timescale}: TrackMouseEvent) {
456    const data = this.fetcher.data;
457    if (data === undefined) return false;
458    const time = timescale.pxToHpTime(x);
459    const index = search(data.startQs, time.toTime());
460    const id = index === -1 ? undefined : data.ids[index];
461    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
462    if (!id || this.utidHoveredInThisTrack === -1) return false;
463
464    this.trace.selection.selectTrackEvent(this.uri, id);
465    return true;
466  }
467
468  async getSelectionDetails?(
469    eventId: number,
470  ): Promise<TrackEventDetails | undefined> {
471    const sched = await getSched(this.trace.engine, asSchedSqlId(eventId));
472    if (sched === undefined) {
473      return undefined;
474    }
475    const wakeup = await getSchedWakeupInfo(this.trace.engine, sched);
476    return {
477      ts: sched.ts,
478      dur: sched.dur,
479      wakeupTs: wakeup?.wakeupTs,
480      wakerCpu: wakeup?.wakerCpu,
481    };
482  }
483
484  detailsPanel() {
485    return new SchedSliceDetailsPanel(this.trace, this.threads);
486  }
487}
488
489// Creates a diagonal hatched pattern to be used for distinguishing slices with
490// real-time priorities. The pattern is created once as an offscreen canvas and
491// is kept cached inside the Context2D of the main canvas, without making
492// assumptions on the lifetime of the main canvas.
493function getHatchedPattern(mainCtx: CanvasRenderingContext2D): CanvasPattern {
494  const mctx = mainCtx as CanvasRenderingContext2D & {
495    sliceHatchedPattern?: CanvasPattern;
496  };
497  if (mctx.sliceHatchedPattern !== undefined) return mctx.sliceHatchedPattern;
498  const canvas = document.createElement('canvas');
499  const SIZE = 8;
500  canvas.width = canvas.height = SIZE;
501  const ctx = assertExists(canvas.getContext('2d'));
502  ctx.strokeStyle = 'rgba(255,255,255,0.3)';
503  ctx.beginPath();
504  ctx.lineWidth = 1;
505  ctx.moveTo(0, SIZE);
506  ctx.lineTo(SIZE, 0);
507  ctx.stroke();
508  mctx.sliceHatchedPattern = assertExists(mctx.createPattern(canvas, 'repeat'));
509  return mctx.sliceHatchedPattern;
510}
511