blob: eddf01806bcb1dcfaad9050586952a54d01f208b [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 {
Actions,
AddTrackArgs,
DeferredAction,
} from '../common/actions';
import {Engine} from '../common/engine';
import {featureFlags, PERF_SAMPLE_FLAG} from '../common/feature_flags';
import {
NUM,
NUM_NULL,
STR,
STR_NULL,
} from '../common/query_result';
import {SCROLLING_TRACK_GROUP, TrackKindPriority} from '../common/state';
import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../tracks/actual_frames/common';
import {ANDROID_LOGS_TRACK_KIND} from '../tracks/android_log/common';
import {ASYNC_SLICE_TRACK_KIND} from '../tracks/async_slices/common';
import {SLICE_TRACK_KIND} from '../tracks/chrome_slices/common';
import {COUNTER_TRACK_KIND} from '../tracks/counter/common';
import {CPU_FREQ_TRACK_KIND} from '../tracks/cpu_freq/common';
import {CPU_PROFILE_TRACK_KIND} from '../tracks/cpu_profile/common';
import {CPU_SLICE_TRACK_KIND} from '../tracks/cpu_slices/common';
import {
EXPECTED_FRAMES_SLICE_TRACK_KIND,
} from '../tracks/expected_frames/common';
import {HEAP_PROFILE_TRACK_KIND} from '../tracks/heap_profile/common';
import {NULL_TRACK_KIND} from '../tracks/null_track';
import {
PERF_SAMPLES_PROFILE_TRACK_KIND,
} from '../tracks/perf_samples_profile/common';
import {
PROCESS_SCHEDULING_TRACK_KIND,
} from '../tracks/process_scheduling/common';
import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary/common';
import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state/common';
const NULL_TRACKS_FLAG = featureFlags.register({
id: 'nullTracks',
name: 'Null tracks',
description: 'Display some empty tracks.',
defaultValue: false,
});
const TRACKS_V2_FLAG = featureFlags.register({
id: 'tracksV2',
name: 'Tracks V2',
description: 'Show tracks built on top of the Track V2 API.',
defaultValue: false,
});
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';
export async function decideTracks(
engineId: string, engine: Engine): Promise<DeferredAction[]> {
return (new TrackDecider(engineId, engine)).decideTracks();
}
class TrackDecider {
private engineId: string;
private engine: Engine;
private upidToUuid = new Map<number, string>();
private utidToUuid = new Map<number, string>();
private tracksToAdd: AddTrackArgs[] = [];
private addTrackGroupActions: DeferredAction[] = [];
constructor(engineId: string, engine: Engine) {
this.engineId = engineId;
this.engine = engine;
}
static getTrackName(args: Partial<{
name: string | null,
utid: number,
processName: string|null,
pid: number|null,
threadName: string|null,
tid: number|null,
upid: number|null,
kind: string,
threadTrack: boolean
}>) {
const {
name,
upid,
utid,
processName,
threadName,
pid,
tid,
kind,
threadTrack,
} = args;
const hasName = name !== undefined && name !== null && name !== '[NULL]';
const hasUpid = upid !== undefined && upid !== null;
const hasUtid = utid !== undefined && utid !== null;
const hasProcessName = processName !== undefined && processName !== null;
const hasThreadName = threadName !== undefined && threadName !== null;
const hasTid = tid !== undefined && tid !== null;
const hasPid = pid !== undefined && pid !== null;
const hasKind = kind !== undefined;
const isThreadTrack = threadTrack !== undefined && threadTrack;
// If we don't have any useful information (better than
// upid/utid) we show the track kind to help with tracking
// down where this is coming from.
const kindSuffix = hasKind ? ` (${kind})` : '';
if (isThreadTrack && hasName && hasTid) {
return `${name} (${tid})`;
} else if (hasName) {
return `${name}`;
} else if (hasUpid && hasPid && hasProcessName) {
return `${processName} ${pid}`;
} else if (hasUpid && hasPid) {
return `Process ${pid}`;
} else if (hasThreadName && hasTid) {
return `${threadName} ${tid}`;
} else if (hasTid) {
return `Thread ${tid}`;
} else if (hasUpid) {
return `upid: ${upid}${kindSuffix}`;
} else if (hasUtid) {
return `utid: ${utid}${kindSuffix}`;
} else if (hasKind) {
return `Unnamed ${kind}`;
}
return 'Unknown';
}
addNullTracks(): void {
this.tracksToAdd.push({
engineId: this.engineId,
kind: NULL_TRACK_KIND,
trackKindPriority: TrackKindPriority.ORDINARY,
name: `Null track foo`,
trackGroup: SCROLLING_TRACK_GROUP,
config: {},
});
this.tracksToAdd.push({
engineId: this.engineId,
kind: NULL_TRACK_KIND,
trackKindPriority: TrackKindPriority.ORDINARY,
name: `Null track bar`,
trackGroup: SCROLLING_TRACK_GROUP,
config: {},
});
}
async addCpuSchedulingTracks(): Promise<void> {
const cpus = await this.engine.getCpus();
for (const cpu of cpus) {
this.tracksToAdd.push({
engineId: this.engineId,
kind: CPU_SLICE_TRACK_KIND,
trackKindPriority: TrackKindPriority.ORDINARY,
name: `Cpu ${cpu}`,
trackGroup: SCROLLING_TRACK_GROUP,
config: {
cpu,
},
});
}
}
async addCpuFreqTracks(): Promise<void> {
const cpus = await this.engine.getCpus();
const maxCpuFreqResult = await this.engine.query(`
select ifnull(max(value), 0) as freq
from counter c
inner join cpu_counter_track t on c.track_id = t.id
where name = 'cpufreq';
`);
const maxCpuFreq = maxCpuFreqResult.firstRow({freq: NUM}).freq;
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 this.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) {
const row = cpuFreqIdleResult.firstRow({
cpuFreqId: NUM,
cpuIdleId: NUM_NULL,
});
const freqTrackId = row.cpuFreqId;
const idleTrackId = row.cpuIdleId === null ? undefined : row.cpuIdleId;
this.tracksToAdd.push({
engineId: this.engineId,
kind: CPU_FREQ_TRACK_KIND,
trackKindPriority: TrackKindPriority.ORDINARY,
name: `Cpu ${cpu} Frequency`,
trackGroup: SCROLLING_TRACK_GROUP,
config: {
cpu,
maximumValue: maxCpuFreq,
freqTrackId,
idleTrackId,
},
});
}
}
}
async addGlobalAsyncTracks(): Promise<void> {
const rawGlobalAsyncTracks = await this.engine.query(`
SELECT
t.name as name,
t.track_ids as trackIds,
MAX(experimental_slice_layout.layout_depth) as maxDepth
FROM (
SELECT name, GROUP_CONCAT(track.id) AS track_ids
FROM track
WHERE track.type = "track" or track.type = "gpu_track"
GROUP BY name
) AS t CROSS JOIN experimental_slice_layout
WHERE t.track_ids = experimental_slice_layout.filter_track_ids
GROUP BY t.track_ids
ORDER BY t.name;
`);
const it = rawGlobalAsyncTracks.iter({
name: STR_NULL,
trackIds: STR,
maxDepth: NUM,
});
for (; it.valid(); it.next()) {
const name = it.name === null ? undefined : it.name;
const rawTrackIds = it.trackIds;
const trackIds = rawTrackIds.split(',').map((v) => Number(v));
const maxDepth = it.maxDepth;
const kind = ASYNC_SLICE_TRACK_KIND;
const track = {
engineId: this.engineId,
kind,
trackKindPriority: TrackDecider.inferTrackKindPriority(name),
trackGroup: SCROLLING_TRACK_GROUP,
name: TrackDecider.getTrackName({name, kind}),
config: {
maxDepth,
trackIds,
},
};
this.tracksToAdd.push(track);
}
}
async addGpuFreqTracks(): Promise<void> {
const numGpus = await this.engine.getNumberOfGpus();
const maxGpuFreqResult = await this.engine.query(`
select ifnull(max(value), 0) as maximumValue
from counter c
inner join gpu_counter_track t on c.track_id = t.id
where name = 'gpufreq';
`);
const maximumValue =
maxGpuFreqResult.firstRow({maximumValue: NUM}).maximumValue;
for (let gpu = 0; gpu < numGpus; gpu++) {
// Only add a gpu freq track if we have
// gpu freq data.
const freqExistsResult = await this.engine.query(`
select id
from gpu_counter_track
where name = 'gpufreq' and gpu_id = ${gpu}
limit 1;
`);
if (freqExistsResult.numRows() > 0) {
const trackId = freqExistsResult.firstRow({id: NUM}).id;
this.tracksToAdd.push({
engineId: this.engineId,
kind: COUNTER_TRACK_KIND,
name: `Gpu ${gpu} Frequency`,
trackKindPriority: TrackKindPriority.ORDINARY,
trackGroup: SCROLLING_TRACK_GROUP,
config: {
trackId,
maximumValue,
},
});
}
}
}
async addGlobalCounterTracks(): Promise<void> {
// Add global or GPU counter tracks that are not bound to any pid/tid.
const globalCounters = await this.engine.query(`
select name, id
from (
select name, id
from counter_track
where type = 'counter_track'
union
select name, id
from gpu_counter_track
where name != 'gpufreq'
)
order by name
`);
const it = globalCounters.iter({
name: STR,
id: NUM,
});
for (; it.valid(); it.next()) {
const name = it.name;
const trackId = it.id;
this.tracksToAdd.push({
engineId: this.engineId,
kind: COUNTER_TRACK_KIND,
name,
trackKindPriority: TrackDecider.inferTrackKindPriority(name),
trackGroup: SCROLLING_TRACK_GROUP,
config: {
name,
trackId,
},
});
}
}
async addCpuPerfCounterTracks(): 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 result = await this.engine.query(`
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
`);
const it = result.iter({
name: STR,
id: NUM,
});
for (; it.valid(); it.next()) {
const name = it.name;
const trackId = it.id;
this.tracksToAdd.push({
engineId: this.engineId,
kind: COUNTER_TRACK_KIND,
name,
trackKindPriority: TrackDecider.inferTrackKindPriority(name),
trackGroup: SCROLLING_TRACK_GROUP,
config: {
name,
trackId,
},
});
}
}
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 summaryTrackId = uuidv4();
let foundSummary = false;
for (const track of ionTracks) {
if (!foundSummary &&
[MEM_DMA_COUNTER_NAME, MEM_ION].includes(track.name)) {
foundSummary = true;
track.id = summaryTrackId;
track.trackGroup = undefined;
} else {
track.trackGroup = id;
}
}
const addGroup = Actions.addTrackGroup({
engineId: this.engineId,
summaryTrackId,
name: MEM_DMA_COUNTER_NAME,
id,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
}
async groupGlobalF2fsIostatTracks(tag: string, group: string): Promise<void> {
const f2fsIostatTracks: AddTrackArgs[] = [];
const devMap = new Map<string, string>();
for (const track of this.tracksToAdd) {
if (track.name.startsWith(tag)) {
f2fsIostatTracks.push(track);
}
}
if (f2fsIostatTracks.length === 0) {
return;
}
for (const track of f2fsIostatTracks) {
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 summaryTrackId = uuidv4();
this.tracksToAdd.push({
id: summaryTrackId,
engineId: this.engineId,
kind: NULL_TRACK_KIND,
trackKindPriority: TrackKindPriority.ORDINARY,
name: groupName,
trackGroup: undefined,
config: {},
});
const addGroup = Actions.addTrackGroup({
engineId: this.engineId,
summaryTrackId,
name: groupName,
id: value,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
}
}
async addLogsTrack(): Promise<void> {
const result =
await this.engine.query(`select count(1) as cnt from android_logs`);
const count = result.firstRow({cnt: NUM}).cnt;
if (count > 0) {
this.tracksToAdd.push({
engineId: this.engineId,
kind: ANDROID_LOGS_TRACK_KIND,
name: 'Android logs',
trackKindPriority: TrackKindPriority.ORDINARY,
trackGroup: SCROLLING_TRACK_GROUP,
config: {},
});
}
}
async addAnnotationTracks(): Promise<void> {
const sliceResult = await this.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;
summaryTrackId: 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 summaryTrackId = 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();
summaryTrackId = uuidv4();
groupNameToIds.set(groupName, {
id: trackGroupId,
summaryTrackId,
});
}
}
this.tracksToAdd.push({
id: summaryTrackId,
engineId: this.engineId,
kind: SLICE_TRACK_KIND,
name,
trackKindPriority: TrackDecider.inferTrackKindPriority(name),
trackGroup: trackGroupId,
config: {
maxDepth: 0,
namespace: 'annotation',
trackId: id,
},
});
}
for (const [groupName, groupIds] of groupNameToIds) {
const addGroup = Actions.addTrackGroup({
engineId: this.engineId,
summaryTrackId: groupIds.summaryTrackId,
name: groupName,
id: groupIds.id,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
}
const counterResult = await this.engine.query(`
SELECT
id,
name,
upid,
min_value as minValue,
max_value as maxValue
FROM annotation_counter_track`);
const counterIt = counterResult.iter({
id: NUM,
name: STR,
upid: NUM,
minValue: NUM_NULL,
maxValue: NUM_NULL,
});
for (; counterIt.valid(); counterIt.next()) {
const id = counterIt.id;
const name = counterIt.name;
const upid = counterIt.upid;
const minimumValue =
counterIt.minValue === null ? undefined : counterIt.minValue;
const maximumValue =
counterIt.maxValue === null ? undefined : counterIt.maxValue;
this.tracksToAdd.push({
engineId: this.engineId,
kind: 'CounterTrack',
name,
trackKindPriority: TrackDecider.inferTrackKindPriority(name),
trackGroup: upid === 0 ? SCROLLING_TRACK_GROUP :
this.upidToUuid.get(upid),
config: {
name,
namespace: 'annotation',
trackId: id,
minimumValue,
maximumValue,
},
});
}
}
async addThreadStateTracks(): Promise<void> {
const result = await this.engine.query(`
select
utid,
tid,
upid,
pid,
thread.name as threadName
from
thread_state
left join thread using(utid)
left join process using(upid)
where utid != 0
group by utid`);
const it = result.iter({
utid: NUM,
upid: NUM_NULL,
tid: NUM_NULL,
pid: NUM_NULL,
threadName: STR_NULL,
});
for (; it.valid(); it.next()) {
const utid = it.utid;
const tid = it.tid;
const upid = it.upid;
const pid = it.pid;
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 kind = THREAD_STATE_TRACK_KIND;
this.tracksToAdd.push({
engineId: this.engineId,
kind,
name: TrackDecider.getTrackName({utid, tid, threadName, kind}),
trackGroup: uuid,
trackKindPriority:
TrackDecider.inferTrackKindPriority(threadName, tid, pid),
config: {utid, tid},
});
}
}
async addThreadCpuSampleTracks(): Promise<void> {
const result = await this.engine.query(`
select
utid,
tid,
upid,
thread.name as threadName
from
thread
join (select utid
from cpu_profile_stack_sample group by utid
) using(utid)
left join process using(upid)
where utid != 0
group by 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({
engineId: this.engineId,
kind: CPU_PROFILE_TRACK_KIND,
// TODO(hjd): The threadName can be null, use instead.
trackKindPriority: TrackDecider.inferTrackKindPriority(threadName),
name: `${threadName} (CPU Stack Samples)`,
trackGroup: uuid,
config: {utid},
});
}
}
async addThreadCounterTracks(): Promise<void> {
const result = await this.engine.query(`
select
thread_counter_track.name as trackName,
utid,
upid,
tid,
thread.name as threadName,
thread_counter_track.id as trackId,
thread.start_ts as startTs,
thread.end_ts as endTs
from thread_counter_track
join thread using(utid)
left join process using(upid)
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,
startTs: NUM_NULL,
trackId: NUM,
endTs: NUM_NULL,
});
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 startTs = it.startTs === null ? undefined : it.startTs;
const endTs = it.endTs === null ? undefined : it.endTs;
const kind = COUNTER_TRACK_KIND;
const name = TrackDecider.getTrackName(
{name: trackName, utid, tid, kind, threadName, threadTrack: true});
this.tracksToAdd.push({
engineId: this.engineId,
kind,
name,
trackKindPriority: TrackDecider.inferTrackKindPriority(threadName),
trackGroup: uuid,
config: {name, trackId, startTs, endTs, tid},
});
}
}
async addProcessAsyncSliceTracks(): Promise<void> {
const result = await this.engine.query(`
select
process_track.upid as upid,
process_track.name as trackName,
group_concat(process_track.id) as trackIds,
process.name as processName,
process.pid as pid
from process_track
left join process using(upid)
where
process_track.name is null or
process_track.name not like "% Timeline"
group by
process_track.upid,
process_track.name
`);
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 trackIds = rawTrackIds.split(',').map((v) => Number(v));
const processName = it.processName;
const pid = it.pid;
const uuid = this.getUuid(0, upid);
// TODO(hjd): 1+N queries are bad in the track_decider
const depthResult = await this.engine.query(`
SELECT IFNULL(MAX(layout_depth), 0) as depth
FROM experimental_slice_layout('${rawTrackIds}');
`);
const maxDepth = depthResult.firstRow({depth: NUM}).depth;
const kind = ASYNC_SLICE_TRACK_KIND;
const name = TrackDecider.getTrackName(
{name: trackName, upid, pid, processName, kind});
this.tracksToAdd.push({
engineId: this.engineId,
kind,
name,
trackKindPriority: TrackDecider.inferTrackKindPriority(name),
trackGroup: uuid,
config: {
trackIds,
maxDepth,
},
});
}
}
async addActualFramesTracks(): Promise<void> {
const result = await this.engine.query(`
select
upid,
trackName,
trackIds,
process.name as processName,
process.pid as pid
from (
select
process_track.upid as upid,
process_track.name as trackName,
group_concat(process_track.id) as trackIds
from process_track
where process_track.name like "Actual Timeline"
group by
process_track.upid,
process_track.name
) left join process using(upid)
`);
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 trackIds = rawTrackIds.split(',').map((v) => Number(v));
const processName = it.processName;
const pid = it.pid;
const uuid = this.getUuid(0, upid);
// TODO(hjd): 1+N queries are bad in the track_decider
const depthResult = await this.engine.query(`
SELECT IFNULL(MAX(layout_depth), 0) as depth
FROM experimental_slice_layout('${rawTrackIds}');
`);
const maxDepth = depthResult.firstRow({depth: NUM}).depth;
const kind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
const name = TrackDecider.getTrackName(
{name: trackName, upid, pid, processName, kind});
this.tracksToAdd.push({
engineId: this.engineId,
kind,
name,
trackKindPriority: TrackDecider.inferTrackKindPriority(trackName),
trackGroup: uuid,
config: {
trackIds,
maxDepth,
},
});
}
}
async addExpectedFramesTracks(): Promise<void> {
const result = await this.engine.query(`
select
upid,
trackName,
trackIds,
process.name as processName,
process.pid as pid
from (
select
process_track.upid as upid,
process_track.name as trackName,
group_concat(process_track.id) as trackIds
from process_track
where process_track.name like "Expected Timeline"
group by
process_track.upid,
process_track.name
) left join process using(upid)
`);
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 trackIds = rawTrackIds.split(',').map((v) => Number(v));
const processName = it.processName;
const pid = it.pid;
const uuid = this.getUuid(0, upid);
// TODO(hjd): 1+N queries are bad in the track_decider
const depthResult = await this.engine.query(`
SELECT IFNULL(MAX(layout_depth), 0) as depth
FROM experimental_slice_layout('${rawTrackIds}');
`);
const maxDepth = depthResult.firstRow({depth: NUM}).depth;
const kind = EXPECTED_FRAMES_SLICE_TRACK_KIND;
const name = TrackDecider.getTrackName(
{name: trackName, upid, pid, processName, kind});
this.tracksToAdd.push({
engineId: this.engineId,
kind,
name,
trackKindPriority: TrackDecider.inferTrackKindPriority(trackName),
trackGroup: uuid,
config: {
trackIds,
maxDepth,
},
});
}
}
async addThreadSliceTracks(): Promise<void> {
const result = await this.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,
max(slice.depth) as maxDepth,
(count(thread_slice.id) = count(slice.id)) as onlyThreadSlice,
process.upid as upid,
process.pid as pid
from slice
join thread_track on slice.track_id = thread_track.id
join thread using(utid)
left join process using(upid)
left join thread_slice on slice.id = thread_slice.id
group by thread_track.id
`);
const it = result.iter({
utid: NUM,
trackId: NUM,
trackName: STR_NULL,
isDefaultTrackForScope: NUM_NULL,
tid: NUM_NULL,
threadName: STR_NULL,
maxDepth: NUM,
upid: NUM_NULL,
pid: NUM_NULL,
onlyThreadSlice: NUM,
});
for (; it.valid(); it.next()) {
const utid = it.utid;
const trackId = it.trackId;
const trackName = it.trackName;
// Note that !!null === false.
const isDefaultTrackForScope = !!it.isDefaultTrackForScope;
const tid = it.tid;
const threadName = it.threadName;
const upid = it.upid;
const pid = it.pid;
const maxDepth = it.maxDepth;
const onlyThreadSlice = it.onlyThreadSlice;
const trackKindPriority =
TrackDecider.inferTrackKindPriority(threadName, tid, pid);
const uuid = this.getUuid(utid, upid);
const kind = SLICE_TRACK_KIND;
const name = TrackDecider.getTrackName(
{name: trackName, utid, tid, threadName, kind});
this.tracksToAdd.push({
engineId: this.engineId,
kind,
name,
trackGroup: uuid,
trackKindPriority,
config: {
trackId,
maxDepth,
tid,
isThreadSlice: onlyThreadSlice === 1,
isDefaultTrackForScope,
},
});
if (TRACKS_V2_FLAG.get()) {
this.tracksToAdd.push({
engineId: this.engineId,
kind: 'GenericSliceTrack',
name,
trackGroup: uuid,
trackKindPriority,
config: {sqlTrackId: trackId},
});
}
}
}
async addProcessCounterTracks(): Promise<void> {
const result = await this.engine.query(`
select
process_counter_track.id as trackId,
process_counter_track.name as trackName,
upid,
process.pid,
process.name as processName,
process.start_ts as startTs,
process.end_ts as endTs
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,
startTs: NUM_NULL,
endTs: NUM_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(0, upid);
const startTs = it.startTs === null ? undefined : it.startTs;
const endTs = it.endTs === null ? undefined : it.endTs;
const kind = COUNTER_TRACK_KIND;
const name = TrackDecider.getTrackName(
{name: trackName, upid, pid, kind, processName});
this.tracksToAdd.push({
engineId: this.engineId,
kind,
name,
trackKindPriority: TrackDecider.inferTrackKindPriority(trackName),
trackGroup: uuid,
config: {
name,
trackId,
startTs,
endTs,
},
});
}
}
async addProcessHeapProfileTracks(): Promise<void> {
const result = await this.engine.query(`
select distinct(upid) from heap_profile_allocation
union
select distinct(upid) from heap_graph_object
`);
for (const it = result.iter({upid: NUM}); it.valid(); it.next()) {
const upid = it.upid;
const uuid = this.getUuid(0, upid);
this.tracksToAdd.push({
engineId: this.engineId,
kind: HEAP_PROFILE_TRACK_KIND,
trackKindPriority: TrackKindPriority.ORDINARY,
name: `Heap Profile`,
trackGroup: uuid,
config: {upid},
});
}
}
async addProcessPerfSamplesTracks(): Promise<void> {
const result = await this.engine.query(`
select distinct(process.upid) from process
join thread on process.upid = thread.upid
join perf_sample on thread.utid = perf_sample.utid
`);
for (const it = result.iter({upid: NUM}); it.valid(); it.next()) {
const upid = it.upid;
const uuid = this.getUuid(0, upid);
this.tracksToAdd.push({
engineId: this.engineId,
kind: PERF_SAMPLES_PROFILE_TRACK_KIND,
trackKindPriority: TrackKindPriority.ORDINARY,
name: `Perf Samples`,
trackGroup: uuid,
config: {upid},
});
}
}
getUuidUnchecked(utid: number, upid: number|null) {
return upid === null ? this.utidToUuid.get(utid) :
this.upidToUuid.get(upid);
}
getUuid(utid: number, upid: number|null) {
return assertExists(this.getUuidUnchecked(utid, upid));
}
getOrCreateUuid(utid: number, 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(): 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.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 summaryTrackId = uuidv4();
this.tracksToAdd.push({
id: summaryTrackId,
engineId: this.engineId,
kind: PROCESS_SUMMARY_TRACK,
trackKindPriority: TrackKindPriority.MAIN_THREAD,
name: `Kernel thread summary`,
config: {pidForColor: 2, upid: it.upid, utid: it.utid},
});
const addTrackGroup = Actions.addTrackGroup({
engineId: this.engineId,
summaryTrackId,
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(): 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 this.engine.query(`
select
the_tracks.upid,
the_tracks.utid,
total_dur as hasSched,
hasHeapProfiles,
process.pid as pid,
thread.tid as tid,
process.name as processName,
thread.name as threadName,
process.arg_set_id as argSetId,
(case process.name
when 'Browser' then 3
when 'Gpu' then 2
when 'Renderer' then 1
else 0
end) as chromeProcessRank
from (
select upid, 0 as utid from process_track
union
select upid, 0 as utid from process_counter_track
union
select upid, utid from thread_counter_track join thread using(utid)
union
select upid, utid from thread_track join thread using(utid)
union
select upid, utid from sched join thread using(utid) group by utid
union
select distinct(process.upid), 0 as utid from process
join thread on process.upid = thread.upid
join perf_sample on thread.utid = perf_sample.utid
union
select upid, utid from (
select distinct(utid) from cpu_profile_stack_sample
) join thread using(utid)
union
select distinct(upid) as upid, 0 as utid from heap_profile_allocation
union
select distinct(upid) as upid, 0 as utid from heap_graph_object
) the_tracks
left join (
select upid, sum(thread_total_dur) as total_dur
from (
select utid, sum(dur) as thread_total_dur
from sched where dur != -1 and utid != 0
group by utid
)
join thread using (utid)
group by upid
) using(upid)
left join (
select
distinct(upid) as upid,
true as hasHeapProfiles
from heap_profile_allocation
union
select
distinct(upid) as upid,
true as hasHeapProfiles
from heap_graph_object
) using (upid)
left join (
select
thread.upid as upid,
sum(cnt) as perfSampleCount
from (select utid, count(*) as cnt from perf_sample group by utid) s
join thread on thread.utid = s.utid
group by thread.upid
) using (upid)
left join (
select
process.upid as upid,
sum(cnt) as sliceCount
from (select track_id, count(*) as cnt from slice group by track_id)
left join thread_track on track_id = thread_track.id
left join thread on thread_track.utid = thread.utid
left join process_track on track_id = process_track.id
join process on process.upid = thread.upid
or process_track.upid = process.upid
where process.upid is not null
group by process.upid
) using (upid)
left join thread using(utid)
left join process using(upid)
order by
chromeProcessRank desc,
hasHeapProfiles desc,
perfSampleCount desc,
total_dur desc,
sliceCount desc,
processName asc nulls last,
the_tracks.upid asc nulls last,
threadName asc nulls last,
the_tracks.utid asc nulls last;
`);
const it = result.iter({
utid: NUM,
upid: NUM_NULL,
tid: NUM_NULL,
pid: NUM_NULL,
threadName: STR_NULL,
processName: STR_NULL,
hasSched: NUM_NULL,
hasHeapProfiles: NUM_NULL,
argSetId: NUM_NULL,
});
for (; it.valid(); it.next()) {
const utid = it.utid;
const tid = it.tid;
const upid = it.upid;
const pid = it.pid;
const threadName = it.threadName;
const processName = it.processName;
const hasSched = !!it.hasSched;
const hasHeapProfiles = !!it.hasHeapProfiles;
const labels = [];
if (it.argSetId !== null) {
const result = await this.engine.query(`
select string_value as label
from args
where arg_set_id = ${it.argSetId}
`);
const argIt = result.iter({label: STR_NULL});
for (; argIt.valid(); argIt.next()) {
if (argIt.label !== null) {
labels.push(argIt.label);
}
}
}
// Group by upid if present else by utid.
let pUuid =
upid === null ? this.utidToUuid.get(utid) : this.upidToUuid.get(upid);
// These should only happen once for each track group.
if (pUuid === undefined) {
pUuid = this.getOrCreateUuid(utid, upid);
const summaryTrackId = uuidv4();
const pidForColor = pid || tid || upid || utid || 0;
const kind =
hasSched ? PROCESS_SCHEDULING_TRACK_KIND : PROCESS_SUMMARY_TRACK;
this.tracksToAdd.push({
id: summaryTrackId,
engineId: this.engineId,
kind,
trackKindPriority: TrackDecider.inferTrackKindPriority(threadName),
name: `${upid === null ? tid : pid} summary`,
config: {pidForColor, upid, utid, tid},
labels,
});
const name = TrackDecider.getTrackName(
{utid, processName, pid, threadName, tid, upid});
const addTrackGroup = Actions.addTrackGroup({
engineId: this.engineId,
summaryTrackId,
name,
id: pUuid,
// Perf profiling tracks remain collapsed, otherwise we would have too
// many expanded process tracks for some perf traces, leading to
// jankyness.
collapsed: !hasHeapProfiles,
});
this.addTrackGroupActions.push(addTrackGroup);
}
}
}
async decideTracks(): Promise<DeferredAction[]> {
// Add first the global tracks that don't require per-process track groups.
if (NULL_TRACKS_FLAG.get()) {
await this.addNullTracks();
}
await this.addCpuSchedulingTracks();
await this.addCpuFreqTracks();
await this.addGlobalAsyncTracks();
await this.addGpuFreqTracks();
await this.addGlobalCounterTracks();
await this.addCpuPerfCounterTracks();
await this.addAnnotationTracks();
await this.groupGlobalIonTracks();
await this.groupGlobalF2fsIostatTracks(
F2FS_IOSTAT_TAG, F2FS_IOSTAT_GROUP_NAME);
await this.groupGlobalF2fsIostatTracks(
F2FS_IOSTAT_LAT_TAG, F2FS_IOSTAT_LAT_GROUP_NAME);
// 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.addProcessTrackGroups();
await this.addProcessHeapProfileTracks();
if (PERF_SAMPLE_FLAG.get()) {
await this.addProcessPerfSamplesTracks();
}
await this.addProcessCounterTracks();
await this.addProcessAsyncSliceTracks();
await this.addActualFramesTracks();
await this.addExpectedFramesTracks();
await this.addThreadCounterTracks();
await this.addThreadStateTracks();
await this.addThreadSliceTracks();
await this.addThreadCpuSampleTracks();
await this.addLogsTrack();
this.addTrackGroupActions.push(
Actions.addTracks({tracks: this.tracksToAdd}));
return this.addTrackGroupActions;
}
private static inferTrackKindPriority(
threadName?: string|null, tid?: number|null,
pid?: number|null): TrackKindPriority {
if (pid !== undefined && pid !== null && pid === tid) {
return TrackKindPriority.MAIN_THREAD;
}
if (threadName === undefined || threadName === null) {
return TrackKindPriority.ORDINARY;
}
// Chrome main threads should always come first within their process.
if (threadName === 'CrBrowserMain' || threadName === 'CrRendererMain' ||
threadName === 'CrGpuMain') {
return TrackKindPriority.MAIN_THREAD;
}
// Chrome IO threads should always come immediately after the main thread.
if (threadName === 'Chrome_ChildIOThread' ||
threadName === 'Chrome_IOThread') {
return TrackKindPriority.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 TrackKindPriority.CHROME_COMPOSITOR;
}
switch (true) {
case /.*RenderThread.*/.test(threadName):
return TrackKindPriority.RENDER_THREAD;
case /.*GPU completion.*/.test(threadName):
return TrackKindPriority.GPU_COMPLETION;
default:
return TrackKindPriority.ORDINARY;
}
}
}