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 * as m from 'mithril'; 17 18import {Actions} from '../common/actions'; 19import {Row} from '../common/protos'; 20import {QueryResponse} from '../common/queries'; 21import {fromNs} from '../common/time'; 22 23import {copyToClipboard} from './clipboard'; 24import {globals} from './globals'; 25import {Panel} from './panel'; 26import { 27 findUiTrackId, 28 horizontalScrollAndZoomToRange, 29 verticalScrollToTrack 30} from './scroll_helper'; 31 32interface QueryTableRowAttrs { 33 row: Row; 34 columns: string[]; 35} 36 37class QueryTableRow implements m.ClassComponent<QueryTableRowAttrs> { 38 static columnsContainsSliceLocation(columns: string[]) { 39 const requiredColumns = ['ts', 'dur', 'track_id']; 40 for (const col of requiredColumns) { 41 if (!columns.includes(col)) return false; 42 } 43 return true; 44 } 45 46 static rowOnClickHandler( 47 event: Event, row: Row, nextTab: 'CurrentSelection'|'QueryResults') { 48 // TODO(dproy): Make click handler work from analyze page. 49 if (globals.state.route !== '/viewer') return; 50 // If the click bubbles up to the pan and zoom handler that will deselect 51 // the slice. 52 event.stopPropagation(); 53 54 const sliceStart = fromNs(row.ts as number); 55 // row.dur can be negative. Clamp to 1ns. 56 const sliceDur = fromNs(Math.max(row.dur as number, 1)); 57 const sliceEnd = sliceStart + sliceDur; 58 const trackId = row.track_id as number; 59 const uiTrackId = findUiTrackId(trackId); 60 if (uiTrackId === null) return; 61 verticalScrollToTrack(uiTrackId, true); 62 horizontalScrollAndZoomToRange(sliceStart, sliceEnd); 63 let sliceId: number|undefined; 64 if (row.type?.toString().includes('slice')) { 65 sliceId = row.id as number | undefined; 66 } else { 67 sliceId = row.slice_id as number | undefined; 68 } 69 if (sliceId !== undefined) { 70 globals.makeSelection( 71 Actions.selectChromeSlice( 72 {id: sliceId, trackId: uiTrackId, table: 'slice'}), 73 nextTab === 'QueryResults' ? globals.frontendLocalState.currentTab : 74 'current_selection'); 75 } 76 } 77 78 view(vnode: m.Vnode<QueryTableRowAttrs>) { 79 const cells = []; 80 const {row, columns} = vnode.attrs; 81 for (const col of columns) { 82 cells.push(m('td', row[col])); 83 } 84 const containsSliceLocation = 85 QueryTableRow.columnsContainsSliceLocation(columns); 86 const maybeOnClick = containsSliceLocation ? 87 (e: Event) => QueryTableRow.rowOnClickHandler(e, row, 'QueryResults') : 88 null; 89 const maybeOnDblClick = containsSliceLocation ? 90 (e: Event) => 91 QueryTableRow.rowOnClickHandler(e, row, 'CurrentSelection') : 92 null; 93 return m( 94 'tr', 95 { 96 onclick: maybeOnClick, 97 // TODO(altimin): Consider improving the logic here (e.g. delay?) to 98 // account for cases when dblclick fires late. 99 ondblclick: maybeOnDblClick, 100 'clickable': containsSliceLocation 101 }, 102 cells); 103 } 104} 105 106interface QueryTableAttrs { 107 queryId: string; 108} 109 110export class QueryTable extends Panel<QueryTableAttrs> { 111 private previousResponse?: QueryResponse; 112 113 onbeforeupdate(vnode: m.CVnode<QueryTableAttrs>) { 114 const {queryId} = vnode.attrs; 115 const resp = globals.queryResults.get(queryId) as QueryResponse; 116 const res = resp !== this.previousResponse; 117 return res; 118 } 119 120 view(vnode: m.CVnode<QueryTableAttrs>) { 121 const {queryId} = vnode.attrs; 122 const resp = globals.queryResults.get(queryId) as QueryResponse; 123 if (resp === undefined) { 124 return m(''); 125 } 126 this.previousResponse = resp; 127 const cols = []; 128 for (const col of resp.columns) { 129 cols.push(m('td', col)); 130 } 131 const header = m('tr', cols); 132 133 const rows = []; 134 for (let i = 0; i < resp.rows.length; i++) { 135 rows.push(m(QueryTableRow, {row: resp.rows[i], columns: resp.columns})); 136 } 137 138 return m( 139 'div', 140 m( 141 'header.overview', 142 `Query result - ${Math.round(resp.durationMs)} ms`, 143 m('span.code', resp.query), 144 resp.error ? 145 null : 146 m('button.query-ctrl', 147 { 148 onclick: () => { 149 const lines: string[][] = []; 150 lines.push(resp.columns); 151 for (const row of resp.rows) { 152 const line = []; 153 for (const col of resp.columns) { 154 line.push(row[col].toString()); 155 } 156 lines.push(line); 157 } 158 copyToClipboard( 159 lines.map(line => line.join('\t')).join('\n')); 160 }, 161 }, 162 'Copy as .tsv'), 163 m('button.query-ctrl', 164 { 165 onclick: () => { 166 globals.queryResults.delete(queryId); 167 globals.rafScheduler.scheduleFullRedraw(); 168 } 169 }, 170 'Close'), 171 ), 172 resp.error ? 173 m('.query-error', `SQL error: ${resp.error}`) : 174 m('.query-table-container', 175 m('table.query-table', m('thead', header), m('tbody', rows)))); 176 } 177 178 renderCanvas() {} 179} 180