1// Copyright (C) 2023 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'; 16import {v4 as uuidv4} from 'uuid'; 17 18import {assertExists} from '../base/logging'; 19import {QueryResponse, runQuery} from '../common/queries'; 20import {raf} from '../core/raf_scheduler'; 21import {QueryError} from '../trace_processor/query_result'; 22import { 23 AddDebugTrackMenu, 24 uuidToViewName, 25} from './debug_tracks/add_debug_track_menu'; 26import {Button} from '../widgets/button'; 27import {PopupMenu2} from '../widgets/menu'; 28import {PopupPosition} from '../widgets/popup'; 29 30import {BottomTab, NewBottomTabArgs} from './bottom_tab'; 31import {QueryTable} from './query_table'; 32import {globals} from './globals'; 33import {Actions} from '../common/actions'; 34import {BottomTabToTabAdapter} from '../public/utils'; 35import {Engine} from '../public'; 36 37interface QueryResultTabConfig { 38 readonly query: string; 39 readonly title: string; 40 // Optional data to display in this tab instead of fetching it again 41 // (e.g. when duplicating an existing tab which already has the data). 42 readonly prefetchedResponse?: QueryResponse; 43} 44 45// External interface for adding a new query results tab 46// Automatically decided whether to add v1 or v2 tab 47export function addQueryResultsTab( 48 config: QueryResultTabConfig, 49 tag?: string, 50): void { 51 const queryResultsTab = new QueryResultTab({ 52 config, 53 engine: getEngine(), 54 uuid: uuidv4(), 55 }); 56 57 const uri = 'queryResults#' + (tag ?? uuidv4()); 58 59 globals.tabManager.registerTab({ 60 uri, 61 content: new BottomTabToTabAdapter(queryResultsTab), 62 isEphemeral: true, 63 }); 64 65 globals.dispatch(Actions.showTab({uri})); 66} 67 68// TODO(stevegolton): Find a way to make this more elegant. 69function getEngine(): Engine { 70 const engConfig = globals.getCurrentEngine(); 71 const engineId = assertExists(engConfig).id; 72 return assertExists(globals.engines.get(engineId)).getProxy('QueryResult'); 73} 74 75export class QueryResultTab extends BottomTab<QueryResultTabConfig> { 76 static readonly kind = 'dev.perfetto.QueryResultTab'; 77 78 queryResponse?: QueryResponse; 79 sqlViewName?: string; 80 81 static create(args: NewBottomTabArgs<QueryResultTabConfig>): QueryResultTab { 82 return new QueryResultTab(args); 83 } 84 85 constructor(args: NewBottomTabArgs<QueryResultTabConfig>) { 86 super(args); 87 88 this.initTrack(args); 89 } 90 91 async initTrack(args: NewBottomTabArgs<QueryResultTabConfig>) { 92 let uuid = ''; 93 if (this.config.prefetchedResponse !== undefined) { 94 this.queryResponse = this.config.prefetchedResponse; 95 uuid = args.uuid; 96 } else { 97 const result = await runQuery(this.config.query, this.engine); 98 this.queryResponse = result; 99 raf.scheduleFullRedraw(); 100 if (result.error !== undefined) { 101 return; 102 } 103 104 uuid = uuidv4(); 105 } 106 107 if (uuid !== '') { 108 this.sqlViewName = await this.createViewForDebugTrack(uuid); 109 if (this.sqlViewName) { 110 raf.scheduleFullRedraw(); 111 } 112 } 113 } 114 115 getTitle(): string { 116 const suffix = this.queryResponse 117 ? ` (${this.queryResponse.rows.length})` 118 : ''; 119 return `${this.config.title}${suffix}`; 120 } 121 122 viewTab(): m.Child { 123 return m(QueryTable, { 124 query: this.config.query, 125 resp: this.queryResponse, 126 fillParent: true, 127 contextButtons: [ 128 this.sqlViewName === undefined 129 ? null 130 : m( 131 PopupMenu2, 132 { 133 trigger: m(Button, {label: 'Show debug track'}), 134 popupPosition: PopupPosition.Top, 135 }, 136 m(AddDebugTrackMenu, { 137 dataSource: { 138 sqlSource: `select * from ${this.sqlViewName}`, 139 columns: assertExists(this.queryResponse).columns, 140 }, 141 engine: this.engine, 142 }), 143 ), 144 ], 145 }); 146 } 147 148 isLoading() { 149 return this.queryResponse === undefined; 150 } 151 152 async createViewForDebugTrack(uuid: string): Promise<string> { 153 const viewId = uuidToViewName(uuid); 154 // Assuming that the query results come from a SELECT query, try creating a 155 // view to allow us to reuse it for further queries. 156 const hasValidQueryResponse = 157 this.queryResponse && this.queryResponse.error === undefined; 158 const sqlQuery = hasValidQueryResponse 159 ? this.queryResponse!.lastStatementSql 160 : this.config.query; 161 try { 162 const createViewResult = await this.engine.query( 163 `create view ${viewId} as ${sqlQuery}`, 164 ); 165 if (createViewResult.error()) { 166 // If it failed, do nothing. 167 return ''; 168 } 169 } catch (e) { 170 if (e instanceof QueryError) { 171 // If it failed, do nothing. 172 return ''; 173 } 174 throw e; 175 } 176 return viewId; 177 } 178} 179