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'; 17import {assertExists} from '../../base/logging'; 18import {QueryResponse, runQuery} from './queries'; 19import {QueryError} from '../../trace_processor/query_result'; 20import {AddDebugTrackMenu} from '../tracks/add_debug_track_menu'; 21import {Button} from '../../widgets/button'; 22import {PopupMenu2} from '../../widgets/menu'; 23import {PopupPosition} from '../../widgets/popup'; 24import {QueryTable} from './query_table'; 25import {Trace} from '../../public/trace'; 26import {Tab} from '../../public/tab'; 27 28interface QueryResultTabConfig { 29 readonly query: string; 30 readonly title: string; 31 // Optional data to display in this tab instead of fetching it again 32 // (e.g. when duplicating an existing tab which already has the data). 33 readonly prefetchedResponse?: QueryResponse; 34} 35 36// External interface for adding a new query results tab 37// Automatically decided whether to add v1 or v2 tab 38export function addQueryResultsTab( 39 trace: Trace, 40 config: QueryResultTabConfig, 41 tag?: string, 42): void { 43 const queryResultsTab = new QueryResultTab(trace, config); 44 45 const uri = 'queryResults#' + (tag ?? uuidv4()); 46 47 trace.tabs.registerTab({ 48 uri, 49 content: queryResultsTab, 50 isEphemeral: true, 51 }); 52 trace.tabs.showTab(uri); 53} 54 55export class QueryResultTab implements Tab { 56 private queryResponse?: QueryResponse; 57 private sqlViewName?: string; 58 59 constructor( 60 private readonly trace: Trace, 61 private readonly args: QueryResultTabConfig, 62 ) { 63 this.initTrack(); 64 } 65 66 private async initTrack() { 67 if (this.args.prefetchedResponse !== undefined) { 68 this.queryResponse = this.args.prefetchedResponse; 69 } else { 70 const result = await runQuery(this.args.query, this.trace.engine); 71 this.queryResponse = result; 72 if (result.error !== undefined) { 73 return; 74 } 75 } 76 77 // TODO(stevegolton): Do we really need to create this view upfront? 78 this.sqlViewName = await this.createViewForDebugTrack(uuidv4()); 79 if (this.sqlViewName) { 80 this.trace.scheduleFullRedraw(); 81 } 82 } 83 84 getTitle(): string { 85 const suffix = this.queryResponse 86 ? ` (${this.queryResponse.rows.length})` 87 : ''; 88 return `${this.args.title}${suffix}`; 89 } 90 91 render(): m.Children { 92 return m(QueryTable, { 93 trace: this.trace, 94 query: this.args.query, 95 resp: this.queryResponse, 96 fillParent: true, 97 contextButtons: [ 98 this.sqlViewName === undefined 99 ? null 100 : m( 101 PopupMenu2, 102 { 103 trigger: m(Button, {label: 'Show debug track'}), 104 popupPosition: PopupPosition.Top, 105 }, 106 m(AddDebugTrackMenu, { 107 trace: this.trace, 108 dataSource: { 109 sqlSource: `select * from ${this.sqlViewName}`, 110 columns: assertExists(this.queryResponse).columns, 111 }, 112 }), 113 ), 114 ], 115 }); 116 } 117 118 isLoading() { 119 return this.queryResponse === undefined; 120 } 121 122 async createViewForDebugTrack(uuid: string): Promise<string> { 123 const viewId = uuidToViewName(uuid); 124 // Assuming that the query results come from a SELECT query, try creating a 125 // view to allow us to reuse it for further queries. 126 const hasValidQueryResponse = 127 this.queryResponse && this.queryResponse.error === undefined; 128 const sqlQuery = hasValidQueryResponse 129 ? this.queryResponse!.lastStatementSql 130 : this.args.query; 131 try { 132 const createViewResult = await this.trace.engine.query( 133 `create view ${viewId} as ${sqlQuery}`, 134 ); 135 if (createViewResult.error()) { 136 // If it failed, do nothing. 137 return ''; 138 } 139 } catch (e) { 140 if (e instanceof QueryError) { 141 // If it failed, do nothing. 142 return ''; 143 } 144 throw e; 145 } 146 return viewId; 147 } 148} 149 150export function uuidToViewName(uuid: string): string { 151 return `view_${uuid.split('-').join('_')}`; 152} 153