1// Copyright (C) 2023 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import m from 'mithril'; 16 17import {duration, Time, time} from '../../base/time'; 18import {raf} from '../../core/raf_scheduler'; 19import {BottomTab, NewBottomTabArgs} from '../bottom_tab'; 20import {GenericSliceDetailsTabConfig} from '../generic_slice_details_tab'; 21import {hasArgs, renderArguments} from '../slice_args'; 22import {getSlice, SliceDetails, sliceRef} from '../sql/slice'; 23import {asSliceSqlId, Utid} from '../sql_types'; 24import {getProcessName, getThreadName} from '../thread_and_process_info'; 25import {getThreadState, ThreadState, threadStateRef} from '../thread_state'; 26import {DurationWidget} from '../widgets/duration'; 27import {Timestamp} from '../widgets/timestamp'; 28import { 29 ColumnType, 30 durationFromSql, 31 LONG, 32 STR, 33 timeFromSql, 34} from '../../trace_processor/query_result'; 35import {sqlValueToReadableString} from '../../trace_processor/sql_utils'; 36import {DetailsShell} from '../../widgets/details_shell'; 37import {GridLayout} from '../../widgets/grid_layout'; 38import {Section} from '../../widgets/section'; 39import {dictToTree, dictToTreeNodes, Tree, TreeNode} from '../../widgets/tree'; 40 41export const ARG_PREFIX = 'arg_'; 42 43function sqlValueToNumber(value?: ColumnType): number | undefined { 44 if (typeof value === 'bigint') return Number(value); 45 if (typeof value !== 'number') return undefined; 46 return value; 47} 48 49function sqlValueToUtid(value?: ColumnType): Utid | undefined { 50 if (typeof value === 'bigint') return Number(value) as Utid; 51 if (typeof value !== 'number') return undefined; 52 return value as Utid; 53} 54 55function renderTreeContents(dict: {[key: string]: m.Child}): m.Child[] { 56 const children: m.Child[] = []; 57 for (const key of Object.keys(dict)) { 58 if (dict[key] === null || dict[key] === undefined) continue; 59 children.push( 60 m(TreeNode, { 61 left: key, 62 right: dict[key], 63 }), 64 ); 65 } 66 return children; 67} 68 69export class DebugSliceDetailsTab extends BottomTab<GenericSliceDetailsTabConfig> { 70 static readonly kind = 'dev.perfetto.DebugSliceDetailsTab'; 71 72 data?: { 73 name: string; 74 ts: time; 75 dur: duration; 76 args: {[key: string]: ColumnType}; 77 }; 78 // We will try to interpret the arguments as references into well-known 79 // tables. These values will be set if the relevant columns exist and 80 // are consistent (e.g. 'ts' and 'dur' for this slice correspond to values 81 // in these well-known tables). 82 threadState?: ThreadState; 83 slice?: SliceDetails; 84 85 static create( 86 args: NewBottomTabArgs<GenericSliceDetailsTabConfig>, 87 ): DebugSliceDetailsTab { 88 return new DebugSliceDetailsTab(args); 89 } 90 91 private async maybeLoadThreadState( 92 id: number | undefined, 93 ts: time, 94 dur: duration, 95 table: string | undefined, 96 utid?: Utid, 97 ): Promise<ThreadState | undefined> { 98 if (id === undefined) return undefined; 99 if (utid === undefined) return undefined; 100 101 const threadState = await getThreadState(this.engine, id); 102 if (threadState === undefined) return undefined; 103 if ( 104 table === 'thread_state' || 105 (threadState.ts === ts && 106 threadState.dur === dur && 107 threadState.thread?.utid === utid) 108 ) { 109 return threadState; 110 } else { 111 return undefined; 112 } 113 } 114 115 private renderThreadStateInfo(): m.Child { 116 if (this.threadState === undefined) return null; 117 return m( 118 TreeNode, 119 { 120 left: threadStateRef(this.threadState), 121 right: '', 122 }, 123 renderTreeContents({ 124 Thread: getThreadName(this.threadState.thread), 125 Process: getProcessName(this.threadState.thread?.process), 126 State: this.threadState.state, 127 }), 128 ); 129 } 130 131 private async maybeLoadSlice( 132 id: number | undefined, 133 ts: time, 134 dur: duration, 135 table: string | undefined, 136 trackId?: number, 137 ): Promise<SliceDetails | undefined> { 138 if (id === undefined) return undefined; 139 if (table !== 'slice' && trackId === undefined) return undefined; 140 141 const slice = await getSlice(this.engine, asSliceSqlId(id)); 142 if (slice === undefined) return undefined; 143 if ( 144 table === 'slice' || 145 (slice.ts === ts && slice.dur === dur && slice.trackId === trackId) 146 ) { 147 return slice; 148 } else { 149 return undefined; 150 } 151 } 152 153 private renderSliceInfo(): m.Child { 154 if (this.slice === undefined) return null; 155 return m( 156 TreeNode, 157 { 158 left: sliceRef(this.slice, 'Slice'), 159 right: '', 160 }, 161 m(TreeNode, { 162 left: 'Name', 163 right: this.slice.name, 164 }), 165 m(TreeNode, { 166 left: 'Thread', 167 right: getThreadName(this.slice.thread), 168 }), 169 m(TreeNode, { 170 left: 'Process', 171 right: getProcessName(this.slice.process), 172 }), 173 hasArgs(this.slice.args) && 174 m( 175 TreeNode, 176 { 177 left: 'Args', 178 }, 179 renderArguments(this.engine, this.slice.args), 180 ), 181 ); 182 } 183 184 private async loadData() { 185 const queryResult = await this.engine.query( 186 `select * from ${this.config.sqlTableName} where id = ${this.config.id}`, 187 ); 188 const row = queryResult.firstRow({ 189 ts: LONG, 190 dur: LONG, 191 name: STR, 192 }); 193 this.data = { 194 name: row.name, 195 ts: Time.fromRaw(row.ts), 196 dur: row.dur, 197 args: {}, 198 }; 199 200 for (const key of Object.keys(row)) { 201 if (key.startsWith(ARG_PREFIX)) { 202 this.data.args[key.substr(ARG_PREFIX.length)] = ( 203 row as {[key: string]: ColumnType} 204 )[key]; 205 } 206 } 207 208 this.threadState = await this.maybeLoadThreadState( 209 sqlValueToNumber(this.data.args['id']), 210 this.data.ts, 211 this.data.dur, 212 sqlValueToReadableString(this.data.args['table_name']), 213 sqlValueToUtid(this.data.args['utid']), 214 ); 215 216 this.slice = await this.maybeLoadSlice( 217 sqlValueToNumber(this.data.args['id']) ?? 218 sqlValueToNumber(this.data.args['slice_id']), 219 this.data.ts, 220 this.data.dur, 221 sqlValueToReadableString(this.data.args['table_name']), 222 sqlValueToNumber(this.data.args['track_id']), 223 ); 224 225 raf.scheduleRedraw(); 226 } 227 228 constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) { 229 super(args); 230 this.loadData(); 231 } 232 233 viewTab() { 234 if (this.data === undefined) { 235 return m('h2', 'Loading'); 236 } 237 const details = dictToTreeNodes({ 238 'Name': this.data['name'] as string, 239 'Start time': m(Timestamp, {ts: timeFromSql(this.data['ts'])}), 240 'Duration': m(DurationWidget, {dur: durationFromSql(this.data['dur'])}), 241 'Debug slice id': `${this.config.sqlTableName}[${this.config.id}]`, 242 }); 243 details.push(this.renderThreadStateInfo()); 244 details.push(this.renderSliceInfo()); 245 246 const args: {[key: string]: m.Child} = {}; 247 for (const key of Object.keys(this.data.args)) { 248 args[key] = sqlValueToReadableString(this.data.args[key]); 249 } 250 251 return m( 252 DetailsShell, 253 { 254 title: 'Debug Slice', 255 }, 256 m( 257 GridLayout, 258 m(Section, {title: 'Details'}, m(Tree, details)), 259 m(Section, {title: 'Arguments'}, dictToTree(args)), 260 ), 261 ); 262 } 263 264 getTitle(): string { 265 return `Current Selection`; 266 } 267 268 isLoading() { 269 return this.data === undefined; 270 } 271} 272