xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.AsyncSlices/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 {removeFalsyValues} from '../../base/array_utils';
16import {TrackNode} from '../../public/workspace';
17import {SLICE_TRACK_KIND} from '../../public/track_kinds';
18import {Trace} from '../../public/trace';
19import {PerfettoPlugin} from '../../public/plugin';
20import {getThreadUriPrefix, getTrackName} from '../../public/utils';
21import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
22import {AsyncSliceTrack} from './async_slice_track';
23import {exists} from '../../base/utils';
24import {assertExists, assertTrue} from '../../base/logging';
25import {SliceSelectionAggregator} from './slice_selection_aggregator';
26import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
27
28export default class implements PerfettoPlugin {
29  static readonly id = 'dev.perfetto.AsyncSlices';
30  static readonly dependencies = [ProcessThreadGroupsPlugin];
31
32  async onTraceLoad(ctx: Trace): Promise<void> {
33    const trackIdsToUris = new Map<number, string>();
34
35    await this.addGlobalAsyncTracks(ctx, trackIdsToUris);
36    await this.addProcessAsyncSliceTracks(ctx, trackIdsToUris);
37    await this.addThreadAsyncSliceTracks(ctx, trackIdsToUris);
38
39    ctx.selection.registerSqlSelectionResolver({
40      sqlTableName: 'slice',
41      callback: async (id: number) => {
42        // Locate the track for a given id in the slice table
43        const result = await ctx.engine.query(`
44          select
45            track_id as trackId
46          from
47            slice
48          where slice.id = ${id}
49        `);
50
51        if (result.numRows() === 0) {
52          return undefined;
53        }
54
55        const {trackId} = result.firstRow({
56          trackId: NUM,
57        });
58
59        const trackUri = trackIdsToUris.get(trackId);
60        if (!trackUri) {
61          return undefined;
62        }
63
64        return {
65          trackUri,
66          eventId: id,
67        };
68      },
69    });
70
71    ctx.selection.registerAreaSelectionAggregator(
72      new SliceSelectionAggregator(),
73    );
74  }
75
76  async addGlobalAsyncTracks(
77    ctx: Trace,
78    trackIdsToUris: Map<number, string>,
79  ): Promise<void> {
80    const {engine} = ctx;
81    // TODO(stevegolton): The track exclusion logic is currently a hack. This will be replaced
82    // by a mechanism for more specific plugins to override tracks from more generic plugins.
83    const suspendResumeLatencyTrackName = 'Suspend/Resume Latency';
84    const rawGlobalAsyncTracks = await engine.query(`
85      include perfetto module graphs.search;
86      include perfetto module viz.summary.tracks;
87
88      with global_tracks_grouped as (
89        select
90          t.parent_id,
91          t.name,
92          group_concat(t.id) as trackIds,
93          count() as trackCount,
94          ifnull(min(a.order_id), 0) as order_id
95        from track t
96        join _slice_track_summary using (id)
97        left join _track_event_tracks_ordered a USING (id)
98        where
99          t.type in ('__intrinsic_track', 'gpu_track', '__intrinsic_cpu_track')
100          and (name != '${suspendResumeLatencyTrackName}' or name is null)
101          and classification not in (
102            'linux_rpm',
103            'linux_device_frequency',
104            'irq_counter',
105            'softirq_counter',
106            'android_energy_estimation_breakdown',
107            'android_energy_estimation_breakdown_per_uid'
108          )
109        group by parent_id, name
110        order by parent_id, order_id
111      ),
112      intermediate_groups as (
113        select
114          t.name,
115          t.id,
116          t.parent_id,
117          ifnull(a.order_id, 0) as order_id
118        from graph_reachable_dfs!(
119          (
120            select id as source_node_id, parent_id as dest_node_id
121            from track
122            where parent_id is not null
123          ),
124          (
125            select distinct parent_id as node_id
126            from global_tracks_grouped
127            where parent_id is not null
128          )
129        ) g
130        join track t on g.node_id = t.id
131        left join _track_event_tracks_ordered a USING (id)
132      )
133      select
134        t.name as name,
135        t.parent_id as parentId,
136        t.trackIds as trackIds,
137        t.order_id as orderId,
138        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
139      from global_tracks_grouped t
140      union all
141      select
142        t.name as name,
143        t.parent_id as parentId,
144        cast_string!(t.id) as trackIds,
145        t.order_id as orderId,
146        NULL as maxDepth
147      from intermediate_groups t
148      left join _slice_track_summary s using (id)
149      where s.id is null
150      order by parentId, orderId
151    `);
152    const it = rawGlobalAsyncTracks.iter({
153      name: STR_NULL,
154      parentId: NUM_NULL,
155      trackIds: STR,
156      orderId: NUM,
157      maxDepth: NUM_NULL,
158    });
159
160    // Create a map of track nodes by id
161    const trackMap = new Map<
162      number,
163      {parentId: number | null; trackNode: TrackNode}
164    >();
165
166    for (; it.valid(); it.next()) {
167      const rawName = it.name === null ? undefined : it.name;
168      const title = getTrackName({
169        name: rawName,
170        kind: SLICE_TRACK_KIND,
171      });
172      const rawTrackIds = it.trackIds;
173      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
174      const maxDepth = it.maxDepth;
175
176      if (maxDepth === null) {
177        assertTrue(trackIds.length == 1);
178        const trackNode = new TrackNode({title, sortOrder: -25});
179        trackMap.set(trackIds[0], {parentId: it.parentId, trackNode});
180      } else {
181        const uri = `/async_slices_${rawName}_${it.parentId}`;
182        ctx.tracks.registerTrack({
183          uri,
184          title,
185          tags: {
186            trackIds,
187            kind: SLICE_TRACK_KIND,
188            scope: 'global',
189          },
190          track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
191        });
192        const trackNode = new TrackNode({
193          uri,
194          title,
195          sortOrder: it.orderId,
196        });
197        trackIds.forEach((id) => {
198          trackMap.set(id, {parentId: it.parentId, trackNode});
199          trackIdsToUris.set(id, uri);
200        });
201      }
202    }
203
204    // Attach track nodes to parents / or the workspace if they have no parent
205    trackMap.forEach(({parentId, trackNode}) => {
206      if (exists(parentId)) {
207        const parent = assertExists(trackMap.get(parentId));
208        parent.trackNode.addChildInOrder(trackNode);
209      } else {
210        ctx.workspace.addChildInOrder(trackNode);
211      }
212    });
213  }
214
215  async addProcessAsyncSliceTracks(
216    ctx: Trace,
217    trackIdsToUris: Map<number, string>,
218  ): Promise<void> {
219    const result = await ctx.engine.query(`
220      select
221        upid,
222        t.name as trackName,
223        t.track_ids as trackIds,
224        process.name as processName,
225        process.pid as pid,
226        t.parent_id as parentId,
227        __max_layout_depth(t.track_count, t.track_ids) as maxDepth
228      from _process_track_summary_by_upid_and_parent_id_and_name t
229      join process using (upid)
230      where t.name is null or t.name not glob "* Timeline"
231    `);
232
233    const it = result.iter({
234      upid: NUM,
235      parentId: NUM_NULL,
236      trackName: STR_NULL,
237      trackIds: STR,
238      processName: STR_NULL,
239      pid: NUM_NULL,
240      maxDepth: NUM,
241    });
242
243    const trackMap = new Map<
244      number,
245      {parentId: number | null; upid: number; trackNode: TrackNode}
246    >();
247
248    for (; it.valid(); it.next()) {
249      const upid = it.upid;
250      const trackName = it.trackName;
251      const rawTrackIds = it.trackIds;
252      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
253      const processName = it.processName;
254      const pid = it.pid;
255      const maxDepth = it.maxDepth;
256
257      const kind = SLICE_TRACK_KIND;
258      const title = getTrackName({
259        name: trackName,
260        upid,
261        pid,
262        processName,
263        kind,
264      });
265
266      const uri = `/process_${upid}/async_slices_${rawTrackIds}`;
267      ctx.tracks.registerTrack({
268        uri,
269        title,
270        tags: {
271          trackIds,
272          kind: SLICE_TRACK_KIND,
273          scope: 'process',
274          upid,
275        },
276        track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
277      });
278      const track = new TrackNode({uri, title, sortOrder: 30});
279      trackIds.forEach((id) => {
280        trackMap.set(id, {trackNode: track, parentId: it.parentId, upid});
281        trackIdsToUris.set(id, uri);
282      });
283    }
284
285    // Attach track nodes to parents / or the workspace if they have no parent
286    trackMap.forEach((t) => {
287      const parent = exists(t.parentId) && trackMap.get(t.parentId);
288      if (parent !== false && parent !== undefined) {
289        parent.trackNode.addChildInOrder(t.trackNode);
290      } else {
291        const processGroup = ctx.plugins
292          .getPlugin(ProcessThreadGroupsPlugin)
293          .getGroupForProcess(t.upid);
294        processGroup?.addChildInOrder(t.trackNode);
295      }
296    });
297  }
298
299  async addThreadAsyncSliceTracks(
300    ctx: Trace,
301    trackIdsToUris: Map<number, string>,
302  ): Promise<void> {
303    const result = await ctx.engine.query(`
304      include perfetto module viz.summary.slices;
305      include perfetto module viz.summary.threads;
306      include perfetto module viz.threads;
307
308      select
309        t.utid,
310        t.parent_id as parentId,
311        thread.upid,
312        t.name as trackName,
313        thread.name as threadName,
314        thread.tid as tid,
315        t.track_ids as trackIds,
316        __max_layout_depth(t.track_count, t.track_ids) as maxDepth,
317        k.is_main_thread as isMainThread,
318        k.is_kernel_thread AS isKernelThread
319      from _thread_track_summary_by_utid_and_name t
320      join _threads_with_kernel_flag k using(utid)
321      join thread using (utid)
322    `);
323
324    const it = result.iter({
325      utid: NUM,
326      parentId: NUM_NULL,
327      upid: NUM_NULL,
328      trackName: STR_NULL,
329      trackIds: STR,
330      maxDepth: NUM,
331      isMainThread: NUM_NULL,
332      isKernelThread: NUM,
333      threadName: STR_NULL,
334      tid: NUM_NULL,
335    });
336
337    const trackMap = new Map<
338      number,
339      {parentId: number | null; utid: number; trackNode: TrackNode}
340    >();
341
342    for (; it.valid(); it.next()) {
343      const {
344        utid,
345        parentId,
346        upid,
347        trackName,
348        isMainThread,
349        isKernelThread,
350        maxDepth,
351        threadName,
352        tid,
353      } = it;
354      const rawTrackIds = it.trackIds;
355      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
356      const title = getTrackName({
357        name: trackName,
358        utid,
359        tid,
360        threadName,
361        kind: 'Slices',
362      });
363
364      const uri = `/${getThreadUriPrefix(upid, utid)}_slice_${rawTrackIds}`;
365      ctx.tracks.registerTrack({
366        uri,
367        title,
368        tags: {
369          trackIds,
370          kind: SLICE_TRACK_KIND,
371          scope: 'thread',
372          utid,
373          upid: upid ?? undefined,
374          ...(isKernelThread === 1 && {kernelThread: true}),
375        },
376        chips: removeFalsyValues([
377          isKernelThread === 0 && isMainThread === 1 && 'main thread',
378        ]),
379        track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
380      });
381      const track = new TrackNode({uri, title, sortOrder: 20});
382      trackIds.forEach((id) => {
383        trackMap.set(id, {trackNode: track, parentId, utid});
384        trackIdsToUris.set(id, uri);
385      });
386    }
387
388    // Attach track nodes to parents / or the workspace if they have no parent
389    trackMap.forEach((t) => {
390      const parent = exists(t.parentId) && trackMap.get(t.parentId);
391      if (parent !== false && parent !== undefined) {
392        parent.trackNode.addChildInOrder(t.trackNode);
393      } else {
394        const group = ctx.plugins
395          .getPlugin(ProcessThreadGroupsPlugin)
396          .getGroupForThread(t.utid);
397        group?.addChildInOrder(t.trackNode);
398      }
399    });
400  }
401}
402