1// Copyright (C) 2024 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 {sqliteString} from '../../../../base/string_utils'; 17import {Duration, Time} from '../../../../base/time'; 18import {SqlValue, STR} from '../../../../trace_processor/query_result'; 19import { 20 asSchedSqlId, 21 asSliceSqlId, 22 asThreadStateSqlId, 23 asUpid, 24 asUtid, 25} from '../../../sql_utils/core_types'; 26import {Anchor} from '../../../../widgets/anchor'; 27import {renderError} from '../../../../widgets/error'; 28import {PopupMenu} from '../../../../widgets/menu'; 29import {DurationWidget} from '../../duration'; 30import {showProcessDetailsMenuItem} from '../../process'; 31import {SchedRef} from '../../sched'; 32import {SliceRef} from '../../slice'; 33import {showThreadDetailsMenuItem} from '../../thread'; 34import {ThreadStateRef} from '../../thread_state'; 35import {Timestamp} from '../../timestamp'; 36import {TableColumn, TableManager} from './table_column'; 37import { 38 getStandardContextMenuItems, 39 renderStandardCell, 40} from './render_cell_utils'; 41import {SqlColumn, sqlColumnId, SqlExpression} from './sql_column'; 42 43function wrongTypeError(type: string, name: SqlColumn, value: SqlValue) { 44 return renderError( 45 `Wrong type for ${type} column ${sqlColumnId(name)}: bigint expected, ${typeof value} found`, 46 ); 47} 48 49export type ColumnParams = { 50 alias?: string; 51 startsHidden?: boolean; 52 title?: string; 53}; 54 55export type StandardColumnParams = ColumnParams; 56 57export interface IdColumnParams { 58 // Whether this column is a primary key (ID) for this table or whether it's a reference 59 // to another table's primary key. 60 type?: 'id' | 'joinid'; 61 // Whether the column is guaranteed not to have null values. 62 // (this will allow us to upgrage the joins on this column to more performant INNER JOINs). 63 notNull?: boolean; 64} 65 66export class StandardColumn implements TableColumn { 67 constructor( 68 public readonly column: SqlColumn, 69 private params?: StandardColumnParams, 70 ) {} 71 72 renderCell(value: SqlValue, tableManager?: TableManager): m.Children { 73 return renderStandardCell(value, this.column, tableManager); 74 } 75 76 initialColumns(): TableColumn[] { 77 return this.params?.startsHidden ? [] : [this]; 78 } 79} 80 81export class TimestampColumn implements TableColumn { 82 constructor(public readonly column: SqlColumn) {} 83 84 renderCell(value: SqlValue, tableManager?: TableManager): m.Children { 85 if (typeof value === 'number') { 86 value = BigInt(Math.round(value)); 87 } 88 if (typeof value !== 'bigint') { 89 return renderStandardCell(value, this.column, tableManager); 90 } 91 return m(Timestamp, { 92 ts: Time.fromRaw(value), 93 extraMenuItems: 94 tableManager && 95 getStandardContextMenuItems(value, this.column, tableManager), 96 }); 97 } 98} 99 100export class DurationColumn implements TableColumn { 101 constructor(public column: SqlColumn) {} 102 103 renderCell(value: SqlValue, tableManager?: TableManager): m.Children { 104 if (typeof value === 'number') { 105 value = BigInt(Math.round(value)); 106 } 107 if (typeof value !== 'bigint') { 108 return renderStandardCell(value, this.column, tableManager); 109 } 110 111 return m(DurationWidget, { 112 dur: Duration.fromRaw(value), 113 extraMenuItems: 114 tableManager && 115 getStandardContextMenuItems(value, this.column, tableManager), 116 }); 117 } 118} 119 120export class SliceIdColumn implements TableColumn { 121 constructor( 122 public readonly column: SqlColumn, 123 private params?: IdColumnParams, 124 ) {} 125 126 renderCell(value: SqlValue, manager?: TableManager): m.Children { 127 const id = value; 128 129 if (!manager || id === null) { 130 return renderStandardCell(id, this.column, manager); 131 } 132 133 return m(SliceRef, { 134 id: asSliceSqlId(Number(id)), 135 name: `${id}`, 136 switchToCurrentSelectionTab: false, 137 }); 138 } 139 140 listDerivedColumns() { 141 if (this.params?.type === 'id') return undefined; 142 return async () => 143 new Map<string, TableColumn>([ 144 ['ts', new TimestampColumn(this.getChildColumn('ts'))], 145 ['dur', new DurationColumn(this.getChildColumn('dur'))], 146 ['name', new StandardColumn(this.getChildColumn('name'))], 147 ['parent_id', new SliceIdColumn(this.getChildColumn('parent_id'))], 148 ]); 149 } 150 151 private getChildColumn(name: string): SqlColumn { 152 return { 153 column: name, 154 source: { 155 table: 'slice', 156 joinOn: {id: this.column}, 157 }, 158 }; 159 } 160} 161 162export class SchedIdColumn implements TableColumn { 163 constructor(public readonly column: SqlColumn) {} 164 165 renderCell(value: SqlValue, manager?: TableManager): m.Children { 166 const id = value; 167 168 if (!manager || id === null) { 169 return renderStandardCell(id, this.column, manager); 170 } 171 if (typeof id !== 'bigint') return wrongTypeError('id', this.column, id); 172 173 return m(SchedRef, { 174 id: asSchedSqlId(Number(id)), 175 name: `${id}`, 176 switchToCurrentSelectionTab: false, 177 }); 178 } 179} 180 181export class ThreadStateIdColumn implements TableColumn { 182 constructor(public readonly column: SqlColumn) {} 183 184 renderCell(value: SqlValue, manager?: TableManager): m.Children { 185 const id = value; 186 187 if (!manager || id === null) { 188 return renderStandardCell(id, this.column, manager); 189 } 190 if (typeof id !== 'bigint') return wrongTypeError('id', this.column, id); 191 192 return m(ThreadStateRef, { 193 id: asThreadStateSqlId(Number(id)), 194 name: `${id}`, 195 switchToCurrentSelectionTab: false, 196 }); 197 } 198} 199 200export class ThreadIdColumn implements TableColumn { 201 constructor( 202 public readonly column: SqlColumn, 203 private params?: IdColumnParams, 204 ) {} 205 206 renderCell(value: SqlValue, manager?: TableManager): m.Children { 207 const utid = value; 208 209 if (!manager || utid === null) { 210 return renderStandardCell(utid, this.column, manager); 211 } 212 213 if (typeof utid !== 'bigint') { 214 throw new Error( 215 `thread.utid is expected to be bigint, got ${typeof utid}`, 216 ); 217 } 218 219 return m( 220 PopupMenu, 221 { 222 trigger: m(Anchor, `${utid}`), 223 }, 224 225 showThreadDetailsMenuItem(asUtid(Number(utid))), 226 getStandardContextMenuItems(utid, this.column, manager), 227 ); 228 } 229 230 listDerivedColumns() { 231 if (this.params?.type === 'id') return undefined; 232 return async () => 233 new Map<string, TableColumn>([ 234 ['tid', new StandardColumn(this.getChildColumn('tid'))], 235 ['name', new StandardColumn(this.getChildColumn('name'))], 236 ['start_ts', new TimestampColumn(this.getChildColumn('start_ts'))], 237 ['end_ts', new TimestampColumn(this.getChildColumn('end_ts'))], 238 ['upid', new ProcessIdColumn(this.getChildColumn('upid'))], 239 [ 240 'is_main_thread', 241 new StandardColumn(this.getChildColumn('is_main_thread')), 242 ], 243 ]); 244 } 245 246 initialColumns(): TableColumn[] { 247 return [ 248 this, 249 new StandardColumn(this.getChildColumn('tid')), 250 new StandardColumn(this.getChildColumn('name')), 251 ]; 252 } 253 254 private getChildColumn(name: string): SqlColumn { 255 return { 256 column: name, 257 source: { 258 table: 'thread', 259 joinOn: {id: this.column}, 260 // If the column is guaranteed not to have null values, we can use an INNER JOIN. 261 innerJoin: this.params?.notNull === true, 262 }, 263 }; 264 } 265} 266 267export class ProcessIdColumn implements TableColumn { 268 constructor( 269 public readonly column: SqlColumn, 270 private params?: IdColumnParams, 271 ) {} 272 273 renderCell(value: SqlValue, manager?: TableManager): m.Children { 274 const upid = value; 275 276 if (!manager || upid === null) { 277 return renderStandardCell(upid, this.column, manager); 278 } 279 280 if (typeof upid !== 'bigint') { 281 throw new Error( 282 `thread.upid is expected to be bigint, got ${typeof upid}`, 283 ); 284 } 285 286 return m( 287 PopupMenu, 288 { 289 trigger: m(Anchor, `${upid}`), 290 }, 291 292 showProcessDetailsMenuItem(asUpid(Number(upid))), 293 getStandardContextMenuItems(upid, this.column, manager), 294 ); 295 } 296 297 listDerivedColumns() { 298 if (this.params?.type === 'id') return undefined; 299 return async () => 300 new Map<string, TableColumn>([ 301 ['pid', new StandardColumn(this.getChildColumn('pid'))], 302 ['name', new StandardColumn(this.getChildColumn('name'))], 303 ['start_ts', new TimestampColumn(this.getChildColumn('start_ts'))], 304 ['end_ts', new TimestampColumn(this.getChildColumn('end_ts'))], 305 [ 306 'parent_upid', 307 new ProcessIdColumn(this.getChildColumn('parent_upid')), 308 ], 309 [ 310 'is_main_thread', 311 new StandardColumn(this.getChildColumn('is_main_thread')), 312 ], 313 ]); 314 } 315 316 initialColumns(): TableColumn[] { 317 return [ 318 this, 319 new StandardColumn(this.getChildColumn('pid')), 320 new StandardColumn(this.getChildColumn('name')), 321 ]; 322 } 323 324 private getChildColumn(name: string): SqlColumn { 325 return { 326 column: name, 327 source: { 328 table: 'process', 329 joinOn: {id: this.column}, 330 // If the column is guaranteed not to have null values, we can use an INNER JOIN. 331 innerJoin: this.params?.notNull === true, 332 }, 333 }; 334 } 335} 336 337class ArgColumn implements TableColumn<{type: SqlColumn}> { 338 public readonly column: SqlColumn; 339 private id: string; 340 341 constructor( 342 private argSetId: SqlColumn, 343 private key: string, 344 ) { 345 this.id = `${sqlColumnId(this.argSetId)}[${this.key}]`; 346 this.column = new SqlExpression( 347 (cols: string[]) => `COALESCE(${cols[0]}, ${cols[1]}, ${cols[2]})`, 348 [ 349 this.getRawColumn('string_value'), 350 this.getRawColumn('int_value'), 351 this.getRawColumn('real_value'), 352 ], 353 this.id, 354 ); 355 } 356 357 supportingColumns() { 358 return {type: this.getRawColumn('value_type')}; 359 } 360 361 private getRawColumn( 362 type: 'string_value' | 'int_value' | 'real_value' | 'id' | 'value_type', 363 ): SqlColumn { 364 return { 365 column: type, 366 source: { 367 table: 'args', 368 joinOn: { 369 arg_set_id: this.argSetId, 370 key: `${sqliteString(this.key)}`, 371 }, 372 }, 373 id: `${this.id}.${type.replace(/_value$/g, '')}`, 374 }; 375 } 376 377 renderCell( 378 value: SqlValue, 379 tableManager?: TableManager, 380 values?: {type: SqlValue}, 381 ): m.Children { 382 // If the value is NULL, then filters can check for id column for better performance. 383 if (value === null) { 384 return renderStandardCell( 385 value, 386 this.getRawColumn('value_type'), 387 tableManager, 388 ); 389 } 390 if (values?.type === 'int') { 391 return renderStandardCell( 392 value, 393 this.getRawColumn('int_value'), 394 tableManager, 395 ); 396 } 397 if (values?.type === 'string') { 398 return renderStandardCell( 399 value, 400 this.getRawColumn('string_value'), 401 tableManager, 402 ); 403 } 404 if (values?.type === 'real') { 405 return renderStandardCell( 406 value, 407 this.getRawColumn('real_value'), 408 tableManager, 409 ); 410 } 411 return renderStandardCell(value, this.column, tableManager); 412 } 413} 414 415export class ArgSetIdColumn implements TableColumn { 416 constructor(public readonly column: SqlColumn) {} 417 418 renderCell(value: SqlValue, tableManager: TableManager): m.Children { 419 return renderStandardCell(value, this.column, tableManager); 420 } 421 422 listDerivedColumns(manager: TableManager) { 423 return async () => { 424 const queryResult = await manager.trace.engine.query(` 425 SELECT 426 DISTINCT args.key 427 FROM (${manager.getSqlQuery({arg_set_id: this.column})}) data 428 JOIN args USING (arg_set_id) 429 `); 430 const result = new Map(); 431 const it = queryResult.iter({key: STR}); 432 for (; it.valid(); it.next()) { 433 result.set(it.key, argTableColumn(this.column, it.key)); 434 } 435 return result; 436 }; 437 } 438 439 initialColumns() { 440 return []; 441 } 442} 443 444export function argTableColumn(argSetId: SqlColumn, key: string): TableColumn { 445 return new ArgColumn(argSetId, key); 446} 447