blob: 0a58c43e97e96f60d77b95a395ef7657e48659bd [file] [log] [blame]
// Copyright (C) 2019 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 {assertTrue} from '../base/logging';
import {Time, time} from '../base/time';
import {Args, ArgValue} from '../common/arg_types';
import {ChromeSliceSelection, getLegacySelection} from '../common/state';
import {
CounterDetails,
globals,
SliceDetails,
ThreadStateDetails,
} from '../frontend/globals';
import {
publishCounterDetails,
publishSliceDetails,
publishThreadStateDetails,
} from '../frontend/publish';
import {Engine} from '../trace_processor/engine';
import {
durationFromSql,
LONG,
NUM,
NUM_NULL,
STR,
STR_NULL,
timeFromSql,
} from '../trace_processor/query_result';
import {SLICE_TRACK_KIND} from '../core_plugins/chrome_slices/chrome_slice_track';
import {Controller} from './controller';
export interface SelectionControllerArgs {
engine: Engine;
}
interface ThreadDetails {
tid: number;
threadName?: string;
}
interface ProcessDetails {
pid?: number;
processName?: string;
uid?: number;
packageName?: string;
versionCode?: number;
}
// This class queries the TP for the details on a specific slice that has
// been clicked.
export class SelectionController extends Controller<'main'> {
private lastSelectedId?: number | string;
private lastSelectedKind?: string;
constructor(private args: SelectionControllerArgs) {
super('main');
}
run() {
const selection = getLegacySelection(globals.state);
if (!selection || selection.kind === 'AREA') return;
const selectWithId = [
'SLICE',
'COUNTER',
'CHROME_SLICE',
'HEAP_PROFILE',
'THREAD_STATE',
];
if (
!selectWithId.includes(selection.kind) ||
(selectWithId.includes(selection.kind) &&
selection.id === this.lastSelectedId &&
selection.kind === this.lastSelectedKind)
) {
return;
}
const selectedId = selection.id;
const selectedKind = selection.kind;
this.lastSelectedId = selectedId;
this.lastSelectedKind = selectedKind;
if (selectedId === undefined) return;
if (selection.kind === 'COUNTER') {
this.counterDetails(
selection.leftTs,
selection.rightTs,
selection.id,
).then((results) => {
if (
results !== undefined &&
selection.kind === selectedKind &&
selection.id === selectedId
) {
publishCounterDetails(results);
}
});
} else if (selection.kind === 'SLICE') {
this.sliceDetails(selectedId as number);
} else if (selection.kind === 'THREAD_STATE') {
this.threadStateDetails(selection.id);
} else if (selection.kind === 'CHROME_SLICE') {
this.chromeSliceDetails(selection);
}
}
async chromeSliceDetails(selection: ChromeSliceSelection) {
const selectedId = selection.id;
const table = selection.table;
let leafTable: string;
let promisedArgs: Promise<Args>;
// TODO(b/155483804): This is a hack to ensure annotation slices are
// selectable for now. We should tidy this up when improving this class.
if (table === 'annotation') {
leafTable = 'annotation_slice';
promisedArgs = Promise.resolve(new Map());
} else {
const result = await this.args.engine.query(`
SELECT
type as leafTable,
arg_set_id as argSetId
FROM slice WHERE id = ${selectedId}`);
if (result.numRows() === 0) {
return;
}
const row = result.firstRow({
leafTable: STR,
argSetId: NUM,
});
leafTable = row.leafTable;
const argSetId = row.argSetId;
promisedArgs = this.getArgs(argSetId);
}
const promisedDetails = this.args.engine.query(`
SELECT *, ABS_TIME_STR(ts) as absTime FROM ${leafTable} WHERE id = ${selectedId};
`);
const [details, args] = await Promise.all([promisedDetails, promisedArgs]);
if (details.numRows() <= 0) return;
const rowIter = details.iter({});
assertTrue(rowIter.valid());
// A few columns are hard coded as part of the SliceDetails interface.
// Long term these should be handled generically as args but for now
// handle them specially:
let ts = undefined;
let absTime = undefined;
let dur = undefined;
let name = undefined;
let category = undefined;
let threadDur = undefined;
let threadTs = undefined;
let trackId = undefined;
// We select all columns from the leafTable to ensure that we include
// additional fields from the child tables (like `thread_dur` from
// `thread_slice` or `frame_number` from `frame_slice`).
// However, this also includes some basic columns (especially from `slice`)
// that are not interesting (i.e. `arg_set_id`, which has already been used
// to resolve and show the arguments) and should not be shown to the user.
const ignoredColumns = [
'type',
'depth',
'parent_id',
'stack_id',
'parent_stack_id',
'arg_set_id',
'thread_instruction_count',
'thread_instruction_delta',
];
for (const k of details.columns()) {
const v = rowIter.get(k);
switch (k) {
case 'id':
break;
case 'ts':
ts = timeFromSql(v);
break;
case 'thread_ts':
threadTs = timeFromSql(v);
break;
case 'absTime':
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
if (v) absTime = `${v}`;
/* eslint-enable */
break;
case 'name':
name = `${v}`;
break;
case 'dur':
dur = durationFromSql(v);
break;
case 'thread_dur':
threadDur = durationFromSql(v);
break;
case 'category':
case 'cat':
category = `${v}`;
break;
case 'track_id':
trackId = Number(v);
break;
default:
if (!ignoredColumns.includes(k)) args.set(k, `${v}`);
}
}
const selected: SliceDetails = {
id: selectedId,
ts,
threadTs,
absTime,
dur,
threadDur,
name,
category,
args,
};
if (trackId !== undefined) {
const columnInfo = (
await this.args.engine.query(`
WITH
leafTrackTable AS (SELECT type FROM track WHERE id = ${trackId}),
cols AS (
SELECT name
FROM pragma_table_info((SELECT type FROM leafTrackTable))
)
SELECT
type as leafTrackTable,
'upid' in cols AS hasUpid,
'utid' in cols AS hasUtid
FROM leafTrackTable
`)
).firstRow({hasUpid: NUM, hasUtid: NUM, leafTrackTable: STR});
const hasUpid = columnInfo.hasUpid !== 0;
const hasUtid = columnInfo.hasUtid !== 0;
if (hasUtid) {
const utid = (
await this.args.engine.query(`
SELECT utid
FROM ${columnInfo.leafTrackTable}
WHERE id = ${trackId};
`)
).firstRow({
utid: NUM,
}).utid;
Object.assign(selected, await this.computeThreadDetails(utid));
} else if (hasUpid) {
const upid = (
await this.args.engine.query(`
SELECT upid
FROM ${columnInfo.leafTrackTable}
WHERE id = ${trackId};
`)
).firstRow({
upid: NUM,
}).upid;
Object.assign(selected, await this.computeProcessDetails(upid));
}
}
// Check selection is still the same on completion of query.
if (selection === getLegacySelection(globals.state)) {
publishSliceDetails(selected);
}
}
async getArgs(argId: number): Promise<Args> {
const args = new Map<string, ArgValue>();
const query = `
select
key AS name,
display_value AS value
FROM args
WHERE arg_set_id = ${argId}
`;
const result = await this.args.engine.query(query);
const it = result.iter({
name: STR,
value: STR_NULL,
});
for (; it.valid(); it.next()) {
const name = it.name;
const value = it.value || 'NULL';
if (name === 'destination slice id' && !isNaN(Number(value))) {
const destTrackId = await this.getDestTrackId(value);
args.set('Destination Slice', {
kind: 'SLICE',
trackId: destTrackId,
sliceId: Number(value),
rawValue: value,
});
} else {
args.set(name, value);
}
}
return args;
}
async getDestTrackId(sliceId: string): Promise<string> {
const trackIdQuery = `select track_id as trackId from slice
where slice_id = ${sliceId}`;
const result = await this.args.engine.query(trackIdQuery);
const trackId = result.firstRow({trackId: NUM}).trackId;
// TODO(hjd): If we had a consistent mapping from TP track_id
// UI track id for slice tracks this would be unnecessary.
let trackKey = '';
for (const track of Object.values(globals.state.tracks)) {
const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
if (trackInfo?.kind === SLICE_TRACK_KIND) {
const trackIds = trackInfo?.trackIds;
if (trackIds && trackIds.length > 0 && trackIds[0] === trackId) {
trackKey = track.key;
break;
}
}
}
return trackKey;
}
// TODO(altimin): We currently rely on the ThreadStateDetails for supporting
// marking the area (the rest goes is handled by ThreadStateTab
// directly. Refactor it to be plugin-friendly and remove this.
async threadStateDetails(id: number) {
const query = `
SELECT
ts,
thread_state.dur as dur
from thread_state
where thread_state.id = ${id}
`;
const result = await this.args.engine.query(query);
const selection = getLegacySelection(globals.state);
if (result.numRows() > 0 && selection) {
const row = result.firstRow({
ts: LONG,
dur: LONG,
});
const selected: ThreadStateDetails = {
ts: Time.fromRaw(row.ts),
dur: row.dur,
};
publishThreadStateDetails(selected);
}
}
async sliceDetails(id: number) {
const sqlQuery = `SELECT
sched.ts,
sched.dur,
sched.priority,
sched.end_state as endState,
sched.utid,
sched.cpu,
thread_state.id as threadStateId
FROM sched left join thread_state using(ts, utid, cpu)
WHERE sched.id = ${id}`;
const result = await this.args.engine.query(sqlQuery);
// Check selection is still the same on completion of query.
const selection = getLegacySelection(globals.state);
if (result.numRows() > 0 && selection) {
const row = result.firstRow({
ts: LONG,
dur: LONG,
priority: NUM,
endState: STR_NULL,
utid: NUM,
cpu: NUM,
threadStateId: NUM_NULL,
});
const ts = Time.fromRaw(row.ts);
const dur = row.dur;
const priority = row.priority;
const endState = row.endState;
const utid = row.utid;
const cpu = row.cpu;
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
const threadStateId = row.threadStateId || undefined;
const selected: SliceDetails = {
ts,
dur,
priority,
endState,
cpu,
id,
utid,
threadStateId,
};
Object.assign(selected, await this.computeThreadDetails(utid));
this.schedulingDetails(ts, utid)
.then((wakeResult) => {
Object.assign(selected, wakeResult);
})
.finally(() => {
publishSliceDetails(selected);
});
}
}
async counterDetails(
ts: time,
rightTs: time,
id: number,
): Promise<CounterDetails> {
const counter = await this.args.engine.query(
`SELECT value, track_id as trackId FROM counter WHERE id = ${id}`,
);
const row = counter.iter({
value: NUM,
trackId: NUM,
});
const value = row.value;
const trackId = row.trackId;
// Finding previous value. If there isn't previous one, it will return 0 for
// ts and value.
const previous = await this.args.engine.query(`SELECT
MAX(ts),
IFNULL(value, 0) as value
FROM counter WHERE ts < ${ts} and track_id = ${trackId}`);
const previousValue = previous.firstRow({value: NUM}).value;
const endTs = rightTs !== -1n ? rightTs : globals.traceContext.end;
const delta = value - previousValue;
const duration = endTs - ts;
const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
const name = trackKey ? globals.state.tracks[trackKey].name : undefined;
return {startTime: ts, value, delta, duration, name};
}
async schedulingDetails(ts: time, utid: number) {
// Find the ts of the first wakeup before the current slice.
const wakeResult = await this.args.engine.query(`
select ts, waker_utid as wakerUtid
from thread_state
where utid = ${utid} and ts < ${ts} and state = 'R'
order by ts desc
limit 1
`);
if (wakeResult.numRows() === 0) {
return undefined;
}
const wakeFirstRow = wakeResult.firstRow({ts: LONG, wakerUtid: NUM_NULL});
const wakeupTs = wakeFirstRow.ts;
const wakerUtid = wakeFirstRow.wakerUtid;
if (wakerUtid === null) {
return undefined;
}
// Find the previous sched slice for the current utid.
const prevSchedResult = await this.args.engine.query(`
select ts
from sched
where utid = ${utid} and ts < ${ts}
order by ts desc
limit 1
`);
// If this is the first sched slice for this utid or if the wakeup found
// was after the previous slice then we know the wakeup was for this slice.
if (
prevSchedResult.numRows() !== 0 &&
wakeupTs < prevSchedResult.firstRow({ts: LONG}).ts
) {
return undefined;
}
// Find the sched slice with the utid of the waker running when the
// sched wakeup occurred. This is the waker.
const wakerResult = await this.args.engine.query(`
select cpu
from sched
where
utid = ${wakerUtid} and
ts < ${wakeupTs} and
ts + dur >= ${wakeupTs};
`);
if (wakerResult.numRows() === 0) {
return undefined;
}
const wakerRow = wakerResult.firstRow({cpu: NUM});
return {wakeupTs, wakerUtid, wakerCpu: wakerRow.cpu};
}
async computeThreadDetails(
utid: number,
): Promise<ThreadDetails & ProcessDetails> {
const threadInfo = (
await this.args.engine.query(`
SELECT tid, name, upid
FROM thread
WHERE utid = ${utid};
`)
).firstRow({tid: NUM, name: STR_NULL, upid: NUM_NULL});
const threadDetails = {
tid: threadInfo.tid,
threadName: threadInfo.name || undefined,
};
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (threadInfo.upid) {
return Object.assign(
{},
threadDetails,
await this.computeProcessDetails(threadInfo.upid),
);
}
return threadDetails;
}
async computeProcessDetails(upid: number): Promise<ProcessDetails> {
const details: ProcessDetails = {};
const processResult = (
await this.args.engine.query(`
SELECT pid, name, uid FROM process WHERE upid = ${upid};
`)
).firstRow({pid: NUM, name: STR_NULL, uid: NUM_NULL});
details.pid = processResult.pid;
details.processName = processResult.name || undefined;
if (processResult.uid === null) {
return details;
}
details.uid = processResult.uid;
const packageResult = await this.args.engine.query(`
SELECT
package_name as packageName,
version_code as versionCode
FROM package_list WHERE uid = ${details.uid};
`);
// The package_list table is not populated in some traces so we need to
// check if the result has returned any rows.
if (packageResult.numRows() > 0) {
const packageDetails = packageResult.firstRow({
packageName: STR,
versionCode: NUM,
});
details.packageName = packageDetails.packageName;
details.versionCode = packageDetails.versionCode;
}
return details;
}
}