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 15 16import m from 'mithril'; 17import {BigintMath} from '../base/bigint_math'; 18 19import {Actions} from '../common/actions'; 20import {QueryResponse} from '../common/queries'; 21import {ColumnType, Row} from '../common/query_result'; 22import {TPTime, tpTimeFromNanos} from '../common/time'; 23import {TPDuration} from '../common/time'; 24 25import {Anchor} from './anchor'; 26import {copyToClipboard, queryResponseToClipboard} from './clipboard'; 27import {downloadData} from './download_utils'; 28import {globals} from './globals'; 29import {Panel} from './panel'; 30import {Router} from './router'; 31import { 32 focusHorizontalRange, 33 verticalScrollToTrack, 34} from './scroll_helper'; 35import {Button} from './widgets/button'; 36 37interface QueryTableRowAttrs { 38 row: Row; 39 columns: string[]; 40} 41 42// Convert column value to number if it's a bigint or a number, otherwise throw 43function colToTimestamp(colValue: ColumnType): TPTime { 44 if (typeof colValue === 'bigint') { 45 return colValue; 46 } else if (typeof colValue === 'number') { 47 return tpTimeFromNanos(colValue); 48 } else { 49 throw Error('Value is not a number or a bigint'); 50 } 51} 52 53function colToNumber(colValue: ColumnType): number { 54 if (typeof colValue === 'bigint') { 55 return Number(colValue); 56 } else if (typeof colValue === 'number') { 57 return colValue; 58 } else { 59 throw Error('Value is not a number or a bigint'); 60 } 61} 62 63function colToDuration(colValue: ColumnType): TPDuration { 64 return colToTimestamp(colValue); 65} 66 67function clampDurationLower( 68 dur: TPDuration, lowerClamp: TPDuration): TPDuration { 69 return BigintMath.max(dur, lowerClamp); 70} 71 72class QueryTableRow implements m.ClassComponent<QueryTableRowAttrs> { 73 static columnsContainsSliceLocation(columns: string[]) { 74 const requiredColumns = ['ts', 'dur', 'track_id']; 75 for (const col of requiredColumns) { 76 if (!columns.includes(col)) return false; 77 } 78 return true; 79 } 80 81 static rowOnClickHandler( 82 event: Event, row: Row, nextTab: 'CurrentSelection'|'QueryResults') { 83 // TODO(dproy): Make click handler work from analyze page. 84 if (Router.parseUrl(window.location.href).page !== '/viewer') return; 85 // If the click bubbles up to the pan and zoom handler that will deselect 86 // the slice. 87 event.stopPropagation(); 88 89 const sliceStart = colToTimestamp(row.ts); 90 // row.dur can be negative. Clamp to 1ns. 91 const sliceDur = clampDurationLower(colToDuration(row.dur), 1n); 92 const sliceEnd = sliceStart + sliceDur; 93 const trackId = colToNumber(row.track_id); 94 const uiTrackId = globals.state.uiTrackIdByTraceTrackId[trackId]; 95 if (uiTrackId === undefined) return; 96 verticalScrollToTrack(uiTrackId, true); 97 focusHorizontalRange(sliceStart, sliceEnd); 98 99 let sliceId: number|undefined; 100 if (row.type?.toString().includes('slice')) { 101 sliceId = colToNumber(row.id); 102 } else { 103 sliceId = colToNumber(row.slice_id); 104 } 105 if (sliceId !== undefined) { 106 globals.makeSelection( 107 Actions.selectChromeSlice( 108 {id: sliceId, trackId: uiTrackId, table: 'slice'}), 109 nextTab === 'QueryResults' ? globals.state.currentTab : 110 'current_selection'); 111 } 112 } 113 114 view(vnode: m.Vnode<QueryTableRowAttrs>) { 115 const cells = []; 116 const {row, columns} = vnode.attrs; 117 for (const col of columns) { 118 const value = row[col]; 119 if (value instanceof Uint8Array) { 120 cells.push( 121 m('td', 122 m(Anchor, 123 { 124 onclick: () => downloadData(`${col}.blob`, value), 125 }, 126 `Blob (${value.length} bytes)`))); 127 } else if (typeof value === 'bigint') { 128 cells.push(m('td', value.toString())); 129 } else { 130 cells.push(m('td', value)); 131 } 132 } 133 const containsSliceLocation = 134 QueryTableRow.columnsContainsSliceLocation(columns); 135 const maybeOnClick = containsSliceLocation ? 136 (e: Event) => QueryTableRow.rowOnClickHandler(e, row, 'QueryResults') : 137 null; 138 const maybeOnDblClick = containsSliceLocation ? 139 (e: Event) => 140 QueryTableRow.rowOnClickHandler(e, row, 'CurrentSelection') : 141 null; 142 return m( 143 'tr', 144 { 145 'onclick': maybeOnClick, 146 // TODO(altimin): Consider improving the logic here (e.g. delay?) to 147 // account for cases when dblclick fires late. 148 'ondblclick': maybeOnDblClick, 149 'clickable': containsSliceLocation, 150 }, 151 cells); 152 } 153} 154 155interface QueryTableContentAttrs { 156 resp: QueryResponse; 157} 158 159class QueryTableContent implements m.ClassComponent<QueryTableContentAttrs> { 160 private previousResponse?: QueryResponse; 161 162 onbeforeupdate(vnode: m.CVnode<QueryTableContentAttrs>) { 163 return vnode.attrs.resp !== this.previousResponse; 164 } 165 166 view(vnode: m.CVnode<QueryTableContentAttrs>) { 167 const resp = vnode.attrs.resp; 168 this.previousResponse = resp; 169 const cols = []; 170 for (const col of resp.columns) { 171 cols.push(m('td', col)); 172 } 173 const tableHeader = m('tr', cols); 174 175 const rows = 176 resp.rows.map((row) => m(QueryTableRow, {row, columns: resp.columns})); 177 178 if (resp.error) { 179 return m('.query-error', `SQL error: ${resp.error}`); 180 } else { 181 return m( 182 '.query-table-container.x-scrollable', 183 m('table.query-table', m('thead', tableHeader), m('tbody', rows))); 184 } 185 } 186} 187 188interface QueryTableAttrs { 189 query: string; 190 onClose: () => void; 191 resp?: QueryResponse; 192 contextButtons?: m.Child[]; 193} 194 195export class QueryTable extends Panel<QueryTableAttrs> { 196 view(vnode: m.CVnode<QueryTableAttrs>) { 197 const resp = vnode.attrs.resp; 198 199 const header: m.Child[] = [ 200 m('span', 201 resp ? `Query result - ${Math.round(resp.durationMs)} ms` : 202 `Query - running`), 203 m('span.code.text-select', vnode.attrs.query), 204 m('span.spacer'), 205 ...(vnode.attrs.contextButtons ?? []), 206 m(Button, { 207 label: 'Copy query', 208 minimal: true, 209 onclick: () => { 210 copyToClipboard(vnode.attrs.query); 211 }, 212 }), 213 ]; 214 if (resp) { 215 if (resp.error === undefined) { 216 header.push(m(Button, { 217 label: 'Copy result (.tsv)', 218 minimal: true, 219 onclick: () => { 220 queryResponseToClipboard(resp); 221 }, 222 })); 223 } 224 } 225 header.push(m(Button, { 226 label: 'Close', 227 minimal: true, 228 onclick: () => vnode.attrs.onClose(), 229 })); 230 231 const headers = [m('header.overview', ...header)]; 232 233 if (resp === undefined) { 234 return m('div', ...headers); 235 } 236 237 if (resp.statementWithOutputCount > 1) { 238 headers.push( 239 m('header.overview', 240 `${resp.statementWithOutputCount} out of ${resp.statementCount} ` + 241 `statements returned a result. Only the results for the last ` + 242 `statement are displayed in the table below.`)); 243 } 244 245 return m('div', ...headers, m(QueryTableContent, {resp})); 246 } 247 248 renderCanvas() {} 249} 250