• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2025 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 {TrackData} from '../../components/tracks/track_data';
16import {INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND} from '../../public/track_kinds';
17import {Trace} from '../../public/trace';
18import {PerfettoPlugin} from '../../public/plugin';
19import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
20import {assertExists} from '../../base/logging';
21import {
22  createProcessInstrumentsSamplesProfileTrack,
23  createThreadInstrumentsSamplesProfileTrack,
24} from './instruments_samples_profile_track';
25import {getThreadUriPrefix} from '../../public/utils';
26import {TrackNode} from '../../public/workspace';
27import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
28import {AreaSelection, areaSelectionsEqual} from '../../public/selection';
29import {
30  metricsFromTableOrSubquery,
31  QueryFlamegraph,
32} from '../../components/query_flamegraph';
33import {Flamegraph} from '../../widgets/flamegraph';
34
35export interface Data extends TrackData {
36  tsStarts: BigInt64Array;
37}
38
39function makeUriForProc(upid: number) {
40  return `/process_${upid}/instruments_samples_profile`;
41}
42
43export default class implements PerfettoPlugin {
44  static readonly id = 'dev.perfetto.InstrumentsSamplesProfile';
45  static readonly dependencies = [ProcessThreadGroupsPlugin];
46
47  async onTraceLoad(ctx: Trace): Promise<void> {
48    const pResult = await ctx.engine.query(`
49      select distinct upid
50      from instruments_sample
51      join thread using (utid)
52      where callsite_id is not null and upid is not null
53    `);
54    for (const it = pResult.iter({upid: NUM}); it.valid(); it.next()) {
55      const upid = it.upid;
56      const uri = makeUriForProc(upid);
57      const title = `Process Callstacks`;
58      ctx.tracks.registerTrack({
59        uri,
60        title,
61        tags: {
62          kind: INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND,
63          upid,
64        },
65        track: createProcessInstrumentsSamplesProfileTrack(ctx, uri, upid),
66      });
67      const group = ctx.plugins
68        .getPlugin(ProcessThreadGroupsPlugin)
69        .getGroupForProcess(upid);
70      const track = new TrackNode({uri, title, sortOrder: -40});
71      group?.addChildInOrder(track);
72    }
73    const tResult = await ctx.engine.query(`
74      select distinct
75        utid,
76        tid,
77        thread.name as threadName,
78        upid
79      from instruments_sample
80      join thread using (utid)
81      where callsite_id is not null
82    `);
83    for (
84      const it = tResult.iter({
85        utid: NUM,
86        tid: NUM,
87        threadName: STR_NULL,
88        upid: NUM_NULL,
89      });
90      it.valid();
91      it.next()
92    ) {
93      const {threadName, utid, tid, upid} = it;
94      const title =
95        threadName === null
96          ? `Thread Callstacks ${tid}`
97          : `${threadName} Callstacks ${tid}`;
98      const uri = `${getThreadUriPrefix(upid, utid)}_instruments_samples_profile`;
99      ctx.tracks.registerTrack({
100        uri,
101        title,
102        tags: {
103          kind: INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND,
104          utid,
105          upid: upid ?? undefined,
106        },
107        track: createThreadInstrumentsSamplesProfileTrack(ctx, uri, utid),
108      });
109      const group = ctx.plugins
110        .getPlugin(ProcessThreadGroupsPlugin)
111        .getGroupForThread(utid);
112      const track = new TrackNode({uri, title, sortOrder: -50});
113      group?.addChildInOrder(track);
114    }
115
116    ctx.onTraceReady.addListener(async () => {
117      await selectInstrumentsSample(ctx);
118    });
119
120    ctx.selection.registerAreaSelectionTab(createAreaSelectionTab(ctx));
121  }
122}
123
124async function selectInstrumentsSample(ctx: Trace) {
125  const profile = await assertExists(ctx.engine).query(`
126    select upid
127    from instruments_sample
128    join thread using (utid)
129    where callsite_id is not null
130    order by ts desc
131    limit 1
132  `);
133  if (profile.numRows() !== 1) return;
134  const row = profile.firstRow({upid: NUM});
135  const upid = row.upid;
136
137  // Create an area selection over the first process with a instruments samples track
138  ctx.selection.selectArea({
139    start: ctx.traceInfo.start,
140    end: ctx.traceInfo.end,
141    trackUris: [makeUriForProc(upid)],
142  });
143}
144
145function createAreaSelectionTab(trace: Trace) {
146  let previousSelection: undefined | AreaSelection;
147  let flamegraph: undefined | QueryFlamegraph;
148
149  return {
150    id: 'instruments_sample_flamegraph',
151    name: 'Instruments Sample Flamegraph',
152    render(selection: AreaSelection) {
153      const changed =
154        previousSelection === undefined ||
155        !areaSelectionsEqual(previousSelection, selection);
156
157      if (changed) {
158        flamegraph = computeInstrumentsSampleFlamegraph(trace, selection);
159        previousSelection = selection;
160      }
161
162      if (flamegraph === undefined) {
163        return undefined;
164      }
165
166      return {isLoading: false, content: flamegraph.render()};
167    },
168  };
169}
170
171function computeInstrumentsSampleFlamegraph(
172  trace: Trace,
173  currentSelection: AreaSelection,
174) {
175  const upids = getUpidsFromInstrumentsSampleAreaSelection(currentSelection);
176  const utids = getUtidsFromInstrumentsSampleAreaSelection(currentSelection);
177  if (utids.length === 0 && upids.length === 0) {
178    return undefined;
179  }
180  const metrics = metricsFromTableOrSubquery(
181    `
182      (
183        select id, parent_id as parentId, name, self_count
184        from _callstacks_for_callsites!((
185          select p.callsite_id
186          from instruments_sample p
187          join thread t using (utid)
188          where p.ts >= ${currentSelection.start}
189            and p.ts <= ${currentSelection.end}
190            and (
191              p.utid in (${utids.join(',')})
192              or t.upid in (${upids.join(',')})
193            )
194        ))
195      )
196    `,
197    [
198      {
199        name: 'Instruments Samples',
200        unit: '',
201        columnName: 'self_count',
202      },
203    ],
204    'include perfetto module appleos.instruments.samples',
205  );
206  return new QueryFlamegraph(trace, metrics, {
207    state: Flamegraph.createDefaultState(metrics),
208  });
209}
210
211function getUpidsFromInstrumentsSampleAreaSelection(
212  currentSelection: AreaSelection,
213) {
214  const upids = [];
215  for (const trackInfo of currentSelection.tracks) {
216    if (
217      trackInfo?.tags?.kind === INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND &&
218      trackInfo.tags?.utid === undefined
219    ) {
220      upids.push(assertExists(trackInfo.tags?.upid));
221    }
222  }
223  return upids;
224}
225
226function getUtidsFromInstrumentsSampleAreaSelection(
227  currentSelection: AreaSelection,
228) {
229  const utids = [];
230  for (const trackInfo of currentSelection.tracks) {
231    if (
232      trackInfo?.tags?.kind === INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND &&
233      trackInfo.tags?.utid !== undefined
234    ) {
235      utids.push(trackInfo.tags?.utid);
236    }
237  }
238  return utids;
239}
240