• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2023 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 {searchEq, searchRange} from '../../base/binary_search';
17import {assertExists, assertTrue} from '../../base/logging';
18import {duration, time, Time} from '../../base/time';
19import {drawTrackHoverTooltip} from '../../base/canvas_utils';
20import {Color} from '../../base/color';
21import {colorForThread} from '../../components/colorizer';
22import {TrackData} from '../../components/tracks/track_data';
23import {TimelineFetcher} from '../../components/tracks/track_helper';
24import {checkerboardExcept} from '../../components/checkerboard';
25import {TrackRenderer} from '../../public/track';
26import {LONG, NUM, QueryResult} from '../../trace_processor/query_result';
27import {uuidv4Sql} from '../../base/uuid';
28import {TrackMouseEvent, TrackRenderContext} from '../../public/track';
29import {Point2D} from '../../base/geom';
30import {Trace} from '../../public/trace';
31import {ThreadMap} from '../dev.perfetto.Thread/threads';
32import {AsyncDisposableStack} from '../../base/disposable_stack';
33import {
34  createPerfettoTable,
35  createVirtualTable,
36} from '../../trace_processor/sql_utils';
37
38export const PROCESS_SCHEDULING_TRACK_KIND = 'ProcessSchedulingTrack';
39
40const MARGIN_TOP = 5;
41const RECT_HEIGHT = 30;
42const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
43
44interface Data extends TrackData {
45  kind: 'slice';
46  maxCpu: number;
47
48  // Slices are stored in a columnar fashion. All fields have the same length.
49  starts: BigInt64Array;
50  ends: BigInt64Array;
51  utids: Uint32Array;
52  cpus: Uint32Array;
53}
54
55export interface Config {
56  pidForColor: number;
57  upid: number | null;
58  utid: number | null;
59}
60
61export class ProcessSchedulingTrack implements TrackRenderer {
62  private mousePos?: Point2D;
63  private utidHoveredInThisTrack = -1;
64  private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
65  private trackUuid = uuidv4Sql();
66
67  constructor(
68    private readonly trace: Trace,
69    private readonly config: Config,
70    private readonly cpuCount: number,
71    private readonly threads: ThreadMap,
72  ) {}
73
74  async onCreate(): Promise<void> {
75    const getQuery = () => {
76      if (this.config.upid !== null) {
77        // TODO(lalitm): remove the harcoding of the cross join here.
78        return `
79          select
80            s.id,
81            s.ts,
82            s.dur,
83            s.cpu
84          from thread t
85          cross join sched s using (utid)
86          where
87            not t.is_idle and
88            t.upid = ${this.config.upid}
89          order by ts
90        `;
91      }
92      assertExists(this.config.utid);
93      return `
94        select
95          s.id,
96          s.ts,
97          s.dur,
98          s.cpu
99        from sched s
100        where
101          s.utid = ${this.config.utid}
102      `;
103    };
104
105    const trash = new AsyncDisposableStack();
106    trash.use(
107      await createPerfettoTable(
108        this.trace.engine,
109        `tmp_${this.trackUuid}`,
110        getQuery(),
111      ),
112    );
113    await createVirtualTable(
114      this.trace.engine,
115      `process_scheduling_${this.trackUuid}`,
116      `__intrinsic_slice_mipmap((
117        select
118          s.id,
119          s.ts,
120          iif(
121            s.dur = -1,
122            ifnull(
123              (
124                select n.ts
125                from tmp_${this.trackUuid} n
126                where n.ts > s.ts and n.cpu = s.cpu
127                order by ts
128                limit 1
129              ),
130              trace_end()
131            ) - s.ts,
132            s.dur
133          ) as dur,
134          s.cpu as depth
135        from tmp_${this.trackUuid} s
136      ))`,
137    );
138    await trash.asyncDispose();
139  }
140
141  async onUpdate({
142    visibleWindow,
143    resolution,
144  }: TrackRenderContext): Promise<void> {
145    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
146  }
147
148  async onDestroy(): Promise<void> {
149    this.fetcher[Symbol.dispose]();
150    await this.trace.engine.tryQuery(`
151      drop table process_scheduling_${this.trackUuid}
152    `);
153  }
154
155  async onBoundsChange(
156    start: time,
157    end: time,
158    resolution: duration,
159  ): Promise<Data> {
160    // Resolution must always be a power of 2 for this logic to work
161    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
162
163    const queryRes = await this.queryData(start, end, resolution);
164    const numRows = queryRes.numRows();
165    const slices: Data = {
166      kind: 'slice',
167      start,
168      end,
169      resolution,
170      length: numRows,
171      maxCpu: this.cpuCount,
172      starts: new BigInt64Array(numRows),
173      ends: new BigInt64Array(numRows),
174      cpus: new Uint32Array(numRows),
175      utids: new Uint32Array(numRows),
176    };
177
178    const it = queryRes.iter({
179      ts: LONG,
180      dur: LONG,
181      cpu: NUM,
182      utid: NUM,
183    });
184
185    for (let row = 0; it.valid(); it.next(), row++) {
186      const start = Time.fromRaw(it.ts);
187      const dur = it.dur;
188      const end = Time.add(start, dur);
189
190      slices.starts[row] = start;
191      slices.ends[row] = end;
192      slices.cpus[row] = it.cpu;
193      slices.utids[row] = it.utid;
194      slices.end = Time.max(end, slices.end);
195    }
196    return slices;
197  }
198
199  private async queryData(
200    start: time,
201    end: time,
202    bucketSize: duration,
203  ): Promise<QueryResult> {
204    return this.trace.engine.query(`
205      select
206        (z.ts / ${bucketSize}) * ${bucketSize} as ts,
207        iif(s.dur = -1, s.dur, max(z.dur, ${bucketSize})) as dur,
208        s.id,
209        z.depth as cpu,
210        utid
211      from process_scheduling_${this.trackUuid}(
212        ${start}, ${end}, ${bucketSize}
213      ) z
214      cross join sched s using (id)
215    `);
216  }
217
218  getHeight(): number {
219    return TRACK_HEIGHT;
220  }
221
222  render({ctx, size, timescale, visibleWindow}: TrackRenderContext): void {
223    // TODO: fonts and colors should come from the CSS and not hardcoded here.
224    const data = this.fetcher.data;
225
226    if (data === undefined) return; // Can't possibly draw anything.
227
228    // If the cached trace slices don't fully cover the visible time range,
229    // show a gray rectangle with a "Loading..." label.
230    checkerboardExcept(
231      ctx,
232      this.getHeight(),
233      0,
234      size.width,
235      timescale.timeToPx(data.start),
236      timescale.timeToPx(data.end),
237    );
238
239    assertTrue(data.starts.length === data.ends.length);
240    assertTrue(data.starts.length === data.utids.length);
241
242    const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu);
243
244    for (let i = 0; i < data.ends.length; i++) {
245      const tStart = Time.fromRaw(data.starts[i]);
246      const tEnd = Time.fromRaw(data.ends[i]);
247
248      // Cull slices that lie completely outside the visible window
249      if (!visibleWindow.overlaps(tStart, tEnd)) continue;
250
251      const utid = data.utids[i];
252      const cpu = data.cpus[i];
253
254      const rectStart = Math.floor(timescale.timeToPx(tStart));
255      const rectEnd = Math.floor(timescale.timeToPx(tEnd));
256      const rectWidth = Math.max(1, rectEnd - rectStart);
257
258      const threadInfo = this.threads.get(utid);
259      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
260      const pid = (threadInfo ? threadInfo.pid : -1) || -1;
261
262      const isHovering = this.trace.timeline.hoveredUtid !== undefined;
263      const isThreadHovered = this.trace.timeline.hoveredUtid === utid;
264      const isProcessHovered = this.trace.timeline.hoveredPid === pid;
265      const colorScheme = colorForThread(threadInfo);
266      let color: Color;
267      if (isHovering && !isThreadHovered) {
268        if (!isProcessHovered) {
269          color = colorScheme.disabled;
270        } else {
271          color = colorScheme.variant;
272        }
273      } else {
274        color = colorScheme.base;
275      }
276      ctx.fillStyle = color.cssString;
277      const y = MARGIN_TOP + cpuTrackHeight * cpu + cpu;
278      ctx.fillRect(rectStart, y, rectWidth, cpuTrackHeight);
279    }
280
281    const hoveredThread = this.threads.get(this.utidHoveredInThisTrack);
282    if (hoveredThread !== undefined && this.mousePos !== undefined) {
283      const tidText = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
284      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
285      if (hoveredThread.pid) {
286        const pidText = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`;
287        drawTrackHoverTooltip(ctx, this.mousePos, size, pidText, tidText);
288      } else {
289        drawTrackHoverTooltip(ctx, this.mousePos, size, tidText);
290      }
291    }
292  }
293
294  onMouseMove({x, y, timescale}: TrackMouseEvent) {
295    const data = this.fetcher.data;
296    this.mousePos = {x, y};
297    if (data === undefined) return;
298    if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
299      this.utidHoveredInThisTrack = -1;
300      this.trace.timeline.hoveredUtid = undefined;
301      this.trace.timeline.hoveredPid = undefined;
302      return;
303    }
304
305    const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu);
306    const cpu = Math.floor((y - MARGIN_TOP) / (cpuTrackHeight + 1));
307    const t = timescale.pxToHpTime(x).toTime('floor');
308
309    const [i, j] = searchRange(data.starts, t, searchEq(data.cpus, cpu));
310    if (i === j || i >= data.starts.length || t > data.ends[i]) {
311      this.utidHoveredInThisTrack = -1;
312      this.trace.timeline.hoveredUtid = undefined;
313      this.trace.timeline.hoveredPid = undefined;
314      return;
315    }
316
317    const utid = data.utids[i];
318    this.utidHoveredInThisTrack = utid;
319    const threadInfo = this.threads.get(utid);
320    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
321    const pid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
322    this.trace.timeline.hoveredUtid = utid;
323    this.trace.timeline.hoveredPid = pid;
324  }
325
326  onMouseOut() {
327    this.utidHoveredInThisTrack = -1;
328    this.trace.timeline.hoveredUtid = undefined;
329    this.trace.timeline.hoveredPid = undefined;
330    this.mousePos = undefined;
331  }
332}
333