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