• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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