blob: 66cf3d0056608813fbf63b9c4e757c6b21323775 [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 {Arg, Args} from '../common/arg_types';
import {Engine} from '../common/engine';
import {
NUM,
NUM_NULL,
STR,
STR_NULL,
} from '../common/query_result';
import {ChromeSliceSelection} from '../common/state';
import {fromNs, toNs} from '../common/time';
import {SliceDetails, ThreadStateDetails} from '../frontend/globals';
import {globals} from '../frontend/globals';
import {
publishCounterDetails,
publishSliceDetails,
publishThreadStateDetails,
} from '../frontend/publish';
import {SLICE_TRACK_KIND} from '../tracks/chrome_slices';
import {parseArgs} from './args_parser';
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 = globals.state.currentSelection;
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 &&
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 = fromNs(Number(v)) - globals.state.traceTime.startSec;
break;
case 'thread_ts':
threadTs = fromNs(Number(v));
break;
case 'absTime':
if (v) absTime = `${v}`;
break;
case 'name':
name = `${v}`;
break;
case 'dur':
dur = fromNs(Number(v));
break;
case 'thread_dur':
threadDur = fromNs(Number(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 argsTree = parseArgs(args);
const selected: SliceDetails = {
id: selectedId,
ts,
threadTs,
absTime,
dur,
threadDur,
name,
category,
args,
argsTree,
};
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 === globals.state.currentSelection) {
publishSliceDetails(selected);
}
}
async getArgs(argId: number): Promise<Args> {
const args = new Map<string, Arg>();
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)});
} 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 trackIdTp = 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 trackId = '';
for (const track of Object.values(globals.state.tracks)) {
if (track.kind === SLICE_TRACK_KIND &&
(track.config as {trackId: number}).trackId === Number(trackIdTp)) {
trackId = track.id;
break;
}
}
return trackId;
}
// 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 = globals.state.currentSelection;
if (result.numRows() > 0 && selection) {
const row = result.firstRow({
ts: NUM,
dur: NUM,
});
const ts = row.ts;
const timeFromStart = fromNs(ts) - globals.state.traceTime.startSec;
const dur = fromNs(row.dur);
const selected: ThreadStateDetails = {ts: timeFromStart, 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 = globals.state.currentSelection;
if (result.numRows() > 0 && selection) {
const row = result.firstRow({
ts: NUM,
dur: NUM,
priority: NUM,
endState: STR_NULL,
utid: NUM,
cpu: NUM,
threadStateId: NUM_NULL,
});
const ts = row.ts;
const timeFromStart = fromNs(ts) - globals.state.traceTime.startSec;
const dur = fromNs(row.dur);
const priority = row.priority;
const endState = row.endState;
const utid = row.utid;
const cpu = row.cpu;
const threadStateId = row.threadStateId || undefined;
const selected: SliceDetails = {
ts: timeFromStart,
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: number, rightTs: number, id: number) {
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 !== -1 ? rightTs : toNs(globals.state.traceTime.endSec);
const delta = value - previousValue;
const duration = endTs - ts;
const startTime = fromNs(ts) - globals.state.traceTime.startSec;
const uiTrackId = globals.state.uiTrackIdByTraceTrackId[trackId];
const name = uiTrackId ? globals.state.tracks[uiTrackId].name : undefined;
return {startTime, value, delta, duration, name};
}
async schedulingDetails(ts: number, utid: number|Long) {
// 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: NUM, 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: NUM}).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: fromNs(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,
};
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;
}
}