blob: f028b150abe961751fa0e9439bfd4f6b0cb7dba2 [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 size 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 {Time, time} from '../base/time';
import {raf} from '../core/raf_scheduler';
import {Anchor} from '../widgets/anchor';
import {Button} from '../widgets/button';
import {DetailsShell} from '../widgets/details_shell';
import {GridLayout} from '../widgets/grid_layout';
import {Section} from '../widgets/section';
import {SqlRef} from '../widgets/sql_ref';
import {Tree, TreeNode} from '../widgets/tree';
import {Intent} from '../widgets/common';
import {BottomTab, NewBottomTabArgs} from './bottom_tab';
import {SchedSqlId, ThreadStateSqlId} from './sql_types';
import {
getFullThreadName,
getProcessName,
getThreadName,
ThreadInfo,
} from './thread_and_process_info';
import {
getThreadState,
getThreadStateFromConstraints,
goToSchedSlice,
ThreadState,
ThreadStateRef,
} from './thread_state';
import {DurationWidget, renderDuration} from './widgets/duration';
import {Timestamp} from './widgets/timestamp';
import {addDebugSliceTrack} from './debug_tracks';
interface ThreadStateTabConfig {
// Id into |thread_state| sql table.
readonly id: ThreadStateSqlId;
}
interface RelatedThreadStates {
prev?: ThreadState;
next?: ThreadState;
waker?: ThreadState;
wakee?: ThreadState[];
}
export class ThreadStateTab extends BottomTab<ThreadStateTabConfig> {
static readonly kind = 'dev.perfetto.ThreadStateTab';
state?: ThreadState;
relatedStates?: RelatedThreadStates;
loaded: boolean = false;
static create(args: NewBottomTabArgs<ThreadStateTabConfig>): ThreadStateTab {
return new ThreadStateTab(args);
}
constructor(args: NewBottomTabArgs<ThreadStateTabConfig>) {
super(args);
this.load().then(() => {
this.loaded = true;
raf.scheduleFullRedraw();
});
}
async load() {
this.state = await getThreadState(this.engine, this.config.id);
if (!this.state) {
return;
}
const relatedStates: RelatedThreadStates = {};
relatedStates.prev = (
await getThreadStateFromConstraints(this.engine, {
filters: [
`ts + dur = ${this.state.ts}`,
`utid = ${this.state.thread?.utid}`,
],
limit: 1,
})
)[0];
relatedStates.next = (
await getThreadStateFromConstraints(this.engine, {
filters: [
`ts = ${this.state.ts + this.state.dur}`,
`utid = ${this.state.thread?.utid}`,
],
limit: 1,
})
)[0];
if (this.state.wakerThread?.utid !== undefined) {
relatedStates.waker = (
await getThreadStateFromConstraints(this.engine, {
filters: [
`utid = ${this.state.wakerThread?.utid}`,
`ts <= ${this.state.ts}`,
`ts + dur >= ${this.state.ts}`,
],
})
)[0];
}
relatedStates.wakee = await getThreadStateFromConstraints(this.engine, {
filters: [
`waker_utid = ${this.state.thread?.utid}`,
`state = 'R'`,
`ts >= ${this.state.ts}`,
`ts <= ${this.state.ts + this.state.dur}`,
],
});
this.relatedStates = relatedStates;
}
getTitle() {
// TODO(altimin): Support dynamic titles here.
return 'Current Selection';
}
viewTab() {
// TODO(altimin/stevegolton): Differentiate between "Current Selection" and
// "Pinned" views in DetailsShell.
return m(
DetailsShell,
{title: 'Thread State', description: this.renderLoadingText()},
m(
GridLayout,
m(
Section,
{title: 'Details'},
this.state && this.renderTree(this.state),
),
m(
Section,
{title: 'Related thread states'},
this.renderRelatedThreadStates(),
),
),
);
}
private renderLoadingText() {
if (!this.loaded) {
return 'Loading';
}
if (!this.state) {
return `Thread state ${this.config.id} does not exist`;
}
// TODO(stevegolton): Return something intelligent here.
return this.config.id;
}
private renderTree(state: ThreadState) {
const thread = state.thread;
const process = state.thread?.process;
return m(
Tree,
m(TreeNode, {
left: 'Start time',
right: m(Timestamp, {ts: state.ts}),
}),
m(TreeNode, {
left: 'Duration',
right: m(DurationWidget, {dur: state.dur}),
}),
m(TreeNode, {
left: 'State',
right: this.renderState(
state.state,
state.cpu,
state.schedSqlId,
state.ts,
),
}),
state.blockedFunction &&
m(TreeNode, {
left: 'Blocked function',
right: state.blockedFunction,
}),
process &&
m(TreeNode, {
left: 'Process',
right: getProcessName(process),
}),
thread && m(TreeNode, {left: 'Thread', right: getThreadName(thread)}),
state.wakerThread && this.renderWakerThread(state.wakerThread),
m(TreeNode, {
left: 'SQL ID',
right: m(SqlRef, {table: 'thread_state', id: state.threadStateSqlId}),
}),
);
}
private renderState(
state: string,
cpu: number | undefined,
id: SchedSqlId | undefined,
ts: time,
): m.Children {
if (!state) {
return null;
}
if (id === undefined || cpu === undefined) {
return state;
}
return m(
Anchor,
{
title: 'Go to CPU slice',
icon: 'call_made',
onclick: () => goToSchedSlice(cpu, id, ts),
},
`${state} on CPU ${cpu}`,
);
}
private renderWakerThread(wakerThread: ThreadInfo) {
return m(
TreeNode,
{left: 'Waker'},
m(TreeNode, {
left: 'Process',
right: getProcessName(wakerThread.process),
}),
m(TreeNode, {left: 'Thread', right: getThreadName(wakerThread)}),
);
}
private renderRelatedThreadStates(): m.Children {
if (this.state === undefined || this.relatedStates === undefined) {
return 'Loading';
}
const startTs = this.state.ts;
const renderRef = (state: ThreadState, name?: string) =>
m(ThreadStateRef, {
id: state.threadStateSqlId,
ts: state.ts,
dur: state.dur,
utid: state.thread!.utid,
name,
});
const sliceColumns = {ts: 'ts', dur: 'dur', name: 'name'};
const sliceColumnNames = ['id', 'utid', 'ts', 'dur', 'name', 'table_name'];
const sliceLiteColumns = {ts: 'ts', dur: 'dur', name: 'thread_name'};
const sliceLiteColumnNames = [
'id',
'utid',
'ts',
'dur',
'thread_name',
'process_name',
'table_name',
];
const nameForNextOrPrev = (state: ThreadState) =>
`${state.state} for ${renderDuration(state.dur)}`;
return [
m(
Tree,
this.relatedStates.waker &&
m(TreeNode, {
left: 'Waker',
right: renderRef(
this.relatedStates.waker,
getFullThreadName(this.relatedStates.waker.thread),
),
}),
this.relatedStates.prev &&
m(TreeNode, {
left: 'Previous state',
right: renderRef(
this.relatedStates.prev,
nameForNextOrPrev(this.relatedStates.prev),
),
}),
this.relatedStates.next &&
m(TreeNode, {
left: 'Next state',
right: renderRef(
this.relatedStates.next,
nameForNextOrPrev(this.relatedStates.next),
),
}),
this.relatedStates.wakee &&
this.relatedStates.wakee.length > 0 &&
m(
TreeNode,
{
left: 'Woken threads',
},
this.relatedStates.wakee.map((state) =>
m(TreeNode, {
left: m(Timestamp, {
ts: state.ts,
display: [
'Start+',
m(DurationWidget, {dur: Time.sub(state.ts, startTs)}),
],
}),
right: renderRef(state, getFullThreadName(state.thread)),
}),
),
),
),
m(Button, {
label: 'Critical path lite',
intent: Intent.Primary,
onclick: () =>
this.engine
.query(`INCLUDE PERFETTO MODULE sched.thread_executing_span;`)
.then(() =>
addDebugSliceTrack(
this.engine,
{
sqlSource: `
SELECT
cr.id,
cr.utid,
cr.ts,
cr.dur,
thread.name AS thread_name,
process.name AS process_name,
'thread_state' AS table_name
FROM
_thread_executing_span_critical_path(
${this.state?.thread?.utid},
trace_bounds.start_ts,
trace_bounds.end_ts - trace_bounds.start_ts) cr,
trace_bounds
JOIN thread USING(utid)
JOIN process USING(upid)
`,
columns: sliceLiteColumnNames,
},
`${this.state?.thread?.name}`,
sliceLiteColumns,
sliceLiteColumnNames,
),
),
}),
m(Button, {
label: 'Critical path',
intent: Intent.Primary,
onclick: () =>
this.engine
.query(
`INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
)
.then(() =>
addDebugSliceTrack(
this.engine,
{
sqlSource: `
SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
FROM
_thread_executing_span_critical_path_stack(
${this.state?.thread?.utid},
trace_bounds.start_ts,
trace_bounds.end_ts - trace_bounds.start_ts) cr,
trace_bounds WHERE name IS NOT NULL
`,
columns: sliceColumnNames,
},
`${this.state?.thread?.name}`,
sliceColumns,
sliceColumnNames,
),
),
}),
];
}
isLoading() {
return this.state === undefined || this.relatedStates === undefined;
}
}