• 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 {Actions} from '../../common/actions';
20import {drawTrackHoverTooltip} from '../../common/canvas_utils';
21import {Color} from '../../core/color';
22import {colorForThread} from '../../core/colorizer';
23import {TrackData} from '../../common/track_data';
24import {TimelineFetcher} from '../../common/track_helper';
25import {checkerboardExcept} from '../../frontend/checkerboard';
26import {globals} from '../../frontend/globals';
27import {PanelSize} from '../../frontend/panel';
28import {Engine, Track} from '../../public';
29import {LONG, NUM, QueryResult} from '../../trace_processor/query_result';
30import {uuidv4Sql} from '../../base/uuid';
31
32export const PROCESS_SCHEDULING_TRACK_KIND = 'ProcessSchedulingTrack';
33
34const MARGIN_TOP = 5;
35const RECT_HEIGHT = 30;
36const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
37
38interface Data extends TrackData {
39  kind: 'slice';
40  maxCpu: number;
41
42  // Slices are stored in a columnar fashion. All fields have the same length.
43  starts: BigInt64Array;
44  ends: BigInt64Array;
45  utids: Uint32Array;
46  cpus: Uint32Array;
47}
48
49export interface Config {
50  pidForColor: number;
51  upid: number | null;
52  utid: number | null;
53}
54
55export class ProcessSchedulingTrack implements Track {
56  private mousePos?: {x: number; y: number};
57  private utidHoveredInThisTrack = -1;
58  private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
59  private cpuCount: number;
60  private engine: Engine;
61  private trackUuid = uuidv4Sql();
62  private config: Config;
63
64  constructor(engine: Engine, config: Config, cpuCount: number) {
65    this.engine = engine;
66    this.config = config;
67    this.cpuCount = cpuCount;
68  }
69
70  async onCreate(): Promise<void> {
71    if (this.config.upid !== null) {
72      await this.engine.query(`
73        create virtual table process_scheduling_${this.trackUuid}
74        using __intrinsic_slice_mipmap((
75          select
76            id,
77            ts,
78            iif(
79              dur = -1,
80              lead(ts, 1, trace_end()) over (partition by cpu order by ts) - ts,
81              dur
82            ) as dur,
83            cpu as depth
84          from experimental_sched_upid
85          where
86            utid != 0 and
87            upid = ${this.config.upid}
88        ));
89      `);
90    } else {
91      assertExists(this.config.utid);
92      await this.engine.query(`
93        create virtual table process_scheduling_${this.trackUuid}
94        using __intrinsic_slice_mipmap((
95          select
96            id,
97            ts,
98            iif(
99              dur = -1,
100              lead(ts, 1, trace_end()) over (partition by cpu order by ts) - ts,
101              dur
102            ) as dur,
103            cpu as depth
104          from sched
105          where utid = ${this.config.utid}
106        ));
107      `);
108    }
109  }
110
111  async onUpdate(): Promise<void> {
112    await this.fetcher.requestDataForCurrentTime();
113  }
114
115  async onDestroy(): Promise<void> {
116    this.fetcher.dispose();
117    await this.engine.tryQuery(`
118      drop table process_scheduling_${this.trackUuid}
119    `);
120  }
121
122  async onBoundsChange(
123    start: time,
124    end: time,
125    resolution: duration,
126  ): Promise<Data> {
127    // Resolution must always be a power of 2 for this logic to work
128    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
129
130    const queryRes = await this.queryData(start, end, resolution);
131    const numRows = queryRes.numRows();
132    const slices: Data = {
133      kind: 'slice',
134      start,
135      end,
136      resolution,
137      length: numRows,
138      maxCpu: this.cpuCount,
139      starts: new BigInt64Array(numRows),
140      ends: new BigInt64Array(numRows),
141      cpus: new Uint32Array(numRows),
142      utids: new Uint32Array(numRows),
143    };
144
145    const it = queryRes.iter({
146      ts: LONG,
147      dur: LONG,
148      cpu: NUM,
149      utid: NUM,
150    });
151
152    for (let row = 0; it.valid(); it.next(), row++) {
153      const start = Time.fromRaw(it.ts);
154      const dur = it.dur;
155      const end = Time.add(start, dur);
156
157      slices.starts[row] = start;
158      slices.ends[row] = end;
159      slices.cpus[row] = it.cpu;
160      slices.utids[row] = it.utid;
161      slices.end = Time.max(end, slices.end);
162    }
163    return slices;
164  }
165
166  private async queryData(
167    start: time,
168    end: time,
169    bucketSize: duration,
170  ): Promise<QueryResult> {
171    return this.engine.query(`
172      select
173        (z.ts / ${bucketSize}) * ${bucketSize} as ts,
174        iif(s.dur = -1, s.dur, max(z.dur, ${bucketSize})) as dur,
175        s.id,
176        z.depth as cpu,
177        utid
178      from process_scheduling_${this.trackUuid}(
179        ${start}, ${end}, ${bucketSize}
180      ) z
181      cross join sched s using (id)
182    `);
183  }
184
185  getHeight(): number {
186    return TRACK_HEIGHT;
187  }
188
189  render(ctx: CanvasRenderingContext2D, size: PanelSize): void {
190    // TODO: fonts and colors should come from the CSS and not hardcoded here.
191    const {visibleTimeScale, visibleTimeSpan} = globals.timeline;
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      visibleTimeScale.timeToPx(data.start),
204      visibleTimeScale.timeToPx(data.end),
205    );
206
207    assertTrue(data.starts.length === data.ends.length);
208    assertTrue(data.starts.length === data.utids.length);
209
210    const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu);
211
212    for (let i = 0; i < data.ends.length; i++) {
213      const tStart = Time.fromRaw(data.starts[i]);
214      const tEnd = Time.fromRaw(data.ends[i]);
215
216      // Cull slices that lie completely outside the visible window
217      if (!visibleTimeSpan.intersects(tStart, tEnd)) continue;
218
219      const utid = data.utids[i];
220      const cpu = data.cpus[i];
221
222      const rectStart = Math.floor(visibleTimeScale.timeToPx(tStart));
223      const rectEnd = Math.floor(visibleTimeScale.timeToPx(tEnd));
224      const rectWidth = Math.max(1, rectEnd - rectStart);
225
226      const threadInfo = globals.threads.get(utid);
227      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
228      const pid = (threadInfo ? threadInfo.pid : -1) || -1;
229
230      const isHovering = globals.state.hoveredUtid !== -1;
231      const isThreadHovered = globals.state.hoveredUtid === utid;
232      const isProcessHovered = globals.state.hoveredPid === pid;
233      const colorScheme = colorForThread(threadInfo);
234      let color: Color;
235      if (isHovering && !isThreadHovered) {
236        if (!isProcessHovered) {
237          color = colorScheme.disabled;
238        } else {
239          color = colorScheme.variant;
240        }
241      } else {
242        color = colorScheme.base;
243      }
244      ctx.fillStyle = color.cssString;
245      const y = MARGIN_TOP + cpuTrackHeight * cpu + cpu;
246      ctx.fillRect(rectStart, y, rectWidth, cpuTrackHeight);
247    }
248
249    const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
250    const height = this.getHeight();
251    if (hoveredThread !== undefined && this.mousePos !== undefined) {
252      const tidText = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
253      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
254      if (hoveredThread.pid) {
255        const pidText = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`;
256        drawTrackHoverTooltip(ctx, this.mousePos, height, pidText, tidText);
257      } else {
258        drawTrackHoverTooltip(ctx, this.mousePos, height, tidText);
259      }
260    }
261  }
262
263  onMouseMove(pos: {x: number; y: number}) {
264    const data = this.fetcher.data;
265    this.mousePos = pos;
266    if (data === undefined) return;
267    if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
268      this.utidHoveredInThisTrack = -1;
269      globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
270      return;
271    }
272
273    const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu);
274    const cpu = Math.floor((pos.y - MARGIN_TOP) / (cpuTrackHeight + 1));
275    const {visibleTimeScale} = globals.timeline;
276    const t = visibleTimeScale.pxToHpTime(pos.x).toTime('floor');
277
278    const [i, j] = searchRange(data.starts, t, searchEq(data.cpus, cpu));
279    if (i === j || i >= data.starts.length || t > data.ends[i]) {
280      this.utidHoveredInThisTrack = -1;
281      globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
282      return;
283    }
284
285    const utid = data.utids[i];
286    this.utidHoveredInThisTrack = utid;
287    const threadInfo = globals.threads.get(utid);
288    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
289    const pid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
290    globals.dispatch(Actions.setHoveredUtidAndPid({utid, pid}));
291  }
292
293  onMouseOut() {
294    this.utidHoveredInThisTrack = -1;
295    globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
296    this.mousePos = undefined;
297  }
298}
299