blob: b6dd3881e911750aa5b4fb721e8ef3b4c63a33e0 [file] [log] [blame]
// Copyright (C) 2020 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 {v4 as uuidv4} from 'uuid';
import {assertExists} from '../base/logging';
import {sqliteString} from '../base/string_utils';
import {Actions, AddTrackArgs, DeferredAction} from '../common/actions';
import {
InThreadTrackSortKey,
SCROLLING_TRACK_GROUP,
TrackSortKey,
UtidToTrackSortKey,
} from '../common/state';
import {globals} from '../frontend/globals';
import {PERF_SAMPLE_FLAG} from '../core/feature_flags';
import {PrimaryTrackSortKey} from '../public';
import {getTrackName} from '../public/utils';
import {Engine, EngineProxy} from '../trace_processor/engine';
import {NUM, NUM_NULL, STR, STR_NULL} from '../trace_processor/query_result';
import {ASYNC_SLICE_TRACK_KIND} from '../tracks/async_slices';
import {
ENABLE_SCROLL_JANK_PLUGIN_V2,
getScrollJankTracks,
} from '../tracks/chrome_scroll_jank';
import {decideTracks as scrollJankDecideTracks} from '../tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track';
import {SLICE_TRACK_KIND} from '../tracks/chrome_slices';
import {COUNTER_TRACK_KIND} from '../tracks/counter';
import {
ACTUAL_FRAMES_SLICE_TRACK_KIND,
EXPECTED_FRAMES_SLICE_TRACK_KIND,
} from '../tracks/frames';
import {decideTracks as screenshotDecideTracks} from '../tracks/screenshots';
import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state';
const MEM_DMA_COUNTER_NAME = 'mem.dma_heap';
const MEM_DMA = 'mem.dma_buffer';
const MEM_ION = 'mem.ion';
const F2FS_IOSTAT_TAG = 'f2fs_iostat.';
const F2FS_IOSTAT_GROUP_NAME = 'f2fs_iostat';
const F2FS_IOSTAT_LAT_TAG = 'f2fs_iostat_latency.';
const F2FS_IOSTAT_LAT_GROUP_NAME = 'f2fs_iostat_latency';
const DISK_IOSTAT_TAG = 'diskstat.';
const DISK_IOSTAT_GROUP_NAME = 'diskstat';
const BUDDY_INFO_TAG = 'mem.buddyinfo';
const UFS_CMD_TAG_REGEX = new RegExp('^io.ufs.command.tag.*$');
const UFS_CMD_TAG_GROUP = 'io.ufs.command.tags';
// NB: Userspace wakelocks start with "WakeLock" not "Wakelock".
const KERNEL_WAKELOCK_REGEX = new RegExp('^Wakelock.*$');
const KERNEL_WAKELOCK_GROUP = 'Kernel wakelocks';
const NETWORK_TRACK_REGEX = new RegExp('^.* (Received|Transmitted)( KB)?$');
const NETWORK_TRACK_GROUP = 'Networking';
const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency:');
const ENTITY_RESIDENCY_GROUP = 'Entity residency';
const UCLAMP_REGEX = new RegExp('^UCLAMP_');
const UCLAMP_GROUP = 'Scheduler Utilization Clamping';
const POWER_RAILS_GROUP = 'Power Rails';
const POWER_RAILS_REGEX = new RegExp('^power.');
const FREQUENCY_GROUP = 'Frequency Scaling';
const TEMPERATURE_REGEX = new RegExp('^.* Temperature$');
const TEMPERATURE_GROUP = 'Temperature';
const IRQ_GROUP = 'IRQs';
const IRQ_REGEX = new RegExp('^(Irq|SoftIrq) Cpu.*');
const CHROME_TRACK_REGEX = new RegExp('^Chrome.*|^InputLatency::.*');
const CHROME_TRACK_GROUP = 'Chrome Global Tracks';
const MISC_GROUP = 'Misc Global Tracks';
export async function decideTracks(engine: Engine): Promise<DeferredAction[]> {
return new TrackDecider(engine).decideTracks();
}
class TrackDecider {
private engine: Engine;
private upidToUuid = new Map<number, string>();
private utidToUuid = new Map<number, string>();
private tracksToAdd: AddTrackArgs[] = [];
private addTrackGroupActions: DeferredAction[] = [];
constructor(engine: Engine) {
this.engine = engine;
}
async guessCpuSizes(): Promise<Map<number, string>> {
const cpuToSize = new Map<number, string>();
await this.engine.query(`
INCLUDE PERFETTO MODULE cpu.size;
`);
const result = await this.engine.query(`
SELECT cpu, cpu_guess_core_type(cpu) as size FROM cpu_counter_track;
`);
const it = result.iter({
cpu: NUM,
size: STR_NULL,
});
for (; it.valid(); it.next()) {
const size = it.size;
if (size !== null) {
cpuToSize.set(it.cpu, size);
}
}
return cpuToSize;
}
async addCpuSchedulingTracks(): Promise<void> {
const cpus = await this.engine.getCpus();
const cpuToSize = await this.guessCpuSizes();
for (const cpu of cpus) {
const size = cpuToSize.get(cpu);
const name = size === undefined ? `Cpu ${cpu}` : `Cpu ${cpu} (${size})`;
this.tracksToAdd.push({
uri: `perfetto.CpuSlices#cpu${cpu}`,
trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
name,
trackGroup: SCROLLING_TRACK_GROUP,
});
}
}
async addCpuFreqTracks(engine: EngineProxy): Promise<void> {
const cpus = await this.engine.getCpus();
for (const cpu of cpus) {
// Only add a cpu freq track if we have
// cpu freq data.
// TODO(hjd): Find a way to display cpu idle
// events even if there are no cpu freq events.
const cpuFreqIdleResult = await engine.query(`
select
id as cpuFreqId,
(
select id
from cpu_counter_track
where name = 'cpuidle'
and cpu = ${cpu}
limit 1
) as cpuIdleId
from cpu_counter_track
where name = 'cpufreq' and cpu = ${cpu}
limit 1;
`);
if (cpuFreqIdleResult.numRows() > 0) {
this.tracksToAdd.push({
uri: `perfetto.CpuFreq#${cpu}`,
trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
name: `Cpu ${cpu} Frequency`,
trackGroup: SCROLLING_TRACK_GROUP,
});
}
}
}
async addGlobalAsyncTracks(engine: EngineProxy): Promise<void> {
const rawGlobalAsyncTracks = await engine.query(`
with global_tracks_grouped as (
select distinct t.parent_id, t.name
from track t
join _slice_track_summary using (id)
where t.type in ('track', 'gpu_track', 'cpu_track')
)
select
t.name as name,
t.parent_id as parentId,
p.name as parentName
from global_tracks_grouped AS t
left join track p on (t.parent_id = p.id)
order by p.name, t.name
`);
const it = rawGlobalAsyncTracks.iter({
name: STR_NULL,
parentId: NUM_NULL,
parentName: STR_NULL,
});
const parentIdToGroupId = new Map<number, string>();
for (; it.valid(); it.next()) {
const kind = ASYNC_SLICE_TRACK_KIND;
const rawName = it.name === null ? undefined : it.name;
const rawParentName = it.parentName === null ? undefined : it.parentName;
const name = getTrackName({name: rawName, kind});
const parentTrackId = it.parentId;
let trackGroup = SCROLLING_TRACK_GROUP;
if (parentTrackId !== null) {
const groupId = parentIdToGroupId.get(parentTrackId);
if (groupId === undefined) {
trackGroup = uuidv4();
parentIdToGroupId.set(parentTrackId, trackGroup);
const parentName = getTrackName({name: rawParentName, kind});
this.addTrackGroupActions.push(
Actions.addTrackGroup({
name: parentName,
id: trackGroup,
collapsed: true,
}),
);
} else {
trackGroup = groupId;
}
}
const track: AddTrackArgs = {
uri: `perfetto.AsyncSlices#${rawName}.${it.parentId}`,
trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
trackGroup,
name,
};
this.tracksToAdd.push(track);
}
}
async addGpuFreqTracks(engine: EngineProxy): Promise<void> {
const numGpus = await this.engine.getNumberOfGpus();
for (let gpu = 0; gpu < numGpus; gpu++) {
// Only add a gpu freq track if we have
// gpu freq data.
const freqExistsResult = await engine.query(`
select *
from gpu_counter_track
where name = 'gpufreq' and gpu_id = ${gpu}
limit 1;
`);
if (freqExistsResult.numRows() > 0) {
this.tracksToAdd.push({
uri: `perfetto.Counter#gpu_freq${gpu}`,
name: `Gpu ${gpu} Frequency`,
trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
trackGroup: SCROLLING_TRACK_GROUP,
});
}
}
}
async addCpuFreqLimitCounterTracks(engine: EngineProxy): Promise<void> {
const cpuFreqLimitCounterTracksSql = `
select name, id
from cpu_counter_track
where name glob "Cpu * Freq Limit"
order by name asc
`;
this.addCpuCounterTracks(engine, cpuFreqLimitCounterTracksSql);
}
async addCpuPerfCounterTracks(engine: EngineProxy): Promise<void> {
// Perf counter tracks are bound to CPUs, follow the scheduling and
// frequency track naming convention ("Cpu N ...").
// Note: we might not have a track for a given cpu if no data was seen from
// it. This might look surprising in the UI, but placeholder tracks are
// wasteful as there's no way of collapsing global counter tracks at the
// moment.
const addCpuPerfCounterTracksSql = `
select printf("Cpu %u %s", cpu, name) as name, id
from perf_counter_track as pct
order by perf_session_id asc, pct.name asc, cpu asc
`;
this.addCpuCounterTracks(engine, addCpuPerfCounterTracksSql);
}
async addCpuCounterTracks(engine: EngineProxy, sql: string): Promise<void> {
const result = await engine.query(sql);
const it = result.iter({
name: STR,
id: NUM,
});
for (; it.valid(); it.next()) {
const name = it.name;
const trackId = it.id;
this.tracksToAdd.push({
uri: `perfetto.Counter#cpu${trackId}`,
name,
trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
trackGroup: SCROLLING_TRACK_GROUP,
});
}
}
async groupGlobalIonTracks(): Promise<void> {
const ionTracks: AddTrackArgs[] = [];
let hasSummary = false;
for (const track of this.tracksToAdd) {
const isIon = track.name.startsWith(MEM_ION);
const isIonCounter = track.name === MEM_ION;
const isDmaHeapCounter = track.name === MEM_DMA_COUNTER_NAME;
const isDmaBuffferSlices = track.name === MEM_DMA;
if (isIon || isIonCounter || isDmaHeapCounter || isDmaBuffferSlices) {
ionTracks.push(track);
}
hasSummary = hasSummary || isIonCounter;
hasSummary = hasSummary || isDmaHeapCounter;
}
if (ionTracks.length === 0 || !hasSummary) {
return;
}
const id = uuidv4();
const summaryTrackKey = uuidv4();
let foundSummary = false;
for (const track of ionTracks) {
if (
!foundSummary &&
[MEM_DMA_COUNTER_NAME, MEM_ION].includes(track.name)
) {
foundSummary = true;
track.key = summaryTrackKey;
track.trackGroup = undefined;
} else {
track.trackGroup = id;
}
}
const addGroup = Actions.addTrackGroup({
summaryTrackKey,
name: MEM_DMA_COUNTER_NAME,
id,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
}
async groupGlobalIostatTracks(tag: string, group: string): Promise<void> {
const iostatTracks: AddTrackArgs[] = [];
const devMap = new Map<string, string>();
for (const track of this.tracksToAdd) {
if (track.name.startsWith(tag)) {
iostatTracks.push(track);
}
}
if (iostatTracks.length === 0) {
return;
}
for (const track of iostatTracks) {
const name = track.name.split('.', 3);
if (!devMap.has(name[1])) {
devMap.set(name[1], uuidv4());
}
track.name = name[2];
track.trackGroup = devMap.get(name[1]);
}
for (const [key, value] of devMap) {
const groupName = group + key;
const addGroup = Actions.addTrackGroup({
name: groupName,
id: value,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
}
}
async groupGlobalBuddyInfoTracks(): Promise<void> {
const buddyInfoTracks: AddTrackArgs[] = [];
const devMap = new Map<string, string>();
for (const track of this.tracksToAdd) {
if (track.name.startsWith(BUDDY_INFO_TAG)) {
buddyInfoTracks.push(track);
}
}
if (buddyInfoTracks.length === 0) {
return;
}
for (const track of buddyInfoTracks) {
const tokens = track.name.split('[');
const node = tokens[1].slice(0, -1);
const zone = tokens[2].slice(0, -1);
const size = tokens[3].slice(0, -1);
const groupName = 'Buddyinfo: Node: ' + node + ' Zone: ' + zone;
if (!devMap.has(groupName)) {
devMap.set(groupName, uuidv4());
}
track.name = 'Chunk size: ' + size;
track.trackGroup = devMap.get(groupName);
}
for (const [key, value] of devMap) {
const groupName = key;
const addGroup = Actions.addTrackGroup({
name: groupName,
id: value,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
}
}
async groupFrequencyTracks(groupName: string): Promise<void> {
let groupUuid = undefined;
for (const track of this.tracksToAdd) {
// Group all the frequency tracks together (except the CPU and GPU
// frequency ones).
if (
track.name.endsWith('Frequency') &&
!track.name.startsWith('Cpu') &&
!track.name.startsWith('Gpu')
) {
if (
track.trackGroup !== undefined &&
track.trackGroup !== SCROLLING_TRACK_GROUP
) {
continue;
}
if (groupUuid === undefined) {
groupUuid = uuidv4();
}
track.trackGroup = groupUuid;
}
}
if (groupUuid !== undefined) {
const addGroup = Actions.addTrackGroup({
name: groupName,
id: groupUuid,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
}
}
async groupMiscNonAllowlistedTracks(groupName: string): Promise<void> {
// List of allowlisted track names.
const ALLOWLIST_REGEXES = [
new RegExp('^Cpu .*$', 'i'),
new RegExp('^Gpu .*$', 'i'),
new RegExp('^Trace Triggers$'),
new RegExp('^Android App Startups$'),
new RegExp('^Device State.*$'),
new RegExp('^Android logs$'),
];
let groupUuid = undefined;
for (const track of this.tracksToAdd) {
if (
track.trackGroup !== undefined &&
track.trackGroup !== SCROLLING_TRACK_GROUP
) {
continue;
}
let allowlisted = false;
for (const regex of ALLOWLIST_REGEXES) {
allowlisted = allowlisted || regex.test(track.name);
}
if (allowlisted) {
continue;
}
if (groupUuid === undefined) {
groupUuid = uuidv4();
}
track.trackGroup = groupUuid;
}
if (groupUuid !== undefined) {
const addGroup = Actions.addTrackGroup({
name: groupName,
id: groupUuid,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
}
}
async groupTracksByRegex(regex: RegExp, groupName: string): Promise<void> {
let groupUuid = undefined;
for (const track of this.tracksToAdd) {
if (regex.test(track.name)) {
if (
track.trackGroup !== undefined &&
track.trackGroup !== SCROLLING_TRACK_GROUP
) {
continue;
}
if (groupUuid === undefined) {
groupUuid = uuidv4();
}
track.trackGroup = groupUuid;
}
}
if (groupUuid !== undefined) {
const addGroup = Actions.addTrackGroup({
name: groupName,
id: groupUuid,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
}
}
async addAnnotationTracks(engine: EngineProxy): Promise<void> {
const sliceResult = await engine.query(`
select id, name, upid, group_name
from annotation_slice_track
order by name
`);
const sliceIt = sliceResult.iter({
id: NUM,
name: STR,
upid: NUM,
group_name: STR_NULL,
});
interface GroupIds {
id: string;
summaryTrackKey: string;
}
const groupNameToIds = new Map<string, GroupIds>();
for (; sliceIt.valid(); sliceIt.next()) {
const id = sliceIt.id;
const name = sliceIt.name;
const upid = sliceIt.upid;
const groupName = sliceIt.group_name;
let summaryTrackKey = undefined;
let trackGroupId =
upid === 0 ? SCROLLING_TRACK_GROUP : this.upidToUuid.get(upid);
if (groupName) {
// If this is the first track encountered for a certain group,
// create an id for the group and use this track as the group's
// summary track.
const groupIds = groupNameToIds.get(groupName);
if (groupIds) {
trackGroupId = groupIds.id;
} else {
trackGroupId = uuidv4();
summaryTrackKey = uuidv4();
groupNameToIds.set(groupName, {
id: trackGroupId,
summaryTrackKey,
});
}
}
this.tracksToAdd.push({
uri: `perfetto.Annotation#${id}`,
key: summaryTrackKey,
name,
trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
trackGroup: trackGroupId,
});
}
for (const [groupName, groupIds] of groupNameToIds) {
const addGroup = Actions.addTrackGroup({
summaryTrackKey: groupIds.summaryTrackKey,
name: groupName,
id: groupIds.id,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
}
const counterResult = await engine.query(`
SELECT id, name, upid FROM annotation_counter_track
`);
const counterIt = counterResult.iter({
id: NUM,
name: STR,
upid: NUM,
});
for (; counterIt.valid(); counterIt.next()) {
const id = counterIt.id;
const name = counterIt.name;
const upid = counterIt.upid;
this.tracksToAdd.push({
uri: `perfetto.Annotation#counter${id}`,
name,
trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
trackGroup:
upid === 0 ? SCROLLING_TRACK_GROUP : this.upidToUuid.get(upid),
});
}
}
async addThreadStateTracks(engine: EngineProxy): Promise<void> {
const result = await engine.query(`
select
utid,
upid,
tid,
thread.name as threadName
from thread
join _sched_summary using (utid)
`);
const it = result.iter({
utid: NUM,
upid: NUM_NULL,
tid: NUM_NULL,
threadName: STR_NULL,
});
for (; it.valid(); it.next()) {
const utid = it.utid;
const tid = it.tid;
const upid = it.upid;
const threadName = it.threadName;
const uuid = this.getUuidUnchecked(utid, upid);
if (uuid === undefined) {
// If a thread has no scheduling activity (i.e. the sched table has zero
// rows for that uid) no track group will be created and we want to skip
// the track creation as well.
continue;
}
const priority = InThreadTrackSortKey.THREAD_SCHEDULING_STATE_TRACK;
const name = getTrackName({
utid,
tid,
threadName,
kind: THREAD_STATE_TRACK_KIND,
});
this.tracksToAdd.push({
uri: `perfetto.ThreadState#${utid}`,
name,
trackGroup: uuid,
trackSortKey: {
utid,
priority,
},
});
}
}
async addThreadCpuSampleTracks(engine: EngineProxy): Promise<void> {
const result = await engine.query(`
with thread_cpu_sample as (
select distinct utid
from cpu_profile_stack_sample
where utid != 0
)
select
utid,
tid,
upid,
thread.name as threadName
from thread_cpu_sample
join thread using(utid)`);
const it = result.iter({
utid: NUM,
upid: NUM_NULL,
tid: NUM_NULL,
threadName: STR_NULL,
});
for (; it.valid(); it.next()) {
const utid = it.utid;
const upid = it.upid;
const threadName = it.threadName;
const uuid = this.getUuid(utid, upid);
this.tracksToAdd.push({
uri: `perfetto.CpuProfile#${utid}`,
trackSortKey: {
utid,
priority: InThreadTrackSortKey.CPU_STACK_SAMPLES_TRACK,
},
name: `${threadName} (CPU Stack Samples)`,
trackGroup: uuid,
});
}
}
async addThreadCounterTracks(engine: EngineProxy): Promise<void> {
const result = await engine.query(`
select
thread_counter_track.name as trackName,
utid,
upid,
tid,
thread.name as threadName,
thread_counter_track.id as trackId
from thread_counter_track
join thread using(utid)
where thread_counter_track.name != 'thread_time'
`);
const it = result.iter({
trackName: STR_NULL,
utid: NUM,
upid: NUM_NULL,
tid: NUM_NULL,
threadName: STR_NULL,
trackId: NUM,
});
for (; it.valid(); it.next()) {
const utid = it.utid;
const tid = it.tid;
const upid = it.upid;
const trackId = it.trackId;
const trackName = it.trackName;
const threadName = it.threadName;
const uuid = this.getUuid(utid, upid);
const name = getTrackName({
name: trackName,
utid,
tid,
kind: COUNTER_TRACK_KIND,
threadName,
threadTrack: true,
});
this.tracksToAdd.push({
uri: `perfetto.Counter#thread${trackId}`,
name,
trackSortKey: {
utid,
priority: InThreadTrackSortKey.ORDINARY,
},
trackGroup: uuid,
});
}
}
async addProcessAsyncSliceTracks(engine: EngineProxy): Promise<void> {
const result = await engine.query(`
select
upid,
t.name as trackName,
t.track_ids as trackIds,
process.name as processName,
process.pid as pid
from _process_track_summary_by_upid_and_name t
join process using(upid)
where t.name is null or t.name not glob "* Timeline"
`);
const it = result.iter({
upid: NUM,
trackName: STR_NULL,
trackIds: STR,
processName: STR_NULL,
pid: NUM_NULL,
});
for (; it.valid(); it.next()) {
const upid = it.upid;
const trackName = it.trackName;
const rawTrackIds = it.trackIds;
const processName = it.processName;
const pid = it.pid;
const uuid = this.getUuid(null, upid);
const name = getTrackName({
name: trackName,
upid,
pid,
processName,
kind: ASYNC_SLICE_TRACK_KIND,
});
this.tracksToAdd.push({
uri: `perfetto.AsyncSlices#process.${pid}${rawTrackIds}`,
name,
trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
trackGroup: uuid,
});
}
}
async addUserAsyncSliceTracks(engine: EngineProxy): Promise<void> {
const result = await engine.query(`
select
t.name as name,
t.uid as uid,
package_list.package_name as packageName
from _uid_track_track_summary_by_uid_and_name t
join package_list using (uid)
`);
const it = result.iter({
name: STR_NULL,
uid: NUM_NULL,
packageName: STR_NULL,
});
// Map From [name] -> [uuid, key]
const groupMap = new Map<string, string>();
for (; it.valid(); it.next()) {
if (it.name == null || it.uid == null) {
continue;
}
const rawName = it.name;
const uid = it.uid === null ? undefined : it.uid;
const userName = it.packageName === null ? `UID: ${uid}` : it.packageName;
const groupUuid = `uid-track-group${rawName}`;
if (groupMap.get(rawName) === undefined) {
groupMap.set(rawName, groupUuid);
}
this.tracksToAdd.push({
uri: `perfetto.AsyncSlices#${rawName}.${uid}`,
name: userName,
trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
trackGroup: groupUuid,
});
}
for (const [name, groupUuid] of groupMap) {
const addGroup = Actions.addTrackGroup({
name: name,
id: groupUuid,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
}
}
async addActualFramesTracks(engine: EngineProxy): Promise<void> {
const result = await engine.query(`
select
upid,
t.name as trackName,
process.name as processName,
process.pid as pid
from _process_track_summary_by_upid_and_name t
join process using(upid)
where t.name = "Actual Timeline"
`);
const it = result.iter({
upid: NUM,
trackName: STR_NULL,
processName: STR_NULL,
pid: NUM_NULL,
});
for (; it.valid(); it.next()) {
const upid = it.upid;
const trackName = it.trackName;
const processName = it.processName;
const pid = it.pid;
const uuid = this.getUuid(null, upid);
const kind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
const name = getTrackName({
name: trackName,
upid,
pid,
processName,
kind,
});
this.tracksToAdd.push({
uri: `perfetto.ActualFrames#${upid}`,
name,
trackSortKey: PrimaryTrackSortKey.ACTUAL_FRAMES_SLICE_TRACK,
trackGroup: uuid,
});
}
}
async addExpectedFramesTracks(engine: EngineProxy): Promise<void> {
const result = await engine.query(`
select
upid,
t.name as trackName,
process.name as processName,
process.pid as pid
from _process_track_summary_by_upid_and_name t
join process using(upid)
where t.name = "Expected Timeline"
`);
const it = result.iter({
upid: NUM,
trackName: STR_NULL,
processName: STR_NULL,
pid: NUM_NULL,
});
for (; it.valid(); it.next()) {
const upid = it.upid;
const trackName = it.trackName;
const processName = it.processName;
const pid = it.pid;
const uuid = this.getUuid(null, upid);
const kind = EXPECTED_FRAMES_SLICE_TRACK_KIND;
const name = getTrackName({
name: trackName,
upid,
pid,
processName,
kind,
});
this.tracksToAdd.push({
uri: `perfetto.ExpectedFrames#${upid}`,
name,
trackSortKey: PrimaryTrackSortKey.EXPECTED_FRAMES_SLICE_TRACK,
trackGroup: uuid,
});
}
}
async addThreadSliceTracks(engine: EngineProxy): Promise<void> {
const result = await engine.query(`
select
thread_track.utid as utid,
thread_track.id as trackId,
thread_track.name as trackName,
EXTRACT_ARG(thread_track.source_arg_set_id,
'is_root_in_scope') as isDefaultTrackForScope,
tid,
thread.name as threadName,
thread.upid as upid
from thread_track
join _slice_track_summary using (id)
join thread using(utid)
`);
const it = result.iter({
utid: NUM,
trackId: NUM,
trackName: STR_NULL,
isDefaultTrackForScope: NUM_NULL,
tid: NUM_NULL,
threadName: STR_NULL,
upid: NUM_NULL,
});
for (; it.valid(); it.next()) {
const utid = it.utid;
const trackId = it.trackId;
const trackName = it.trackName;
// Note that !!null === false.
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
const isDefaultTrackForScope = !!it.isDefaultTrackForScope;
const tid = it.tid;
const threadName = it.threadName;
const upid = it.upid;
const uuid = this.getUuid(utid, upid);
const kind = SLICE_TRACK_KIND;
const name = getTrackName({name: trackName, utid, tid, threadName, kind});
this.tracksToAdd.push({
uri: `perfetto.ChromeSlices#${trackId}`,
name,
trackGroup: uuid,
trackSortKey: {
utid,
priority: isDefaultTrackForScope
? InThreadTrackSortKey.DEFAULT_TRACK
: InThreadTrackSortKey.ORDINARY,
},
});
}
}
async addProcessCounterTracks(engine: EngineProxy): Promise<void> {
const result = await engine.query(`
select
process_counter_track.id as trackId,
process_counter_track.name as trackName,
upid,
process.pid,
process.name as processName
from process_counter_track
join process using(upid);
`);
const it = result.iter({
trackId: NUM,
trackName: STR_NULL,
upid: NUM,
pid: NUM_NULL,
processName: STR_NULL,
});
for (let i = 0; it.valid(); ++i, it.next()) {
const pid = it.pid;
const upid = it.upid;
const trackId = it.trackId;
const trackName = it.trackName;
const processName = it.processName;
const uuid = this.getUuid(null, upid);
const name = getTrackName({
name: trackName,
upid,
pid,
kind: COUNTER_TRACK_KIND,
processName,
});
this.tracksToAdd.push({
uri: `perfetto.Counter#process${trackId}`,
name,
trackSortKey: await this.resolveTrackSortKeyForProcessCounterTrack(
upid,
trackName || undefined,
),
trackGroup: uuid,
});
}
}
async addProcessHeapProfileTracks(engine: EngineProxy): Promise<void> {
const result = await engine.query(`
select upid
from _process_available_info_summary
where allocation_count > 0 or graph_object_count > 0
`);
for (const it = result.iter({upid: NUM}); it.valid(); it.next()) {
const upid = it.upid;
const uuid = this.getUuid(null, upid);
this.tracksToAdd.push({
uri: `perfetto.HeapProfile#${upid}`,
trackSortKey: PrimaryTrackSortKey.HEAP_PROFILE_TRACK,
name: `Heap Profile`,
trackGroup: uuid,
});
}
}
async addProcessPerfSamplesTracks(engine: EngineProxy): Promise<void> {
const result = await engine.query(`
select upid, pid
from _process_available_info_summary
join process using (upid)
where perf_sample_count > 0
`);
for (const it = result.iter({upid: NUM, pid: NUM}); it.valid(); it.next()) {
const upid = it.upid;
const pid = it.pid;
const uuid = this.getUuid(null, upid);
this.tracksToAdd.push({
uri: `perfetto.PerfSamplesProfile#${upid}`,
trackSortKey: PrimaryTrackSortKey.PERF_SAMPLES_PROFILE_TRACK,
name: `Callstacks ${pid}`,
trackGroup: uuid,
});
}
}
getUuidUnchecked(utid: number | null, upid: number | null) {
return upid === null
? this.utidToUuid.get(utid!)
: this.upidToUuid.get(upid);
}
getUuid(utid: number | null, upid: number | null) {
return assertExists(this.getUuidUnchecked(utid, upid));
}
getOrCreateUuid(utid: number | null, upid: number | null) {
let uuid = this.getUuidUnchecked(utid, upid);
if (uuid === undefined) {
uuid = uuidv4();
if (upid === null) {
this.utidToUuid.set(utid!, uuid);
} else {
this.upidToUuid.set(upid, uuid);
}
}
return uuid;
}
setUuidForUpid(upid: number, uuid: string) {
this.upidToUuid.set(upid, uuid);
}
async addKernelThreadGrouping(engine: EngineProxy): 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 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 kthreadGroupUuid = uuidv4();
const summaryTrackKey = uuidv4();
this.tracksToAdd.push({
uri: 'perfetto.ProcessSummary#kernel',
key: summaryTrackKey,
trackSortKey: PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
name: `Kernel thread summary`,
});
const addTrackGroup = Actions.addTrackGroup({
summaryTrackKey,
name: `Kernel threads`,
id: kthreadGroupUuid,
collapsed: true,
});
this.addTrackGroupActions.push(addTrackGroup);
// Set the group for all kernel threads (including kthreadd itself).
for (; it.valid(); it.next()) {
this.setUuidForUpid(it.upid, kthreadGroupUuid);
}
}
async addProcessTrackGroups(engine: EngineProxy): Promise<void> {
// We want to create groups of tracks in a specific order.
// The tracks should be grouped:
// by upid
// or (if upid is null) by utid
// the groups should be sorted by:
// Chrome-based process rank based on process names (e.g. Browser)
// has a heap profile or not
// total cpu time *for the whole parent process*
// process name
// upid
// thread name
// utid
const result = await 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
upid,
null as utid,
pid,
null as tid,
processName,
null as threadName,
sumRunningDur > 0 as hasSched,
heapProfileAllocationCount > 0
or heapGraphObjectCount > 0 as hasHeapInfo,
ifnull(chromeProcessLabels, '') as chromeProcessLabels
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
null,
utid,
null as pid,
tid,
null as processName,
threadName,
sumRunningDur > 0 as hasSched,
0 as hasHeapInfo,
'' as chromeProcessLabels
from threadGroups
order by
perfSampleCount desc,
sumRunningDur desc,
sliceCount desc,
threadName asc,
utid asc
)
`);
const it = result.iter({
upid: NUM_NULL,
utid: NUM_NULL,
pid: NUM_NULL,
tid: NUM_NULL,
processName: STR_NULL,
threadName: STR_NULL,
hasSched: NUM_NULL,
hasHeapInfo: NUM_NULL,
chromeProcessLabels: STR,
});
for (; it.valid(); it.next()) {
const utid = it.utid;
const upid = it.upid;
const pid = it.pid;
const tid = it.tid;
const threadName = it.threadName;
const processName = it.processName;
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
const hasSched = !!it.hasSched;
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
const hasHeapInfo = !!it.hasHeapInfo;
const summaryTrackKey = uuidv4();
const type = hasSched ? 'schedule' : 'summary';
const uri = `perfetto.ProcessScheduling#${upid}.${utid}.${type}`;
// If previous groupings (e.g. kernel threads) picked up there tracks,
// don't try to regroup them.
const pUuid =
upid === null ? this.utidToUuid.get(utid!) : this.upidToUuid.get(upid);
if (pUuid !== undefined) {
continue;
}
this.tracksToAdd.push({
uri,
key: summaryTrackKey,
trackSortKey: hasSched
? PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK
: PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
name: `${upid === null ? tid : pid} summary`,
labels: it.chromeProcessLabels.split(','),
});
const name = getTrackName({
utid,
processName,
pid,
threadName,
tid,
upid,
});
const addTrackGroup = Actions.addTrackGroup({
summaryTrackKey,
name,
id: this.getOrCreateUuid(utid, upid),
// Perf profiling tracks remain collapsed, otherwise we would have too
// many expanded process tracks for some perf traces, leading to
// jankyness.
collapsed: !hasHeapInfo,
});
this.addTrackGroupActions.push(addTrackGroup);
}
}
private async computeThreadOrderingMetadata(): Promise<UtidToTrackSortKey> {
const result = await this.engine.query(`
select
utid,
tid,
(select pid from process p where t.upid = p.upid) as pid,
t.name as threadName
from thread t`);
const it = result.iter({
utid: NUM,
tid: NUM_NULL,
pid: NUM_NULL,
threadName: STR_NULL,
});
const threadOrderingMetadata: UtidToTrackSortKey = {};
for (; it.valid(); it.next()) {
threadOrderingMetadata[it.utid] = {
tid: it.tid === null ? undefined : it.tid,
sortKey: TrackDecider.getThreadSortKey(it.threadName, it.tid, it.pid),
};
}
return threadOrderingMetadata;
}
addPluginTracks(): void {
const groupNameToUuid = new Map<string, string>();
const tracks = globals.trackManager.findPotentialTracks();
for (const info of tracks) {
const groupName = info.groupName;
let groupUuid = SCROLLING_TRACK_GROUP;
if (groupName) {
const uuid = groupNameToUuid.get(groupName);
if (uuid) {
groupUuid = uuid;
} else {
// Add the group
groupUuid = uuidv4();
const addGroup = Actions.addTrackGroup({
name: groupName,
id: groupUuid,
collapsed: true,
fixedOrdering: true,
});
this.addTrackGroupActions.push(addGroup);
// Add group to the map
groupNameToUuid.set(groupName, groupUuid);
}
}
this.tracksToAdd.push({
uri: info.uri,
name: info.displayName,
// TODO(hjd): Fix how sorting works. Plugins should expose
// 'sort keys' which the user can use to choose a sort order.
trackSortKey: info.sortKey ?? PrimaryTrackSortKey.ORDINARY_TRACK,
trackGroup: groupUuid,
params: info.params,
});
}
}
async addScrollJankPluginTracks(): Promise<void> {
if (ENABLE_SCROLL_JANK_PLUGIN_V2.get()) {
const result = await getScrollJankTracks(this.engine);
this.tracksToAdd = this.tracksToAdd.concat(result.tracks.tracksToAdd);
this.addTrackGroupActions.push(result.addTrackGroup);
}
}
async decideTracks(): Promise<DeferredAction[]> {
{
const result = screenshotDecideTracks(this.engine);
if (result !== null) {
const {tracksToAdd} = await result;
this.tracksToAdd.push(...tracksToAdd);
}
}
// Add first the global tracks that don't require per-process track groups.
await this.addScrollJankPluginTracks();
await this.addCpuSchedulingTracks();
await this.addCpuFreqTracks(
this.engine.getProxy('TrackDecider::addCpuFreqTracks'),
);
await this.addGlobalAsyncTracks(
this.engine.getProxy('TrackDecider::addGlobalAsyncTracks'),
);
await this.addGpuFreqTracks(
this.engine.getProxy('TrackDecider::addGpuFreqTracks'),
);
await this.addCpuFreqLimitCounterTracks(
this.engine.getProxy('TrackDecider::addCpuFreqLimitCounterTracks'),
);
await this.addCpuPerfCounterTracks(
this.engine.getProxy('TrackDecider::addCpuPerfCounterTracks'),
);
this.addPluginTracks();
await this.addAnnotationTracks(
this.engine.getProxy('TrackDecider::addAnnotationTracks'),
);
await this.groupGlobalIonTracks();
await this.groupGlobalIostatTracks(F2FS_IOSTAT_TAG, F2FS_IOSTAT_GROUP_NAME);
await this.groupGlobalIostatTracks(
F2FS_IOSTAT_LAT_TAG,
F2FS_IOSTAT_LAT_GROUP_NAME,
);
await this.groupGlobalIostatTracks(DISK_IOSTAT_TAG, DISK_IOSTAT_GROUP_NAME);
await this.groupTracksByRegex(UFS_CMD_TAG_REGEX, UFS_CMD_TAG_GROUP);
await this.groupGlobalBuddyInfoTracks();
await this.groupTracksByRegex(KERNEL_WAKELOCK_REGEX, KERNEL_WAKELOCK_GROUP);
await this.groupTracksByRegex(NETWORK_TRACK_REGEX, NETWORK_TRACK_GROUP);
await this.groupTracksByRegex(
ENTITY_RESIDENCY_REGEX,
ENTITY_RESIDENCY_GROUP,
);
await this.groupTracksByRegex(UCLAMP_REGEX, UCLAMP_GROUP);
await this.groupFrequencyTracks(FREQUENCY_GROUP);
await this.groupTracksByRegex(POWER_RAILS_REGEX, POWER_RAILS_GROUP);
await this.groupTracksByRegex(TEMPERATURE_REGEX, TEMPERATURE_GROUP);
await this.groupTracksByRegex(IRQ_REGEX, IRQ_GROUP);
await this.groupTracksByRegex(CHROME_TRACK_REGEX, CHROME_TRACK_GROUP);
await this.groupMiscNonAllowlistedTracks(MISC_GROUP);
// Add user slice tracks before listing the processes. These tracks will
// be listed with their user/package name only, and they will be grouped
// under on their original shared track names. E.g. "GPU Work Period"
await this.addUserAsyncSliceTracks(
this.engine.getProxy('TrackDecider::addUserAsyncSliceTracks'),
);
// 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(
this.engine.getProxy('TrackDecider::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.addProcessTrackGroups(
this.engine.getProxy('TrackDecider::addProcessTrackGroups'),
);
await this.addProcessHeapProfileTracks(
this.engine.getProxy('TrackDecider::addProcessHeapProfileTracks'),
);
if (PERF_SAMPLE_FLAG.get()) {
await this.addProcessPerfSamplesTracks(
this.engine.getProxy('TrackDecider::addProcessPerfSamplesTracks'),
);
}
await this.addProcessCounterTracks(
this.engine.getProxy('TrackDecider::addProcessCounterTracks'),
);
await this.addProcessAsyncSliceTracks(
this.engine.getProxy('TrackDecider::addProcessAsyncSliceTracks'),
);
await this.addActualFramesTracks(
this.engine.getProxy('TrackDecider::addActualFramesTracks'),
);
await this.addExpectedFramesTracks(
this.engine.getProxy('TrackDecider::addExpectedFramesTracks'),
);
await this.addThreadCounterTracks(
this.engine.getProxy('TrackDecider::addThreadCounterTracks'),
);
await this.addThreadStateTracks(
this.engine.getProxy('TrackDecider::addThreadStateTracks'),
);
await this.addThreadSliceTracks(
this.engine.getProxy('TrackDecider::addThreadSliceTracks'),
);
await this.addThreadCpuSampleTracks(
this.engine.getProxy('TrackDecider::addThreadCpuSampleTracks'),
);
// TODO(hjd): Move into plugin API.
{
const result = scrollJankDecideTracks(this.engine, (utid, upid) => {
return this.getUuid(utid, upid);
});
if (result !== null) {
const {tracksToAdd} = await result;
this.tracksToAdd.push(...tracksToAdd);
}
}
this.addTrackGroupActions.push(
Actions.addTracks({tracks: this.tracksToAdd}),
);
const threadOrderingMetadata = await this.computeThreadOrderingMetadata();
this.addTrackGroupActions.push(
Actions.setUtidToTrackSortKey({threadOrderingMetadata}),
);
return this.addTrackGroupActions;
}
// Some process counter tracks are tied to specific threads based on their
// name.
private async resolveTrackSortKeyForProcessCounterTrack(
upid: number,
threadName?: string,
): Promise<TrackSortKey> {
if (threadName !== 'GPU completion') {
return PrimaryTrackSortKey.COUNTER_TRACK;
}
const result = await this.engine.query(`
select utid
from thread
where upid=${upid} and name=${sqliteString(threadName)}
`);
const it = result.iter({
utid: NUM,
});
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
for (; it; it.next()) {
return {
utid: it.utid,
priority: InThreadTrackSortKey.THREAD_COUNTER_TRACK,
};
}
return PrimaryTrackSortKey.COUNTER_TRACK;
}
private static getThreadSortKey(
threadName?: string | null,
tid?: number | null,
pid?: number | null,
): PrimaryTrackSortKey {
if (pid !== undefined && pid !== null && pid === tid) {
return PrimaryTrackSortKey.MAIN_THREAD;
}
if (threadName === undefined || threadName === null) {
return PrimaryTrackSortKey.ORDINARY_THREAD;
}
// Chrome main threads should always come first within their process.
if (
threadName === 'CrBrowserMain' ||
threadName === 'CrRendererMain' ||
threadName === 'CrGpuMain'
) {
return PrimaryTrackSortKey.MAIN_THREAD;
}
// Chrome IO threads should always come immediately after the main thread.
if (
threadName === 'Chrome_ChildIOThread' ||
threadName === 'Chrome_IOThread'
) {
return PrimaryTrackSortKey.CHROME_IO_THREAD;
}
// A Chrome process can have only one compositor thread, so we want to put
// it next to other named processes.
if (threadName === 'Compositor' || threadName === 'VizCompositorThread') {
return PrimaryTrackSortKey.CHROME_COMPOSITOR_THREAD;
}
switch (true) {
case /.*RenderThread.*/.test(threadName):
return PrimaryTrackSortKey.RENDER_THREAD;
case /.*GPU completion.*/.test(threadName):
return PrimaryTrackSortKey.GPU_COMPLETION_THREAD;
default:
return PrimaryTrackSortKey.ORDINARY_THREAD;
}
}
}