| // Copyright (C) 2020 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import m from 'mithril'; |
| |
| import {BigintMath} from '../base/bigint_math'; |
| import {copyToClipboard} from '../base/clipboard'; |
| import {isString} from '../base/object_utils'; |
| import {Time} from '../base/time'; |
| import {Actions} from '../common/actions'; |
| import {QueryResponse} from '../common/queries'; |
| import {Row} from '../trace_processor/query_result'; |
| import {Anchor} from '../widgets/anchor'; |
| import {Button} from '../widgets/button'; |
| import {Callout} from '../widgets/callout'; |
| import {DetailsShell} from '../widgets/details_shell'; |
| |
| import {queryResponseToClipboard} from './clipboard'; |
| import {downloadData} from './download_utils'; |
| import {globals} from './globals'; |
| import {Router} from './router'; |
| import {reveal} from './scroll_helper'; |
| |
| interface QueryTableRowAttrs { |
| row: Row; |
| columns: string[]; |
| } |
| |
| type Numeric = bigint | number; |
| |
| function isIntegral(x: Row[string]): x is Numeric { |
| return ( |
| typeof x === 'bigint' || (typeof x === 'number' && Number.isInteger(x)) |
| ); |
| } |
| |
| function hasTs(row: Row): row is Row & {ts: Numeric} { |
| return 'ts' in row && isIntegral(row.ts); |
| } |
| |
| function hasDur(row: Row): row is Row & {dur: Numeric} { |
| return 'dur' in row && isIntegral(row.dur); |
| } |
| |
| function hasTrackId(row: Row): row is Row & {track_id: Numeric} { |
| return 'track_id' in row && isIntegral(row.track_id); |
| } |
| |
| function hasType(row: Row): row is Row & {type: string} { |
| return 'type' in row && isString(row.type); |
| } |
| |
| function hasId(row: Row): row is Row & {id: Numeric} { |
| return 'id' in row && isIntegral(row.id); |
| } |
| |
| function hasSliceId(row: Row): row is Row & {slice_id: Numeric} { |
| return 'slice_id' in row && isIntegral(row.slice_id); |
| } |
| |
| // These are properties that a row should have in order to be "slice-like", |
| // insofar as it represents a time range and a track id which can be revealed |
| // or zoomed-into on the timeline. |
| type Sliceish = { |
| ts: Numeric; |
| dur: Numeric; |
| track_id: Numeric; |
| }; |
| |
| export function isSliceish(row: Row): row is Row & Sliceish { |
| return hasTs(row) && hasDur(row) && hasTrackId(row); |
| } |
| |
| // Attempts to extract a slice ID from a row, or undefined if none can be found |
| export function getSliceId(row: Row): number | undefined { |
| if (hasType(row) && row.type.includes('slice')) { |
| if (hasId(row)) { |
| return Number(row.id); |
| } |
| } else { |
| if (hasSliceId(row)) { |
| return Number(row.slice_id); |
| } |
| } |
| return undefined; |
| } |
| |
| class QueryTableRow implements m.ClassComponent<QueryTableRowAttrs> { |
| view(vnode: m.Vnode<QueryTableRowAttrs>) { |
| const {row, columns} = vnode.attrs; |
| const cells = columns.map((col) => this.renderCell(col, row[col])); |
| |
| // TODO(dproy): Make click handler work from analyze page. |
| if ( |
| Router.parseUrl(window.location.href).page === '/viewer' && |
| isSliceish(row) |
| ) { |
| return m( |
| 'tr', |
| { |
| onclick: () => this.selectAndRevealSlice(row, false), |
| // TODO(altimin): Consider improving the logic here (e.g. delay?) to |
| // account for cases when dblclick fires late. |
| ondblclick: () => this.selectAndRevealSlice(row, true), |
| clickable: true, |
| title: 'Go to slice', |
| }, |
| cells, |
| ); |
| } else { |
| return m('tr', cells); |
| } |
| } |
| |
| private renderCell(name: string, value: Row[string]) { |
| if (value instanceof Uint8Array) { |
| return m('td', this.renderBlob(name, value)); |
| } else { |
| return m('td', `${value}`); |
| } |
| } |
| |
| private renderBlob(name: string, value: Uint8Array) { |
| return m( |
| Anchor, |
| { |
| onclick: () => downloadData(`${name}.blob`, value), |
| }, |
| `Blob (${value.length} bytes)`, |
| ); |
| } |
| |
| private selectAndRevealSlice( |
| row: Row & Sliceish, |
| switchToCurrentSelectionTab: boolean, |
| ) { |
| const trackId = Number(row.track_id); |
| const sliceStart = Time.fromRaw(BigInt(row.ts)); |
| // row.dur can be negative. Clamp to 1ns. |
| const sliceDur = BigintMath.max(BigInt(row.dur), 1n); |
| const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId); |
| if (trackKey !== undefined) { |
| reveal(trackKey, sliceStart, Time.add(sliceStart, sliceDur), true); |
| const sliceId = getSliceId(row); |
| if (sliceId !== undefined) { |
| this.selectSlice(sliceId, trackKey, switchToCurrentSelectionTab); |
| } |
| } |
| } |
| |
| private selectSlice( |
| sliceId: number, |
| trackKey: string, |
| switchToCurrentSelectionTab: boolean, |
| ) { |
| const action = Actions.selectChromeSlice({ |
| id: sliceId, |
| trackKey, |
| table: 'slice', |
| }); |
| globals.makeSelection(action, {switchToCurrentSelectionTab}); |
| } |
| } |
| |
| interface QueryTableContentAttrs { |
| resp: QueryResponse; |
| } |
| |
| class QueryTableContent implements m.ClassComponent<QueryTableContentAttrs> { |
| private previousResponse?: QueryResponse; |
| |
| onbeforeupdate(vnode: m.CVnode<QueryTableContentAttrs>) { |
| return vnode.attrs.resp !== this.previousResponse; |
| } |
| |
| view(vnode: m.CVnode<QueryTableContentAttrs>) { |
| const resp = vnode.attrs.resp; |
| this.previousResponse = resp; |
| const cols = []; |
| for (const col of resp.columns) { |
| cols.push(m('td', col)); |
| } |
| const tableHeader = m('tr', cols); |
| |
| const rows = resp.rows.map((row) => |
| m(QueryTableRow, {row, columns: resp.columns}), |
| ); |
| |
| if (resp.error) { |
| return m('.query-error', `SQL error: ${resp.error}`); |
| } else { |
| return m( |
| 'table.pf-query-table', |
| m('thead', tableHeader), |
| m('tbody', rows), |
| ); |
| } |
| } |
| } |
| |
| interface QueryTableAttrs { |
| query: string; |
| resp?: QueryResponse; |
| contextButtons?: m.Child[]; |
| fillParent: boolean; |
| } |
| |
| export class QueryTable implements m.ClassComponent<QueryTableAttrs> { |
| view({attrs}: m.CVnode<QueryTableAttrs>) { |
| const {resp, query, contextButtons = [], fillParent} = attrs; |
| |
| return m( |
| DetailsShell, |
| { |
| title: this.renderTitle(resp), |
| description: query, |
| buttons: this.renderButtons(query, contextButtons, resp), |
| fillParent, |
| }, |
| resp && this.renderTableContent(resp), |
| ); |
| } |
| |
| renderTitle(resp?: QueryResponse) { |
| if (!resp) { |
| return 'Query - running'; |
| } |
| const result = resp.error ? 'error' : `${resp.rows.length} rows`; |
| return `Query result (${result}) - ${resp.durationMs.toLocaleString()}ms`; |
| } |
| |
| renderButtons( |
| query: string, |
| contextButtons: m.Child[], |
| resp?: QueryResponse, |
| ) { |
| return [ |
| contextButtons, |
| m(Button, { |
| label: 'Copy query', |
| onclick: () => { |
| copyToClipboard(query); |
| }, |
| }), |
| resp && |
| resp.error === undefined && |
| m(Button, { |
| label: 'Copy result (.tsv)', |
| onclick: () => { |
| queryResponseToClipboard(resp); |
| }, |
| }), |
| ]; |
| } |
| |
| renderTableContent(resp: QueryResponse) { |
| return m( |
| '.pf-query-panel', |
| resp.statementWithOutputCount > 1 && |
| m( |
| '.pf-query-warning', |
| m( |
| Callout, |
| {icon: 'warning'}, |
| `${resp.statementWithOutputCount} out of ${resp.statementCount} `, |
| 'statements returned a result. ', |
| 'Only the results for the last statement are displayed.', |
| ), |
| ), |
| m(QueryTableContent, {resp}), |
| ); |
| } |
| } |