blob: e9bbce9e92f9b48b16f4f88e9b394caa67305aa8 [file] [log] [blame]
// 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}),
);
}
}