blob: 23b51487a7c17306dd58ab6d14defe6f994ecbb3 [file] [log] [blame]
// Copyright (C) 2023 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 {v4 as uuidv4} from 'uuid';
import {assertExists} from '../base/logging';
import {QueryResponse, runQuery} from '../common/queries';
import {raf} from '../core/raf_scheduler';
import {QueryError} from '../trace_processor/query_result';
import {
AddDebugTrackMenu,
uuidToViewName,
} from '../tracks/debug/add_debug_track_menu';
import {Button} from '../widgets/button';
import {PopupMenu2} from '../widgets/menu';
import {PopupPosition} from '../widgets/popup';
import {BottomTab, NewBottomTabArgs} from './bottom_tab';
import {QueryTable} from './query_table';
import {globals} from './globals';
import {Actions} from '../common/actions';
import {BottomTabToTabAdapter} from '../public/utils';
import {EngineProxy} from '../public';
interface QueryResultTabConfig {
readonly query: string;
readonly title: string;
// Optional data to display in this tab instead of fetching it again
// (e.g. when duplicating an existing tab which already has the data).
readonly prefetchedResponse?: QueryResponse;
}
// External interface for adding a new query results tab
// Automatically decided whether to add v1 or v2 tab
export function addQueryResultsTab(
config: QueryResultTabConfig,
tag?: string,
): void {
const queryResultsTab = new QueryResultTab({
config,
engine: getEngine(),
uuid: uuidv4(),
});
const uri = 'queryResults#' + (tag ?? uuidv4());
globals.tabManager.registerTab({
uri,
content: new BottomTabToTabAdapter(queryResultsTab),
isEphemeral: true,
});
globals.dispatch(Actions.showTab({uri}));
}
// TODO(stevegolton): Find a way to make this more elegant.
function getEngine(): EngineProxy {
const engConfig = globals.getCurrentEngine();
const engineId = assertExists(engConfig).id;
return assertExists(globals.engines.get(engineId)).getProxy('QueryResult');
}
export class QueryResultTab extends BottomTab<QueryResultTabConfig> {
static readonly kind = 'dev.perfetto.QueryResultTab';
queryResponse?: QueryResponse;
sqlViewName?: string;
static create(args: NewBottomTabArgs<QueryResultTabConfig>): QueryResultTab {
return new QueryResultTab(args);
}
constructor(args: NewBottomTabArgs<QueryResultTabConfig>) {
super(args);
this.initTrack(args);
}
async initTrack(args: NewBottomTabArgs<QueryResultTabConfig>) {
let uuid = '';
if (this.config.prefetchedResponse !== undefined) {
this.queryResponse = this.config.prefetchedResponse;
uuid = args.uuid;
} else {
const result = await runQuery(this.config.query, this.engine);
this.queryResponse = result;
raf.scheduleFullRedraw();
if (result.error !== undefined) {
return;
}
uuid = uuidv4();
}
if (uuid !== '') {
this.sqlViewName = await this.createViewForDebugTrack(uuid);
if (this.sqlViewName) {
raf.scheduleFullRedraw();
}
}
}
getTitle(): string {
const suffix = this.queryResponse
? ` (${this.queryResponse.rows.length})`
: '';
return `${this.config.title}${suffix}`;
}
viewTab(): m.Child {
return m(QueryTable, {
query: this.config.query,
resp: this.queryResponse,
fillParent: true,
contextButtons: [
this.sqlViewName === undefined
? null
: m(
PopupMenu2,
{
trigger: m(Button, {label: 'Show debug track'}),
popupPosition: PopupPosition.Top,
},
m(AddDebugTrackMenu, {
dataSource: {
sqlSource: `select * from ${this.sqlViewName}`,
columns: assertExists(this.queryResponse).columns,
},
engine: this.engine,
}),
),
],
});
}
isLoading() {
return this.queryResponse === undefined;
}
async createViewForDebugTrack(uuid: string): Promise<string> {
const viewId = uuidToViewName(uuid);
// Assuming that the query results come from a SELECT query, try creating a
// view to allow us to reuse it for further queries.
const hasValidQueryResponse =
this.queryResponse && this.queryResponse.error === undefined;
const sqlQuery = hasValidQueryResponse
? this.queryResponse!.lastStatementSql
: this.config.query;
try {
const createViewResult = await this.engine.query(
`create view ${viewId} as ${sqlQuery}`,
);
if (createViewResult.error()) {
// If it failed, do nothing.
return '';
}
} catch (e) {
if (e instanceof QueryError) {
// If it failed, do nothing.
return '';
}
throw e;
}
return viewId;
}
}