• 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} from '../../base/bigint_math';
16import {assertExists, assertTrue} from '../../base/logging';
17import {duration, Time, time} from '../../base/time';
18import {colorForTid} from '../../components/colorizer';
19import {TrackData} from '../../components/tracks/track_data';
20import {TimelineFetcher} from '../../components/tracks/track_helper';
21import {checkerboardExcept} from '../../components/checkerboard';
22import {Engine} from '../../trace_processor/engine';
23import {TrackRenderer} from '../../public/track';
24import {LONG, NUM} from '../../trace_processor/query_result';
25import {uuidv4Sql} from '../../base/uuid';
26import {TrackRenderContext} from '../../public/track';
27import {AsyncDisposableStack} from '../../base/disposable_stack';
28import {
29  createPerfettoTable,
30  createVirtualTable,
31} from '../../trace_processor/sql_utils';
32
33export const PROCESS_SUMMARY_TRACK = 'ProcessSummaryTrack';
34
35interface Data extends TrackData {
36  starts: BigInt64Array;
37  utilizations: Float64Array;
38}
39
40export interface Config {
41  pidForColor: number;
42  upid: number | null;
43  utid: number | null;
44}
45
46const MARGIN_TOP = 5;
47const RECT_HEIGHT = 30;
48const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
49const SUMMARY_HEIGHT = TRACK_HEIGHT - MARGIN_TOP;
50
51export class ProcessSummaryTrack implements TrackRenderer {
52  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
53  private engine: Engine;
54  private config: Config;
55  private uuid = uuidv4Sql();
56
57  constructor(engine: Engine, config: Config) {
58    this.engine = engine;
59    this.config = config;
60  }
61
62  async onCreate(): Promise<void> {
63    const getQuery = () => {
64      if (this.config.upid !== null) {
65        return `
66          select tt.id as track_id
67          from thread_track as tt
68          join _thread_available_info_summary using (utid)
69          join thread using (utid)
70          where thread.upid = ${this.config.upid}
71          order by slice_count desc
72        `;
73      }
74      return `
75        select tt.id as track_id
76        from thread_track as tt
77        join _thread_available_info_summary using (utid)
78        where tt.utid = ${assertExists(this.config.utid)}
79        order by slice_count desc
80      `;
81    };
82
83    const trash = new AsyncDisposableStack();
84    trash.use(
85      await createPerfettoTable(this.engine, `tmp_${this.uuid}`, getQuery()),
86    );
87    trash.use(
88      await createPerfettoTable(
89        this.engine,
90        `changes_${this.uuid}`,
91        `
92          select ts, 1.0 as value
93          from tmp_${this.uuid}
94          cross join slice using (track_id)
95          where slice.depth = 0
96          union all
97          select ts + dur as ts, -1.0 as value
98          from tmp_${this.uuid}
99          cross join slice using (track_id)
100          where slice.depth = 0
101        `,
102      ),
103    );
104    await createVirtualTable(
105      this.engine,
106      `process_summary_${this.uuid}`,
107      `__intrinsic_counter_mipmap((
108        select
109          ts,
110          sum(value) over (order by ts) / (
111            select count() from tmp_${this.uuid}
112          ) as value
113        from changes_${this.uuid}
114        order by ts
115      ))`,
116    );
117    await trash.asyncDispose();
118  }
119
120  async onUpdate({
121    visibleWindow,
122    resolution,
123  }: TrackRenderContext): Promise<void> {
124    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
125  }
126
127  async onBoundsChange(
128    start: time,
129    end: time,
130    resolution: duration,
131  ): Promise<Data> {
132    // Resolution must always be a power of 2 for this logic to work
133    assertTrue(
134      BigintMath.popcount(resolution) === 1,
135      `${resolution} not pow of 2`,
136    );
137
138    const queryRes = await this.engine.query(`
139      select last_ts as ts, last_value as utilization
140      from process_summary_${this.uuid}(${start}, ${end}, ${resolution});
141    `);
142    const numRows = queryRes.numRows();
143    const slices: Data = {
144      start,
145      end,
146      resolution,
147      length: numRows,
148      starts: new BigInt64Array(numRows),
149      utilizations: new Float64Array(numRows),
150    };
151    const it = queryRes.iter({
152      ts: LONG,
153      utilization: NUM,
154    });
155    for (let row = 0; it.valid(); it.next(), row++) {
156      slices.starts[row] = it.ts;
157      slices.utilizations[row] = it.utilization;
158    }
159    return slices;
160  }
161
162  async onDestroy(): Promise<void> {
163    await this.engine.tryQuery(
164      `drop table if exists process_summary_${this.uuid};`,
165    );
166    this.fetcher[Symbol.dispose]();
167  }
168
169  getHeight(): number {
170    return TRACK_HEIGHT;
171  }
172
173  render(trackCtx: TrackRenderContext): void {
174    const {ctx, size, timescale} = trackCtx;
175
176    const data = this.fetcher.data;
177    if (data === undefined) {
178      return;
179    }
180
181    // If the cached trace slices don't fully cover the visible time range,
182    // show a gray rectangle with a "Loading..." label.
183    checkerboardExcept(
184      ctx,
185      this.getHeight(),
186      0,
187      size.width,
188      timescale.timeToPx(data.start),
189      timescale.timeToPx(data.end),
190    );
191
192    this.renderSummary(trackCtx, data);
193  }
194
195  private renderSummary(
196    {ctx, timescale}: TrackRenderContext,
197    data: Data,
198  ): void {
199    const startPx = 0;
200    const bottomY = TRACK_HEIGHT;
201
202    let lastX = startPx;
203    let lastY = bottomY;
204
205    const color = colorForTid(this.config.pidForColor);
206    ctx.fillStyle = color.base.cssString;
207    ctx.beginPath();
208    ctx.moveTo(lastX, lastY);
209    for (let i = 0; i < data.utilizations.length; i++) {
210      const startTime = Time.fromRaw(data.starts[i]);
211      const utilization = data.utilizations[i];
212      lastX = Math.floor(timescale.timeToPx(startTime));
213      ctx.lineTo(lastX, lastY);
214      lastY = MARGIN_TOP + Math.round(SUMMARY_HEIGHT * (1 - utilization));
215      ctx.lineTo(lastX, lastY);
216    }
217    ctx.lineTo(lastX, bottomY);
218    ctx.closePath();
219    ctx.fill();
220  }
221}
222