• 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
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