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'; 16import {duration, Time, time} from '../../base/time'; 17import {hasArgs, renderArguments} from '../details/slice_args'; 18import {getSlice, SliceDetails} from '../sql_utils/slice'; 19import {asSliceSqlId, Utid} from '../sql_utils/core_types'; 20import {getThreadState, ThreadState} from '../sql_utils/thread_state'; 21import {DurationWidget} from '../widgets/duration'; 22import {Timestamp} from '../widgets/timestamp'; 23import { 24 ColumnType, 25 durationFromSql, 26 LONG, 27 STR, 28 timeFromSql, 29} from '../../trace_processor/query_result'; 30import {sqlValueToReadableString} from '../../trace_processor/sql_utils'; 31import {DetailsShell} from '../../widgets/details_shell'; 32import {GridLayout} from '../../widgets/grid_layout'; 33import {Section} from '../../widgets/section'; 34import {dictToTree, dictToTreeNodes, Tree, TreeNode} from '../../widgets/tree'; 35import {threadStateRef} from '../widgets/thread_state'; 36import {getThreadName} from '../sql_utils/thread'; 37import {getProcessName} from '../sql_utils/process'; 38import {sliceRef} from '../widgets/slice'; 39import {TrackEventDetailsPanel} from '../../public/details_panel'; 40import {Trace} from '../../public/trace'; 41import {SqlRef} from '../../widgets/sql_ref'; 42 43export const ARG_PREFIX = 'arg_'; 44 45function sqlValueToNumber(value?: ColumnType): number | undefined { 46 if (typeof value === 'bigint') return Number(value); 47 if (typeof value !== 'number') return undefined; 48 return value; 49} 50 51function sqlValueToUtid(value?: ColumnType): Utid | undefined { 52 if (typeof value === 'bigint') return Number(value) as Utid; 53 if (typeof value !== 'number') return undefined; 54 return value as Utid; 55} 56 57function renderTreeContents(dict: {[key: string]: m.Child}): m.Child[] { 58 const children: m.Child[] = []; 59 for (const key of Object.keys(dict)) { 60 if (dict[key] === null || dict[key] === undefined) continue; 61 children.push( 62 m(TreeNode, { 63 left: key, 64 right: dict[key], 65 }), 66 ); 67 } 68 return children; 69} 70 71export class DebugSliceTrackDetailsPanel implements TrackEventDetailsPanel { 72 private 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 private threadState?: ThreadState; 83 private slice?: SliceDetails; 84 85 constructor( 86 private readonly trace: Trace, 87 private readonly tableName: string, 88 private readonly eventId: number, 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.trace.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.trace.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.trace, this.slice.args), 180 ), 181 ); 182 } 183 184 async load() { 185 const queryResult = await this.trace.engine.query( 186 `select * from ${this.tableName} where id = ${this.eventId}`, 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 226 render() { 227 if (this.data === undefined) { 228 return m('h2', 'Loading'); 229 } 230 const details = dictToTreeNodes({ 231 'Name': this.data['name'] as string, 232 'Start time': m(Timestamp, {ts: timeFromSql(this.data['ts'])}), 233 'Duration': m(DurationWidget, {dur: durationFromSql(this.data['dur'])}), 234 'SQL ID': m(SqlRef, {table: this.tableName, id: this.eventId}), 235 }); 236 details.push(this.renderThreadStateInfo()); 237 details.push(this.renderSliceInfo()); 238 239 const args: {[key: string]: m.Child} = {}; 240 for (const key of Object.keys(this.data.args)) { 241 args[key] = sqlValueToReadableString(this.data.args[key]); 242 } 243 244 return m( 245 DetailsShell, 246 { 247 title: 'Slice', 248 }, 249 m( 250 GridLayout, 251 m(Section, {title: 'Details'}, m(Tree, details)), 252 m(Section, {title: 'Arguments'}, dictToTree(args)), 253 ), 254 ); 255 } 256 257 getTitle(): string { 258 return `Current Selection`; 259 } 260 261 isLoading() { 262 return this.data === undefined; 263 } 264} 265