xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.Counter/index.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2021 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 {
16  NUM_NULL,
17  STR_NULL,
18  LONG_NULL,
19  NUM,
20  STR,
21} from '../../trace_processor/query_result';
22import {Trace} from '../../public/trace';
23import {COUNTER_TRACK_KIND} from '../../public/track_kinds';
24import {PerfettoPlugin} from '../../public/plugin';
25import {getThreadUriPrefix, getTrackName} from '../../public/utils';
26import {CounterOptions} from '../../components/tracks/base_counter_track';
27import {TraceProcessorCounterTrack} from './trace_processor_counter_track';
28import {exists} from '../../base/utils';
29import {TrackNode} from '../../public/workspace';
30import {CounterSelectionAggregator} from './counter_selection_aggregator';
31import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
32
33const NETWORK_TRACK_REGEX = new RegExp('^.* (Received|Transmitted)( KB)?$');
34const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency:');
35
36type Modes = CounterOptions['yMode'];
37
38// Sets the default 'mode' for counter tracks. If the regex matches
39// then the paired mode is used. Entries are in priority order so the
40// first match wins.
41const COUNTER_REGEX: [RegExp, Modes][] = [
42  // Power counters make more sense in rate mode since you're typically
43  // interested in the slope of the graph rather than the absolute
44  // value.
45  [new RegExp('^power..*$'), 'rate'],
46  // Same for cumulative PSI stall time counters, e.g., psi.cpu.some.
47  [new RegExp('^psi..*$'), 'rate'],
48  // Same for network counters.
49  [NETWORK_TRACK_REGEX, 'rate'],
50  // Entity residency
51  [ENTITY_RESIDENCY_REGEX, 'rate'],
52];
53
54function getCounterMode(name: string): Modes | undefined {
55  for (const [re, mode] of COUNTER_REGEX) {
56    if (name.match(re)) {
57      return mode;
58    }
59  }
60  return undefined;
61}
62
63function getDefaultCounterOptions(name: string): Partial<CounterOptions> {
64  const options: Partial<CounterOptions> = {};
65  options.yMode = getCounterMode(name);
66
67  if (name.endsWith('_pct')) {
68    options.yOverrideMinimum = 0;
69    options.yOverrideMaximum = 100;
70    options.unit = '%';
71  }
72
73  if (name.startsWith('power.')) {
74    options.yRangeSharingKey = 'power';
75  }
76
77  // TODO(stevegolton): We need to rethink how this works for virtual memory.
78  // The problem is we can easily have > 10GB virtual memory which dwarfs
79  // physical memory making other memory tracks difficult to read.
80
81  // if (name.startsWith('mem.')) {
82  //   options.yRangeSharingKey = 'mem';
83  // }
84
85  // All 'Entity residency: foo bar1234' tracks should share a y-axis
86  // with 'Entity residency: foo baz5678' etc tracks:
87  {
88    const r = new RegExp('Entity residency: ([^ ]+) ');
89    const m = r.exec(name);
90    if (m) {
91      options.yRangeSharingKey = `entity-residency-${m[1]}`;
92    }
93  }
94
95  {
96    const r = new RegExp('GPU .* Frequency');
97    const m = r.exec(name);
98    if (m) {
99      options.yRangeSharingKey = 'gpu-frequency';
100    }
101  }
102
103  return options;
104}
105
106export default class implements PerfettoPlugin {
107  static readonly id = 'dev.perfetto.Counter';
108  static readonly dependencies = [ProcessThreadGroupsPlugin];
109
110  async onTraceLoad(ctx: Trace): Promise<void> {
111    await this.addCounterTracks(ctx);
112    await this.addGpuFrequencyTracks(ctx);
113    await this.addCpuFreqLimitCounterTracks(ctx);
114    await this.addCpuTimeCounterTracks(ctx);
115    await this.addCpuPerfCounterTracks(ctx);
116    await this.addThreadCounterTracks(ctx);
117    await this.addProcessCounterTracks(ctx);
118
119    ctx.selection.registerAreaSelectionAggregator(
120      new CounterSelectionAggregator(),
121    );
122  }
123
124  private async addCounterTracks(ctx: Trace) {
125    const result = await ctx.engine.query(`
126      select name, id, unit
127      from (
128        select name, id, unit
129        from counter_track
130        join _counter_track_summary using (id)
131        where is_legacy_global
132        union
133        select name, id, unit
134        from gpu_counter_track
135        join _counter_track_summary using (id)
136        where name != 'gpufreq'
137      )
138      order by name
139    `);
140
141    // Add global or GPU counter tracks that are not bound to any pid/tid.
142    const it = result.iter({
143      name: STR,
144      unit: STR_NULL,
145      id: NUM,
146    });
147
148    for (; it.valid(); it.next()) {
149      const trackId = it.id;
150      const title = it.name;
151      const unit = it.unit ?? undefined;
152
153      const uri = `/counter_${trackId}`;
154      ctx.tracks.registerTrack({
155        uri,
156        title,
157        tags: {
158          kind: COUNTER_TRACK_KIND,
159          trackIds: [trackId],
160        },
161        track: new TraceProcessorCounterTrack(
162          ctx,
163          uri,
164          {
165            ...getDefaultCounterOptions(title),
166            unit,
167          },
168          trackId,
169          title,
170        ),
171      });
172      const track = new TrackNode({uri, title});
173      ctx.workspace.addChildInOrder(track);
174    }
175  }
176
177  async addCpuFreqLimitCounterTracks(ctx: Trace): Promise<void> {
178    const cpuFreqLimitCounterTracksSql = `
179      select name, id
180      from cpu_counter_track
181      join _counter_track_summary using (id)
182      where name glob "Cpu * Freq Limit"
183      order by name asc
184    `;
185
186    this.addCpuCounterTracks(ctx, cpuFreqLimitCounterTracksSql, 'cpuFreqLimit');
187  }
188
189  async addCpuTimeCounterTracks(ctx: Trace): Promise<void> {
190    const cpuTimeCounterTracksSql = `
191      select name, id
192      from cpu_counter_track
193      join _counter_track_summary using (id)
194      where name glob "cpu.times.*"
195      order by name asc
196    `;
197    this.addCpuCounterTracks(ctx, cpuTimeCounterTracksSql, 'cpuTime');
198  }
199
200  async addCpuPerfCounterTracks(ctx: Trace): Promise<void> {
201    // Perf counter tracks are bound to CPUs, follow the scheduling and
202    // frequency track naming convention ("Cpu N ...").
203    // Note: we might not have a track for a given cpu if no data was seen from
204    // it. This might look surprising in the UI, but placeholder tracks are
205    // wasteful as there's no way of collapsing global counter tracks at the
206    // moment.
207    const addCpuPerfCounterTracksSql = `
208      select printf("Cpu %u %s", cpu, name) as name, id
209      from perf_counter_track as pct
210      join _counter_track_summary using (id)
211      order by perf_session_id asc, pct.name asc, cpu asc
212    `;
213    this.addCpuCounterTracks(ctx, addCpuPerfCounterTracksSql, 'cpuPerf');
214  }
215
216  async addCpuCounterTracks(
217    ctx: Trace,
218    sql: string,
219    scope: string,
220  ): Promise<void> {
221    const result = await ctx.engine.query(sql);
222
223    const it = result.iter({
224      name: STR,
225      id: NUM,
226    });
227
228    for (; it.valid(); it.next()) {
229      const name = it.name;
230      const trackId = it.id;
231      const uri = `counter.cpu.${trackId}`;
232      ctx.tracks.registerTrack({
233        uri,
234        title: name,
235        tags: {
236          kind: COUNTER_TRACK_KIND,
237          trackIds: [trackId],
238          scope,
239        },
240        track: new TraceProcessorCounterTrack(
241          ctx,
242          uri,
243          getDefaultCounterOptions(name),
244          trackId,
245          name,
246        ),
247      });
248      const trackNode = new TrackNode({uri, title: name, sortOrder: -20});
249      ctx.workspace.addChildInOrder(trackNode);
250    }
251  }
252
253  async addThreadCounterTracks(ctx: Trace): Promise<void> {
254    const result = await ctx.engine.query(`
255      select
256        thread_counter_track.name as trackName,
257        utid,
258        upid,
259        tid,
260        thread.name as threadName,
261        thread_counter_track.id as trackId,
262        thread.start_ts as startTs,
263        thread.end_ts as endTs
264      from thread_counter_track
265      join _counter_track_summary using (id)
266      join thread using(utid)
267      where thread_counter_track.name != 'thread_time'
268    `);
269
270    const it = result.iter({
271      startTs: LONG_NULL,
272      trackId: NUM,
273      endTs: LONG_NULL,
274      trackName: STR_NULL,
275      utid: NUM,
276      upid: NUM_NULL,
277      tid: NUM_NULL,
278      threadName: STR_NULL,
279    });
280    for (; it.valid(); it.next()) {
281      const utid = it.utid;
282      const upid = it.upid;
283      const tid = it.tid;
284      const trackId = it.trackId;
285      const trackName = it.trackName;
286      const threadName = it.threadName;
287      const kind = COUNTER_TRACK_KIND;
288      const name = getTrackName({
289        name: trackName,
290        utid,
291        tid,
292        kind,
293        threadName,
294        threadTrack: true,
295      });
296      const uri = `${getThreadUriPrefix(upid, utid)}_counter_${trackId}`;
297      ctx.tracks.registerTrack({
298        uri,
299        title: name,
300        tags: {
301          kind,
302          trackIds: [trackId],
303          utid,
304          upid: upid ?? undefined,
305          scope: 'thread',
306        },
307        track: new TraceProcessorCounterTrack(
308          ctx,
309          uri,
310          getDefaultCounterOptions(name),
311          trackId,
312          name,
313        ),
314      });
315      const group = ctx.plugins
316        .getPlugin(ProcessThreadGroupsPlugin)
317        .getGroupForThread(utid);
318      const track = new TrackNode({uri, title: name, sortOrder: 30});
319      group?.addChildInOrder(track);
320    }
321  }
322
323  async addProcessCounterTracks(ctx: Trace): Promise<void> {
324    const result = await ctx.engine.query(`
325      select
326        process_counter_track.id as trackId,
327        process_counter_track.name as trackName,
328        upid,
329        process.pid,
330        process.name as processName
331      from process_counter_track
332      join _counter_track_summary using (id)
333      join process using(upid)
334      order by trackName;
335    `);
336    const it = result.iter({
337      trackId: NUM,
338      trackName: STR_NULL,
339      upid: NUM,
340      pid: NUM_NULL,
341      processName: STR_NULL,
342    });
343    for (let i = 0; it.valid(); ++i, it.next()) {
344      const trackId = it.trackId;
345      const pid = it.pid;
346      const trackName = it.trackName;
347      const upid = it.upid;
348      const processName = it.processName;
349      const kind = COUNTER_TRACK_KIND;
350      const name = getTrackName({
351        name: trackName,
352        upid,
353        pid,
354        kind,
355        processName,
356        ...(exists(trackName) && {trackName}),
357      });
358      const uri = `/process_${upid}/counter_${trackId}`;
359      ctx.tracks.registerTrack({
360        uri,
361        title: name,
362        tags: {
363          kind,
364          trackIds: [trackId],
365          upid,
366          scope: 'process',
367        },
368        track: new TraceProcessorCounterTrack(
369          ctx,
370          uri,
371          getDefaultCounterOptions(name),
372          trackId,
373          name,
374        ),
375      });
376      const group = ctx.plugins
377        .getPlugin(ProcessThreadGroupsPlugin)
378        .getGroupForProcess(upid);
379      const track = new TrackNode({uri, title: name, sortOrder: 20});
380      group?.addChildInOrder(track);
381    }
382  }
383
384  private async addGpuFrequencyTracks(ctx: Trace) {
385    const engine = ctx.engine;
386
387    const result = await engine.query(`
388      select id, gpu_id as gpuId
389      from gpu_counter_track
390      join _counter_track_summary using (id)
391      where name = 'gpufreq'
392    `);
393    const it = result.iter({id: NUM, gpuId: NUM});
394    for (; it.valid(); it.next()) {
395      const uri = `/gpu_frequency_${it.gpuId}`;
396      const name = `Gpu ${it.gpuId} Frequency`;
397      ctx.tracks.registerTrack({
398        uri,
399        title: name,
400        tags: {
401          kind: COUNTER_TRACK_KIND,
402          trackIds: [it.id],
403          scope: 'gpuFreq',
404        },
405        track: new TraceProcessorCounterTrack(
406          ctx,
407          uri,
408          getDefaultCounterOptions(name),
409          it.id,
410          name,
411        ),
412      });
413      const track = new TrackNode({uri, title: name, sortOrder: -20});
414      ctx.workspace.addChildInOrder(track);
415    }
416  }
417}
418