blob: eb720c967afbdd9ab0b285616918dd6ef9f4332f [file] [log] [blame]
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Trace} from '../../public/trace';
import {PerfettoPlugin} from '../../public/plugin';
import {TrackNode} from '../../public/workspace';
import {NUM, STR, STR_NULL} from '../../trace_processor/query_result';
function stripPathFromExecutable(path: string) {
if (path[0] === '/') {
return path.split('/').slice(-1)[0];
} else {
return path;
}
}
function getThreadDisplayName(threadName: string | undefined, tid: number) {
if (threadName) {
return `${stripPathFromExecutable(threadName)} ${tid}`;
} else {
return `Thread ${tid}`;
}
}
// This plugin is responsible for organizing all process and thread groups
// including the kernel groups, sorting, and adding summary tracks.
export default class implements PerfettoPlugin {
static readonly id = 'dev.perfetto.ProcessThreadGroups';
private readonly processGroups = new Map<number, TrackNode>();
private readonly threadGroups = new Map<number, TrackNode>();
constructor(private readonly ctx: Trace) {}
getGroupForProcess(upid: number): TrackNode | undefined {
return this.processGroups.get(upid);
}
getGroupForThread(utid: number): TrackNode | undefined {
return this.threadGroups.get(utid);
}
async onTraceLoad(ctx: Trace): Promise<void> {
// Pre-group all kernel "threads" (actually processes) if this is a linux
// system trace. Below, addProcessTrackGroups will skip them due to an
// existing group uuid, and addThreadStateTracks will fill in the
// per-thread tracks. Quirk: since all threads will appear to be
// TrackKindPriority.MAIN_THREAD, any process-level tracks will end up
// pushed to the bottom of the group in the UI.
await this.addKernelThreadGrouping();
// Create the per-process track groups. Note that this won't necessarily
// create a track per process. If a process has been completely idle and has
// no sched events, no track group will be emitted.
// Will populate this.addTrackGroupActions
await this.addProcessGroups();
await this.addThreadGroups();
ctx.addEventListener('traceready', () => {
// If, by the time the trace has finished loading, some of the process or
// thread group tracks nodes have no children, just remove them.
const removeIfEmpty = (g: TrackNode) => {
if (!g.hasChildren) {
g.remove();
}
};
this.processGroups.forEach(removeIfEmpty);
this.threadGroups.forEach(removeIfEmpty);
});
}
private async addKernelThreadGrouping(): Promise<void> {
// Identify kernel threads if this is a linux system trace, and sufficient
// process information is available. Kernel threads are identified by being
// children of kthreadd (always pid 2).
// The query will return the kthreadd process row first, which must exist
// for any other kthreads to be returned by the query.
// TODO(rsavitski): figure out how to handle the idle process (swapper),
// which has pid 0 but appears as a distinct process (with its own comm) on
// each cpu. It'd make sense to exclude its thread state track, but still
// put process-scoped tracks in this group.
const result = await this.ctx.engine.query(`
select
t.utid, p.upid, (case p.pid when 2 then 1 else 0 end) isKthreadd
from
thread t
join process p using (upid)
left join process parent on (p.parent_upid = parent.upid)
join
(select true from metadata m
where (m.name = 'system_name' and m.str_value = 'Linux')
union
select 1 from (select true from sched limit 1))
where
p.pid = 2 or parent.pid = 2
order by isKthreadd desc
`);
const it = result.iter({
utid: NUM,
upid: NUM,
});
// Not applying kernel thread grouping.
if (!it.valid()) {
return;
}
// Create the track group. Use kthreadd's PROCESS_SUMMARY_TRACK for the
// main track. It doesn't summarise the kernel threads within the group,
// but creating a dedicated track type is out of scope at the time of
// writing.
const kernelThreadsGroup = new TrackNode({
title: 'Kernel threads',
uri: '/kernel',
sortOrder: 50,
isSummary: true,
});
this.ctx.workspace.addChildInOrder(kernelThreadsGroup);
// Set the group for all kernel threads (including kthreadd itself).
for (; it.valid(); it.next()) {
const {utid} = it;
const threadGroup = new TrackNode({
uri: `thread${utid}`,
title: `Thread ${utid}`,
isSummary: true,
headless: true,
});
kernelThreadsGroup.addChildInOrder(threadGroup);
this.threadGroups.set(utid, threadGroup);
}
}
// Adds top level groups for processes and thread that don't belong to a
// process.
private async addProcessGroups(): Promise<void> {
const result = await this.ctx.engine.query(`
with processGroups as (
select
upid,
process.pid as pid,
process.name as processName,
sum_running_dur as sumRunningDur,
thread_slice_count + process_slice_count as sliceCount,
perf_sample_count as perfSampleCount,
allocation_count as heapProfileAllocationCount,
graph_object_count as heapGraphObjectCount,
(
select group_concat(string_value)
from args
where
process.arg_set_id is not null and
arg_set_id = process.arg_set_id and
flat_key = 'chrome.process_label'
) chromeProcessLabels,
case process.name
when 'Browser' then 3
when 'Gpu' then 2
when 'Renderer' then 1
else 0
end as chromeProcessRank
from _process_available_info_summary
join process using(upid)
),
threadGroups as (
select
utid,
tid,
thread.name as threadName,
sum_running_dur as sumRunningDur,
slice_count as sliceCount,
perf_sample_count as perfSampleCount
from _thread_available_info_summary
join thread using (utid)
where upid is null
)
select *
from (
select
'process' as kind,
upid as uid,
pid as id,
processName as name
from processGroups
order by
chromeProcessRank desc,
heapProfileAllocationCount desc,
heapGraphObjectCount desc,
perfSampleCount desc,
sumRunningDur desc,
sliceCount desc,
processName asc,
upid asc
)
union all
select *
from (
select
'thread' as kind,
utid as uid,
tid as id,
threadName as name
from threadGroups
order by
perfSampleCount desc,
sumRunningDur desc,
sliceCount desc,
threadName asc,
utid asc
)
`);
const it = result.iter({
kind: STR,
uid: NUM,
id: NUM,
name: STR_NULL,
});
for (; it.valid(); it.next()) {
const {kind, uid, id, name} = it;
if (kind === 'process') {
// Ignore kernel process groups
if (this.processGroups.has(uid)) {
continue;
}
function getProcessDisplayName(
processName: string | undefined,
pid: number,
) {
if (processName) {
return `${stripPathFromExecutable(processName)} ${pid}`;
} else {
return `Process ${pid}`;
}
}
const displayName = getProcessDisplayName(name ?? undefined, id);
const group = new TrackNode({
uri: `/process_${uid}`,
title: displayName,
isSummary: true,
sortOrder: 50,
});
// Re-insert the child node to sort it
this.ctx.workspace.addChildInOrder(group);
this.processGroups.set(uid, group);
} else {
// Ignore kernel process groups
if (this.threadGroups.has(uid)) {
continue;
}
const displayName = getThreadDisplayName(name ?? undefined, id);
const group = new TrackNode({
uri: `/thread_${uid}`,
title: displayName,
isSummary: true,
sortOrder: 50,
});
// Re-insert the child node to sort it
this.ctx.workspace.addChildInOrder(group);
this.threadGroups.set(uid, group);
}
}
}
// Create all the nested & headless thread groups that live inside existing
// process groups.
private async addThreadGroups(): Promise<void> {
const result = await this.ctx.engine.query(`
with threadGroups as (
select
utid,
upid,
tid,
thread.name as threadName,
CASE
WHEN thread.is_main_thread = 1 THEN 10
WHEN thread.name = 'CrBrowserMain' THEN 10
WHEN thread.name = 'CrRendererMain' THEN 10
WHEN thread.name = 'CrGpuMain' THEN 10
WHEN thread.name glob '*RenderThread*' THEN 9
WHEN thread.name glob '*GPU completion*' THEN 8
WHEN thread.name = 'Chrome_ChildIOThread' THEN 7
WHEN thread.name = 'Chrome_IOThread' THEN 7
WHEN thread.name = 'Compositor' THEN 6
WHEN thread.name = 'VizCompositorThread' THEN 6
ELSE 5
END as priority
from _thread_available_info_summary
join thread using (utid)
where upid is not null
)
select *
from (
select
utid,
upid,
tid,
threadName
from threadGroups
order by
priority desc,
tid asc
)
`);
const it = result.iter({
utid: NUM,
tid: NUM,
upid: NUM,
threadName: STR_NULL,
});
for (; it.valid(); it.next()) {
const {utid, tid, upid, threadName} = it;
// Ignore kernel thread groups
if (this.threadGroups.has(utid)) {
continue;
}
const group = new TrackNode({
uri: `/thread_${utid}`,
title: getThreadDisplayName(threadName ?? undefined, tid),
isSummary: true,
headless: true,
});
this.threadGroups.set(utid, group);
this.processGroups.get(upid)?.addChildInOrder(group);
}
}
}