xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.ProcessThreadGroups/index.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2024 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 {Trace} from '../../public/trace';
16import {PerfettoPlugin} from '../../public/plugin';
17import {TrackNode} from '../../public/workspace';
18import {NUM, STR, STR_NULL} from '../../trace_processor/query_result';
19
20function stripPathFromExecutable(path: string) {
21  if (path[0] === '/') {
22    return path.split('/').slice(-1)[0];
23  } else {
24    return path;
25  }
26}
27
28function getThreadDisplayName(threadName: string | undefined, tid: number) {
29  if (threadName) {
30    return `${stripPathFromExecutable(threadName)} ${tid}`;
31  } else {
32    return `Thread ${tid}`;
33  }
34}
35
36// This plugin is responsible for organizing all process and thread groups
37// including the kernel groups, sorting, and adding summary tracks.
38export default class implements PerfettoPlugin {
39  static readonly id = 'dev.perfetto.ProcessThreadGroups';
40
41  private readonly processGroups = new Map<number, TrackNode>();
42  private readonly threadGroups = new Map<number, TrackNode>();
43
44  constructor(private readonly ctx: Trace) {}
45
46  getGroupForProcess(upid: number): TrackNode | undefined {
47    return this.processGroups.get(upid);
48  }
49
50  getGroupForThread(utid: number): TrackNode | undefined {
51    return this.threadGroups.get(utid);
52  }
53
54  async onTraceLoad(ctx: Trace): Promise<void> {
55    // Pre-group all kernel "threads" (actually processes) if this is a linux
56    // system trace. Below, addProcessTrackGroups will skip them due to an
57    // existing group uuid, and addThreadStateTracks will fill in the
58    // per-thread tracks. Quirk: since all threads will appear to be
59    // TrackKindPriority.MAIN_THREAD, any process-level tracks will end up
60    // pushed to the bottom of the group in the UI.
61    await this.addKernelThreadGrouping();
62
63    // Create the per-process track groups. Note that this won't necessarily
64    // create a track per process. If a process has been completely idle and has
65    // no sched events, no track group will be emitted.
66    // Will populate this.addTrackGroupActions
67    await this.addProcessGroups();
68    await this.addThreadGroups();
69
70    ctx.onTraceReady.addListener(() => {
71      // If, by the time the trace has finished loading, some of the process or
72      // thread group tracks nodes have no children, just remove them.
73      const removeIfEmpty = (g: TrackNode) => {
74        if (!g.hasChildren) {
75          g.remove();
76        }
77      };
78      this.processGroups.forEach(removeIfEmpty);
79      this.threadGroups.forEach(removeIfEmpty);
80    });
81  }
82
83  private async addKernelThreadGrouping(): Promise<void> {
84    // Identify kernel threads if this is a linux system trace, and sufficient
85    // process information is available. Kernel threads are identified by being
86    // children of kthreadd (always pid 2).
87    // The query will return the kthreadd process row first, which must exist
88    // for any other kthreads to be returned by the query.
89    // TODO(rsavitski): figure out how to handle the idle process (swapper),
90    // which has pid 0 but appears as a distinct process (with its own comm) on
91    // each cpu. It'd make sense to exclude its thread state track, but still
92    // put process-scoped tracks in this group.
93    const result = await this.ctx.engine.query(`
94      select
95        t.utid, p.upid, (case p.pid when 2 then 1 else 0 end) isKthreadd
96      from
97        thread t
98        join process p using (upid)
99        left join process parent on (p.parent_upid = parent.upid)
100        join
101          (select true from metadata m
102             where (m.name = 'system_name' and m.str_value = 'Linux')
103           union
104           select 1 from (select true from sched limit 1))
105      where
106        p.pid = 2 or parent.pid = 2
107      order by isKthreadd desc
108    `);
109
110    const it = result.iter({
111      utid: NUM,
112      upid: NUM,
113    });
114
115    // Not applying kernel thread grouping.
116    if (!it.valid()) {
117      return;
118    }
119
120    // Create the track group. Use kthreadd's PROCESS_SUMMARY_TRACK for the
121    // main track. It doesn't summarise the kernel threads within the group,
122    // but creating a dedicated track type is out of scope at the time of
123    // writing.
124    const kernelThreadsGroup = new TrackNode({
125      title: 'Kernel threads',
126      uri: '/kernel',
127      sortOrder: 50,
128      isSummary: true,
129    });
130    this.ctx.workspace.addChildInOrder(kernelThreadsGroup);
131
132    // Set the group for all kernel threads (including kthreadd itself).
133    for (; it.valid(); it.next()) {
134      const {utid} = it;
135
136      const threadGroup = new TrackNode({
137        uri: `thread${utid}`,
138        title: `Thread ${utid}`,
139        isSummary: true,
140        headless: true,
141      });
142      kernelThreadsGroup.addChildInOrder(threadGroup);
143      this.threadGroups.set(utid, threadGroup);
144    }
145  }
146
147  // Adds top level groups for processes and thread that don't belong to a
148  // process.
149  private async addProcessGroups(): Promise<void> {
150    const result = await this.ctx.engine.query(`
151      with processGroups as (
152        select
153          upid,
154          process.pid as pid,
155          process.name as processName,
156          sum_running_dur as sumRunningDur,
157          thread_slice_count + process_slice_count as sliceCount,
158          perf_sample_count as perfSampleCount,
159          allocation_count as heapProfileAllocationCount,
160          graph_object_count as heapGraphObjectCount,
161          (
162            select group_concat(string_value)
163            from args
164            where
165              process.arg_set_id is not null and
166              arg_set_id = process.arg_set_id and
167              flat_key = 'chrome.process_label'
168          ) chromeProcessLabels,
169          case process.name
170            when 'Browser' then 3
171            when 'Gpu' then 2
172            when 'Renderer' then 1
173            else 0
174          end as chromeProcessRank
175        from _process_available_info_summary
176        join process using(upid)
177      ),
178      threadGroups as (
179        select
180          utid,
181          tid,
182          thread.name as threadName,
183          sum_running_dur as sumRunningDur,
184          slice_count as sliceCount,
185          perf_sample_count as perfSampleCount
186        from _thread_available_info_summary
187        join thread using (utid)
188        where upid is null
189      )
190      select *
191      from (
192        select
193          'process' as kind,
194          upid as uid,
195          pid as id,
196          processName as name
197        from processGroups
198        order by
199          chromeProcessRank desc,
200          heapProfileAllocationCount desc,
201          heapGraphObjectCount desc,
202          perfSampleCount desc,
203          sumRunningDur desc,
204          sliceCount desc,
205          processName asc,
206          upid asc
207      )
208      union all
209      select *
210      from (
211        select
212          'thread' as kind,
213          utid as uid,
214          tid as id,
215          threadName as name
216        from threadGroups
217        order by
218          perfSampleCount desc,
219          sumRunningDur desc,
220          sliceCount desc,
221          threadName asc,
222          utid asc
223      )
224  `);
225
226    const it = result.iter({
227      kind: STR,
228      uid: NUM,
229      id: NUM,
230      name: STR_NULL,
231    });
232    for (; it.valid(); it.next()) {
233      const {kind, uid, id, name} = it;
234
235      if (kind === 'process') {
236        // Ignore kernel process groups
237        if (this.processGroups.has(uid)) {
238          continue;
239        }
240
241        function getProcessDisplayName(
242          processName: string | undefined,
243          pid: number,
244        ) {
245          if (processName) {
246            return `${stripPathFromExecutable(processName)} ${pid}`;
247          } else {
248            return `Process ${pid}`;
249          }
250        }
251
252        const displayName = getProcessDisplayName(name ?? undefined, id);
253        const group = new TrackNode({
254          uri: `/process_${uid}`,
255          title: displayName,
256          isSummary: true,
257          sortOrder: 50,
258        });
259
260        // Re-insert the child node to sort it
261        this.ctx.workspace.addChildInOrder(group);
262        this.processGroups.set(uid, group);
263      } else {
264        // Ignore kernel process groups
265        if (this.threadGroups.has(uid)) {
266          continue;
267        }
268
269        const displayName = getThreadDisplayName(name ?? undefined, id);
270        const group = new TrackNode({
271          uri: `/thread_${uid}`,
272          title: displayName,
273          isSummary: true,
274          sortOrder: 50,
275        });
276
277        // Re-insert the child node to sort it
278        this.ctx.workspace.addChildInOrder(group);
279        this.threadGroups.set(uid, group);
280      }
281    }
282  }
283
284  // Create all the nested & headless thread groups that live inside existing
285  // process groups.
286  private async addThreadGroups(): Promise<void> {
287    const result = await this.ctx.engine.query(`
288      with threadGroups as (
289        select
290          utid,
291          upid,
292          tid,
293          thread.name as threadName,
294          CASE
295            WHEN thread.is_main_thread = 1 THEN 10
296            WHEN thread.name = 'CrBrowserMain' THEN 10
297            WHEN thread.name = 'CrRendererMain' THEN 10
298            WHEN thread.name = 'CrGpuMain' THEN 10
299            WHEN thread.name glob '*RenderThread*' THEN 9
300            WHEN thread.name glob '*GPU completion*' THEN 8
301            WHEN thread.name = 'Chrome_ChildIOThread' THEN 7
302            WHEN thread.name = 'Chrome_IOThread' THEN 7
303            WHEN thread.name = 'Compositor' THEN 6
304            WHEN thread.name = 'VizCompositorThread' THEN 6
305            ELSE 5
306          END as priority
307        from _thread_available_info_summary
308        join thread using (utid)
309        where upid is not null
310      )
311      select *
312      from (
313        select
314          utid,
315          upid,
316          tid,
317          threadName
318        from threadGroups
319        order by
320          priority desc,
321          tid asc
322      )
323  `);
324
325    const it = result.iter({
326      utid: NUM,
327      tid: NUM,
328      upid: NUM,
329      threadName: STR_NULL,
330    });
331    for (; it.valid(); it.next()) {
332      const {utid, tid, upid, threadName} = it;
333
334      // Ignore kernel thread groups
335      if (this.threadGroups.has(utid)) {
336        continue;
337      }
338
339      const group = new TrackNode({
340        uri: `/thread_${utid}`,
341        title: getThreadDisplayName(threadName ?? undefined, tid),
342        isSummary: true,
343        headless: true,
344      });
345      this.threadGroups.set(utid, group);
346      this.processGroups.get(upid)?.addChildInOrder(group);
347    }
348  }
349}
350