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 {copyToClipboard} from '../../base/clipboard'; 18import {isString} from '../../base/object_utils'; 19import {Icons} from '../../base/semantic_icons'; 20import {sqliteString} from '../../base/string_utils'; 21import {Duration, Time} from '../../base/time'; 22import {Row} from '../../trace_processor/query_result'; 23import { 24 SqlValue, 25 sqlValueToReadableString, 26} from '../../trace_processor/sql_utils'; 27import {Anchor} from '../../widgets/anchor'; 28import {renderError} from '../../widgets/error'; 29import {MenuItem, PopupMenu2} from '../../widgets/menu'; 30import {SliceRef} from '../sql/slice'; 31import {asSliceSqlId} from '../sql_types'; 32import {DurationWidget} from '../widgets/duration'; 33import {Timestamp} from '../widgets/timestamp'; 34 35import {Column} from './column'; 36import {SqlTableState} from './state'; 37import {SliceIdDisplayConfig} from './table_description'; 38 39// This file is responsible for rendering a value in a given sell based on the 40// column type. 41 42function filterOptionMenuItem( 43 label: string, 44 filter: string, 45 state: SqlTableState, 46): m.Child { 47 return m(MenuItem, { 48 label, 49 onclick: () => { 50 state.addFilter(filter); 51 }, 52 }); 53} 54 55function getStandardFilters( 56 c: Column, 57 value: SqlValue, 58 state: SqlTableState, 59): m.Child[] { 60 if (value === null) { 61 return [ 62 filterOptionMenuItem('is null', `${c.expression} is null`, state), 63 filterOptionMenuItem('is not null', `${c.expression} is not null`, state), 64 ]; 65 } 66 if (isString(value)) { 67 return [ 68 filterOptionMenuItem( 69 'equals to', 70 `${c.expression} = ${sqliteString(value)}`, 71 state, 72 ), 73 filterOptionMenuItem( 74 'not equals to', 75 `${c.expression} != ${sqliteString(value)}`, 76 state, 77 ), 78 ]; 79 } 80 if (typeof value === 'bigint' || typeof value === 'number') { 81 return [ 82 filterOptionMenuItem('equals to', `${c.expression} = ${value}`, state), 83 filterOptionMenuItem( 84 'not equals to', 85 `${c.expression} != ${value}`, 86 state, 87 ), 88 filterOptionMenuItem('greater than', `${c.expression} > ${value}`, state), 89 filterOptionMenuItem( 90 'greater or equals than', 91 `${c.expression} >= ${value}`, 92 state, 93 ), 94 filterOptionMenuItem('less than', `${c.expression} < ${value}`, state), 95 filterOptionMenuItem( 96 'less or equals than', 97 `${c.expression} <= ${value}`, 98 state, 99 ), 100 ]; 101 } 102 return []; 103} 104 105function displayValue(value: SqlValue): m.Child { 106 if (value === null) { 107 return m('i', 'NULL'); 108 } 109 return sqlValueToReadableString(value); 110} 111 112function display(column: Column, row: Row): m.Children { 113 const value = row[column.alias]; 114 return displayValue(value); 115} 116 117function copyMenuItem(label: string, value: string): m.Child { 118 return m(MenuItem, { 119 icon: Icons.Copy, 120 label, 121 onclick: () => { 122 copyToClipboard(value); 123 }, 124 }); 125} 126 127function getContextMenuItems( 128 column: Column, 129 row: Row, 130 state: SqlTableState, 131): m.Child[] { 132 const result: m.Child[] = []; 133 const value = row[column.alias]; 134 135 if (isString(value)) { 136 result.push(copyMenuItem('Copy', value)); 137 } 138 139 const filters = getStandardFilters(column, value, state); 140 if (filters.length > 0) { 141 result.push( 142 m(MenuItem, {label: 'Add filter', icon: Icons.Filter}, ...filters), 143 ); 144 } 145 146 return result; 147} 148 149function renderStandardColumn( 150 column: Column, 151 row: Row, 152 state: SqlTableState, 153): m.Children { 154 const displayValue = display(column, row); 155 const contextMenuItems: m.Child[] = getContextMenuItems(column, row, state); 156 return m( 157 PopupMenu2, 158 { 159 trigger: m(Anchor, displayValue), 160 }, 161 ...contextMenuItems, 162 ); 163} 164 165function renderTimestampColumn( 166 column: Column, 167 row: Row, 168 state: SqlTableState, 169): m.Children { 170 const value = row[column.alias]; 171 if (typeof value !== 'bigint') { 172 return renderStandardColumn(column, row, state); 173 } 174 175 return m(Timestamp, { 176 ts: Time.fromRaw(value), 177 extraMenuItems: getContextMenuItems(column, row, state), 178 }); 179} 180 181function renderDurationColumn( 182 column: Column, 183 row: Row, 184 state: SqlTableState, 185): m.Children { 186 const value = row[column.alias]; 187 if (typeof value !== 'bigint') { 188 return renderStandardColumn(column, row, state); 189 } 190 191 return m(DurationWidget, { 192 dur: Duration.fromRaw(value), 193 extraMenuItems: getContextMenuItems(column, row, state), 194 }); 195} 196 197function renderSliceIdColumn( 198 column: {alias: string; display: SliceIdDisplayConfig}, 199 row: Row, 200): m.Children { 201 const config = column.display; 202 const id = row[column.alias]; 203 const ts = row[config.ts]; 204 const dur = row[config.dur] === null ? -1n : row[config.dur]; 205 const trackId = row[config.trackId]; 206 207 const columnNotFoundError = (type: string, name: string) => 208 renderError(`${type} column ${name} not found`); 209 const wrongTypeError = (type: string, name: string, value: SqlValue) => 210 renderError( 211 `Wrong type for ${type} column ${name}: bigint expected, ${typeof value} found`, 212 ); 213 214 if (typeof id !== 'bigint') { 215 return sqlValueToReadableString(id); 216 } 217 if (ts === undefined) return columnNotFoundError('Timestamp', config.ts); 218 if (typeof ts !== 'bigint') return wrongTypeError('timestamp', config.ts, ts); 219 if (dur === undefined) return columnNotFoundError('Duration', config.dur); 220 if (typeof dur !== 'bigint') { 221 return wrongTypeError('duration', config.dur, ts); 222 } 223 if (trackId === undefined) return columnNotFoundError('Track id', trackId); 224 if (typeof trackId !== 'bigint') { 225 return wrongTypeError('track id', config.trackId, trackId); 226 } 227 228 return m(SliceRef, { 229 id: asSliceSqlId(Number(id)), 230 name: `${id}`, 231 ts: Time.fromRaw(ts), 232 dur: dur, 233 sqlTrackId: Number(trackId), 234 switchToCurrentSelectionTab: false, 235 }); 236} 237 238export function renderCell( 239 column: Column, 240 row: Row, 241 state: SqlTableState, 242): m.Children { 243 if (column.display) { 244 switch (column.display?.type) { 245 case 'slice_id': 246 return renderSliceIdColumn( 247 {alias: column.alias, display: column.display}, 248 row, 249 ); 250 case 'timestamp': 251 return renderTimestampColumn(column, row, state); 252 case 'duration': 253 case 'thread_duration': 254 return renderDurationColumn(column, row, state); 255 } 256 } 257 return renderStandardColumn(column, row, state); 258} 259