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