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