| // 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; |
| } |
| } |