blob: 659c48ccf5a61d76f9cb45ecb21c949c40705db1 [file] [log] [blame]
// Copyright (C) 2023 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 m from 'mithril';
import {duration, Time, time} from '../../base/time';
import {raf} from '../../core/raf_scheduler';
import {BottomTab, NewBottomTabArgs} from '../../frontend/bottom_tab';
import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
import {hasArgs, renderArguments} from '../../frontend/slice_args';
import {getSlice, SliceDetails, sliceRef} from '../../frontend/sql/slice';
import {asSliceSqlId, Utid} from '../../frontend/sql_types';
import {sqlValueToString} from '../../frontend/sql_utils';
import {
getProcessName,
getThreadName,
} from '../../frontend/thread_and_process_info';
import {
getThreadState,
ThreadState,
threadStateRef,
} from '../../frontend/thread_state';
import {DurationWidget} from '../../frontend/widgets/duration';
import {Timestamp} from '../../frontend/widgets/timestamp';
import {
ColumnType,
durationFromSql,
LONG,
STR,
timeFromSql,
} from '../../trace_processor/query_result';
import {DetailsShell} from '../../widgets/details_shell';
import {GridLayout} from '../../widgets/grid_layout';
import {Section} from '../../widgets/section';
import {dictToTree, dictToTreeNodes, Tree, TreeNode} from '../../widgets/tree';
import {ARG_PREFIX} from '../../frontend/debug_tracks';
function sqlValueToNumber(value?: ColumnType): number | undefined {
if (typeof value === 'bigint') return Number(value);
if (typeof value !== 'number') return undefined;
return value;
}
function sqlValueToUtid(value?: ColumnType): Utid | undefined {
if (typeof value === 'bigint') return Number(value) as Utid;
if (typeof value !== 'number') return undefined;
return value as Utid;
}
function renderTreeContents(dict: {[key: string]: m.Child}): m.Child[] {
const children: m.Child[] = [];
for (const key of Object.keys(dict)) {
if (dict[key] === null || dict[key] === undefined) continue;
children.push(
m(TreeNode, {
left: key,
right: dict[key],
}),
);
}
return children;
}
export class DebugSliceDetailsTab extends BottomTab<GenericSliceDetailsTabConfig> {
static readonly kind = 'dev.perfetto.DebugSliceDetailsTab';
data?: {
name: string;
ts: time;
dur: duration;
args: {[key: string]: ColumnType};
};
// We will try to interpret the arguments as references into well-known
// tables. These values will be set if the relevant columns exist and
// are consistent (e.g. 'ts' and 'dur' for this slice correspond to values
// in these well-known tables).
threadState?: ThreadState;
slice?: SliceDetails;
static create(
args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
): DebugSliceDetailsTab {
return new DebugSliceDetailsTab(args);
}
private async maybeLoadThreadState(
id: number | undefined,
ts: time,
dur: duration,
table: string | undefined,
utid?: Utid,
): Promise<ThreadState | undefined> {
if (id === undefined) return undefined;
if (utid === undefined) return undefined;
const threadState = await getThreadState(this.engine, id);
if (threadState === undefined) return undefined;
if (
table === 'thread_state' ||
(threadState.ts === ts &&
threadState.dur === dur &&
threadState.thread?.utid === utid)
) {
return threadState;
} else {
return undefined;
}
}
private renderThreadStateInfo(): m.Child {
if (this.threadState === undefined) return null;
return m(
TreeNode,
{
left: threadStateRef(this.threadState),
right: '',
},
renderTreeContents({
Thread: getThreadName(this.threadState.thread),
Process: getProcessName(this.threadState.thread?.process),
State: this.threadState.state,
}),
);
}
private async maybeLoadSlice(
id: number | undefined,
ts: time,
dur: duration,
table: string | undefined,
trackId?: number,
): Promise<SliceDetails | undefined> {
if (id === undefined) return undefined;
if (table !== 'slice' && trackId === undefined) return undefined;
const slice = await getSlice(this.engine, asSliceSqlId(id));
if (slice === undefined) return undefined;
if (
table === 'slice' ||
(slice.ts === ts && slice.dur === dur && slice.trackId === trackId)
) {
return slice;
} else {
return undefined;
}
}
private renderSliceInfo(): m.Child {
if (this.slice === undefined) return null;
return m(
TreeNode,
{
left: sliceRef(this.slice, 'Slice'),
right: '',
},
m(TreeNode, {
left: 'Name',
right: this.slice.name,
}),
m(TreeNode, {
left: 'Thread',
right: getThreadName(this.slice.thread),
}),
m(TreeNode, {
left: 'Process',
right: getProcessName(this.slice.process),
}),
hasArgs(this.slice.args) &&
m(
TreeNode,
{
left: 'Args',
},
renderArguments(this.engine, this.slice.args),
),
);
}
private async loadData() {
const queryResult = await this.engine.query(
`select * from ${this.config.sqlTableName} where id = ${this.config.id}`,
);
const row = queryResult.firstRow({
ts: LONG,
dur: LONG,
name: STR,
});
this.data = {
name: row.name,
ts: Time.fromRaw(row.ts),
dur: row.dur,
args: {},
};
for (const key of Object.keys(row)) {
if (key.startsWith(ARG_PREFIX)) {
this.data.args[key.substr(ARG_PREFIX.length)] = (
row as {[key: string]: ColumnType}
)[key];
}
}
this.threadState = await this.maybeLoadThreadState(
sqlValueToNumber(this.data.args['id']),
this.data.ts,
this.data.dur,
sqlValueToString(this.data.args['table_name']),
sqlValueToUtid(this.data.args['utid']),
);
this.slice = await this.maybeLoadSlice(
sqlValueToNumber(this.data.args['id']) ??
sqlValueToNumber(this.data.args['slice_id']),
this.data.ts,
this.data.dur,
sqlValueToString(this.data.args['table_name']),
sqlValueToNumber(this.data.args['track_id']),
);
raf.scheduleRedraw();
}
constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
super(args);
this.loadData();
}
viewTab() {
if (this.data === undefined) {
return m('h2', 'Loading');
}
const details = dictToTreeNodes({
'Name': this.data['name'] as string,
'Start time': m(Timestamp, {ts: timeFromSql(this.data['ts'])}),
'Duration': m(DurationWidget, {dur: durationFromSql(this.data['dur'])}),
'Debug slice id': `${this.config.sqlTableName}[${this.config.id}]`,
});
details.push(this.renderThreadStateInfo());
details.push(this.renderSliceInfo());
const args: {[key: string]: m.Child} = {};
for (const key of Object.keys(this.data.args)) {
args[key] = sqlValueToString(this.data.args[key]);
}
return m(
DetailsShell,
{
title: 'Debug Slice',
},
m(
GridLayout,
m(Section, {title: 'Details'}, m(Tree, details)),
m(Section, {title: 'Arguments'}, dictToTree(args)),
),
);
}
getTitle(): string {
return `Current Selection`;
}
isLoading() {
return this.data === undefined;
}
}