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'; 41 42export const ARG_PREFIX = 'arg_'; 43 44function sqlValueToNumber(value?: ColumnType): number | undefined { 45 if (typeof value === 'bigint') return Number(value); 46 if (typeof value !== 'number') return undefined; 47 return value; 48} 49 50function sqlValueToUtid(value?: ColumnType): Utid | undefined { 51 if (typeof value === 'bigint') return Number(value) as Utid; 52 if (typeof value !== 'number') return undefined; 53 return value as Utid; 54} 55 56function renderTreeContents(dict: {[key: string]: m.Child}): m.Child[] { 57 const children: m.Child[] = []; 58 for (const key of Object.keys(dict)) { 59 if (dict[key] === null || dict[key] === undefined) continue; 60 children.push( 61 m(TreeNode, { 62 left: key, 63 right: dict[key], 64 }), 65 ); 66 } 67 return children; 68} 69 70export class SqlTableSliceTrackDetailsPanel implements TrackEventDetailsPanel { 71 private data?: { 72 name: string; 73 ts: time; 74 dur: duration; 75 args: {[key: string]: ColumnType}; 76 }; 77 // We will try to interpret the arguments as references into well-known 78 // tables. These values will be set if the relevant columns exist and 79 // are consistent (e.g. 'ts' and 'dur' for this slice correspond to values 80 // in these well-known tables). 81 private threadState?: ThreadState; 82 private slice?: SliceDetails; 83 84 constructor( 85 private readonly trace: Trace, 86 private readonly tableName: string, 87 private readonly eventId: number, 88 ) {} 89 90 private async maybeLoadThreadState( 91 id: number | undefined, 92 ts: time, 93 dur: duration, 94 table: string | undefined, 95 utid?: Utid, 96 ): Promise<ThreadState | undefined> { 97 if (id === undefined) return undefined; 98 if (utid === undefined) return undefined; 99 100 const threadState = await getThreadState(this.trace.engine, id); 101 if (threadState === undefined) return undefined; 102 if ( 103 table === 'thread_state' || 104 (threadState.ts === ts && 105 threadState.dur === dur && 106 threadState.thread?.utid === utid) 107 ) { 108 return threadState; 109 } else { 110 return undefined; 111 } 112 } 113 114 private renderThreadStateInfo(): m.Child { 115 if (this.threadState === undefined) return null; 116 return m( 117 TreeNode, 118 { 119 left: threadStateRef(this.threadState), 120 right: '', 121 }, 122 renderTreeContents({ 123 Thread: getThreadName(this.threadState.thread), 124 Process: getProcessName(this.threadState.thread?.process), 125 State: this.threadState.state, 126 }), 127 ); 128 } 129 130 private async maybeLoadSlice( 131 id: number | undefined, 132 ts: time, 133 dur: duration, 134 table: string | undefined, 135 trackId?: number, 136 ): Promise<SliceDetails | undefined> { 137 if (id === undefined) return undefined; 138 if (table !== 'slice' && trackId === undefined) return undefined; 139 140 const slice = await getSlice(this.trace.engine, asSliceSqlId(id)); 141 if (slice === undefined) return undefined; 142 if ( 143 table === 'slice' || 144 (slice.ts === ts && slice.dur === dur && slice.trackId === trackId) 145 ) { 146 return slice; 147 } else { 148 return undefined; 149 } 150 } 151 152 private renderSliceInfo(): m.Child { 153 if (this.slice === undefined) return null; 154 return m( 155 TreeNode, 156 { 157 left: sliceRef(this.slice, 'Slice'), 158 right: '', 159 }, 160 m(TreeNode, { 161 left: 'Name', 162 right: this.slice.name, 163 }), 164 m(TreeNode, { 165 left: 'Thread', 166 right: getThreadName(this.slice.thread), 167 }), 168 m(TreeNode, { 169 left: 'Process', 170 right: getProcessName(this.slice.process), 171 }), 172 hasArgs(this.slice.args) && 173 m( 174 TreeNode, 175 { 176 left: 'Args', 177 }, 178 renderArguments(this.trace, this.slice.args), 179 ), 180 ); 181 } 182 183 async load() { 184 const queryResult = await this.trace.engine.query( 185 `select * from ${this.tableName} where id = ${this.eventId}`, 186 ); 187 const row = queryResult.firstRow({ 188 ts: LONG, 189 dur: LONG, 190 name: STR, 191 }); 192 this.data = { 193 name: row.name, 194 ts: Time.fromRaw(row.ts), 195 dur: row.dur, 196 args: {}, 197 }; 198 199 for (const key of Object.keys(row)) { 200 if (key.startsWith(ARG_PREFIX)) { 201 this.data.args[key.substr(ARG_PREFIX.length)] = ( 202 row as {[key: string]: ColumnType} 203 )[key]; 204 } 205 } 206 207 this.threadState = await this.maybeLoadThreadState( 208 sqlValueToNumber(this.data.args['id']), 209 this.data.ts, 210 this.data.dur, 211 sqlValueToReadableString(this.data.args['table_name']), 212 sqlValueToUtid(this.data.args['utid']), 213 ); 214 215 this.slice = await this.maybeLoadSlice( 216 sqlValueToNumber(this.data.args['id']) ?? 217 sqlValueToNumber(this.data.args['slice_id']), 218 this.data.ts, 219 this.data.dur, 220 sqlValueToReadableString(this.data.args['table_name']), 221 sqlValueToNumber(this.data.args['track_id']), 222 ); 223 224 this.trace.scheduleFullRedraw(); 225 } 226 227 render() { 228 if (this.data === undefined) { 229 return m('h2', 'Loading'); 230 } 231 const details = dictToTreeNodes({ 232 'Name': this.data['name'] as string, 233 'Start time': m(Timestamp, {ts: timeFromSql(this.data['ts'])}), 234 'Duration': m(DurationWidget, {dur: durationFromSql(this.data['dur'])}), 235 'Slice id': `${this.tableName}[${this.eventId}]`, 236 }); 237 details.push(this.renderThreadStateInfo()); 238 details.push(this.renderSliceInfo()); 239 240 const args: {[key: string]: m.Child} = {}; 241 for (const key of Object.keys(this.data.args)) { 242 args[key] = sqlValueToReadableString(this.data.args[key]); 243 } 244 245 return m( 246 DetailsShell, 247 { 248 title: 'Slice', 249 }, 250 m( 251 GridLayout, 252 m(Section, {title: 'Details'}, m(Tree, details)), 253 m(Section, {title: 'Arguments'}, dictToTree(args)), 254 ), 255 ); 256 } 257 258 getTitle(): string { 259 return `Current Selection`; 260 } 261 262 isLoading() { 263 return this.data === undefined; 264 } 265} 266