1// Copyright (C) 2020 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 {BigintMath} from '../base/bigint_math'; 18import {copyToClipboard} from '../base/clipboard'; 19import {isString} from '../base/object_utils'; 20import {Time} from '../base/time'; 21import {Actions} from '../common/actions'; 22import {QueryResponse} from '../common/queries'; 23import {Row} from '../trace_processor/query_result'; 24import {Anchor} from '../widgets/anchor'; 25import {Button} from '../widgets/button'; 26import {Callout} from '../widgets/callout'; 27import {DetailsShell} from '../widgets/details_shell'; 28 29import {queryResponseToClipboard} from './clipboard'; 30import {downloadData} from './download_utils'; 31import {globals} from './globals'; 32import {Router} from './router'; 33import {reveal} from './scroll_helper'; 34 35interface QueryTableRowAttrs { 36 row: Row; 37 columns: string[]; 38} 39 40type Numeric = bigint | number; 41 42function isIntegral(x: Row[string]): x is Numeric { 43 return ( 44 typeof x === 'bigint' || (typeof x === 'number' && Number.isInteger(x)) 45 ); 46} 47 48function hasTs(row: Row): row is Row & {ts: Numeric} { 49 return 'ts' in row && isIntegral(row.ts); 50} 51 52function hasDur(row: Row): row is Row & {dur: Numeric} { 53 return 'dur' in row && isIntegral(row.dur); 54} 55 56function hasTrackId(row: Row): row is Row & {track_id: Numeric} { 57 return 'track_id' in row && isIntegral(row.track_id); 58} 59 60function hasType(row: Row): row is Row & {type: string} { 61 return 'type' in row && isString(row.type); 62} 63 64function hasId(row: Row): row is Row & {id: Numeric} { 65 return 'id' in row && isIntegral(row.id); 66} 67 68function hasSliceId(row: Row): row is Row & {slice_id: Numeric} { 69 return 'slice_id' in row && isIntegral(row.slice_id); 70} 71 72// These are properties that a row should have in order to be "slice-like", 73// insofar as it represents a time range and a track id which can be revealed 74// or zoomed-into on the timeline. 75type Sliceish = { 76 ts: Numeric; 77 dur: Numeric; 78 track_id: Numeric; 79}; 80 81export function isSliceish(row: Row): row is Row & Sliceish { 82 return hasTs(row) && hasDur(row) && hasTrackId(row); 83} 84 85// Attempts to extract a slice ID from a row, or undefined if none can be found 86export function getSliceId(row: Row): number | undefined { 87 if (hasType(row) && row.type.includes('slice')) { 88 if (hasId(row)) { 89 return Number(row.id); 90 } 91 } else { 92 if (hasSliceId(row)) { 93 return Number(row.slice_id); 94 } 95 } 96 return undefined; 97} 98 99class QueryTableRow implements m.ClassComponent<QueryTableRowAttrs> { 100 view(vnode: m.Vnode<QueryTableRowAttrs>) { 101 const {row, columns} = vnode.attrs; 102 const cells = columns.map((col) => this.renderCell(col, row[col])); 103 104 // TODO(dproy): Make click handler work from analyze page. 105 if ( 106 Router.parseUrl(window.location.href).page === '/viewer' && 107 isSliceish(row) 108 ) { 109 return m( 110 'tr', 111 { 112 onclick: () => this.selectAndRevealSlice(row, false), 113 // TODO(altimin): Consider improving the logic here (e.g. delay?) to 114 // account for cases when dblclick fires late. 115 ondblclick: () => this.selectAndRevealSlice(row, true), 116 clickable: true, 117 title: 'Go to slice', 118 }, 119 cells, 120 ); 121 } else { 122 return m('tr', cells); 123 } 124 } 125 126 private renderCell(name: string, value: Row[string]) { 127 if (value instanceof Uint8Array) { 128 return m('td', this.renderBlob(name, value)); 129 } else { 130 return m('td', `${value}`); 131 } 132 } 133 134 private renderBlob(name: string, value: Uint8Array) { 135 return m( 136 Anchor, 137 { 138 onclick: () => downloadData(`${name}.blob`, value), 139 }, 140 `Blob (${value.length} bytes)`, 141 ); 142 } 143 144 private selectAndRevealSlice( 145 row: Row & Sliceish, 146 switchToCurrentSelectionTab: boolean, 147 ) { 148 const trackId = Number(row.track_id); 149 const sliceStart = Time.fromRaw(BigInt(row.ts)); 150 // row.dur can be negative. Clamp to 1ns. 151 const sliceDur = BigintMath.max(BigInt(row.dur), 1n); 152 const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId); 153 if (trackKey !== undefined) { 154 reveal(trackKey, sliceStart, Time.add(sliceStart, sliceDur), true); 155 const sliceId = getSliceId(row); 156 if (sliceId !== undefined) { 157 this.selectSlice(sliceId, trackKey, switchToCurrentSelectionTab); 158 } 159 } 160 } 161 162 private selectSlice( 163 sliceId: number, 164 trackKey: string, 165 switchToCurrentSelectionTab: boolean, 166 ) { 167 const action = Actions.selectSlice({ 168 id: sliceId, 169 trackKey, 170 table: 'slice', 171 }); 172 globals.makeSelection(action, {switchToCurrentSelectionTab}); 173 } 174} 175 176interface QueryTableContentAttrs { 177 resp: QueryResponse; 178} 179 180class QueryTableContent implements m.ClassComponent<QueryTableContentAttrs> { 181 private previousResponse?: QueryResponse; 182 183 onbeforeupdate(vnode: m.CVnode<QueryTableContentAttrs>) { 184 return vnode.attrs.resp !== this.previousResponse; 185 } 186 187 view(vnode: m.CVnode<QueryTableContentAttrs>) { 188 const resp = vnode.attrs.resp; 189 this.previousResponse = resp; 190 const cols = []; 191 for (const col of resp.columns) { 192 cols.push(m('td', col)); 193 } 194 const tableHeader = m('tr', cols); 195 196 const rows = resp.rows.map((row) => 197 m(QueryTableRow, {row, columns: resp.columns}), 198 ); 199 200 if (resp.error) { 201 return m('.query-error', `SQL error: ${resp.error}`); 202 } else { 203 return m( 204 'table.pf-query-table', 205 m('thead', tableHeader), 206 m('tbody', rows), 207 ); 208 } 209 } 210} 211 212interface QueryTableAttrs { 213 query: string; 214 resp?: QueryResponse; 215 contextButtons?: m.Child[]; 216 fillParent: boolean; 217} 218 219export class QueryTable implements m.ClassComponent<QueryTableAttrs> { 220 view({attrs}: m.CVnode<QueryTableAttrs>) { 221 const {resp, query, contextButtons = [], fillParent} = attrs; 222 223 return m( 224 DetailsShell, 225 { 226 title: this.renderTitle(resp), 227 description: query, 228 buttons: this.renderButtons(query, contextButtons, resp), 229 fillParent, 230 }, 231 resp && this.renderTableContent(resp), 232 ); 233 } 234 235 renderTitle(resp?: QueryResponse) { 236 if (!resp) { 237 return 'Query - running'; 238 } 239 const result = resp.error ? 'error' : `${resp.rows.length} rows`; 240 return `Query result (${result}) - ${resp.durationMs.toLocaleString()}ms`; 241 } 242 243 renderButtons( 244 query: string, 245 contextButtons: m.Child[], 246 resp?: QueryResponse, 247 ) { 248 return [ 249 contextButtons, 250 m(Button, { 251 label: 'Copy query', 252 onclick: () => { 253 copyToClipboard(query); 254 }, 255 }), 256 resp && 257 resp.error === undefined && 258 m(Button, { 259 label: 'Copy result (.tsv)', 260 onclick: () => { 261 queryResponseToClipboard(resp); 262 }, 263 }), 264 ]; 265 } 266 267 renderTableContent(resp: QueryResponse) { 268 return m( 269 '.pf-query-panel', 270 resp.statementWithOutputCount > 1 && 271 m( 272 '.pf-query-warning', 273 m( 274 Callout, 275 {icon: 'warning'}, 276 `${resp.statementWithOutputCount} out of ${resp.statementCount} `, 277 'statements returned a result. ', 278 'Only the results for the last statement are displayed.', 279 ), 280 ), 281 m(QueryTableContent, {resp}), 282 ); 283 } 284} 285