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