Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 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 | |
Steve Golton | 5038846 | 2023-04-05 16:14:38 +0100 | [diff] [blame] | 15 | import m from 'mithril'; |
Alexander Timin | 2369c4c | 2023-03-21 17:20:46 +0000 | [diff] [blame] | 16 | import {v4 as uuidv4} from 'uuid'; |
| 17 | |
| 18 | import {assertExists} from '../base/logging'; |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 19 | import {QueryResponse, runQuery} from '../common/queries'; |
Hector Dearman | ba396db | 2023-07-03 18:25:08 +0100 | [diff] [blame] | 20 | import {raf} from '../core/raf_scheduler'; |
Hector Dearman | ea4719d | 2023-10-31 09:11:09 +0000 | [diff] [blame] | 21 | import {QueryError} from '../trace_processor/query_result'; |
Alexander Timin | 2369c4c | 2023-03-21 17:20:46 +0000 | [diff] [blame] | 22 | import { |
| 23 | AddDebugTrackMenu, |
| 24 | uuidToViewName, |
| 25 | } from '../tracks/debug/add_debug_track_menu'; |
Steve Golton | 6caf3c0 | 2023-09-07 08:09:06 +0100 | [diff] [blame] | 26 | import {Button} from '../widgets/button'; |
| 27 | import {PopupMenu2} from '../widgets/menu'; |
| 28 | import {PopupPosition} from '../widgets/popup'; |
Alexander Timin | 2369c4c | 2023-03-21 17:20:46 +0000 | [diff] [blame] | 29 | |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 30 | import { |
| 31 | addTab, |
| 32 | BottomTab, |
| 33 | bottomTabRegistry, |
| 34 | closeTab, |
| 35 | NewBottomTabArgs, |
| 36 | } from './bottom_tab'; |
Steve Golton | 17a785c | 2023-11-07 16:05:06 +0000 | [diff] [blame] | 37 | import {globals} from './globals'; |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 38 | import {QueryTable} from './query_table'; |
| 39 | |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 40 | export function runQueryInNewTab(query: string, title: string, tag?: string) { |
| 41 | return addTab({ |
| 42 | kind: QueryResultTab.kind, |
| 43 | tag, |
| 44 | config: { |
| 45 | query, |
| 46 | title, |
| 47 | }, |
| 48 | }); |
| 49 | } |
| 50 | |
Steve Golton | 17a785c | 2023-11-07 16:05:06 +0000 | [diff] [blame] | 51 | globals.registerOpenQueryHandler(runQueryInNewTab); |
| 52 | |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 53 | interface QueryResultTabConfig { |
| 54 | readonly query: string; |
| 55 | readonly title: string; |
| 56 | // Optional data to display in this tab instead of fetching it again |
| 57 | // (e.g. when duplicating an existing tab which already has the data). |
| 58 | readonly prefetchedResponse?: QueryResponse; |
| 59 | } |
| 60 | |
| 61 | export class QueryResultTab extends BottomTab<QueryResultTabConfig> { |
Hector Dearman | d3ccdc8 | 2023-06-19 14:34:14 +0100 | [diff] [blame] | 62 | static readonly kind = 'dev.perfetto.QueryResultTab'; |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 63 | |
| 64 | queryResponse?: QueryResponse; |
Alexander Timin | 2369c4c | 2023-03-21 17:20:46 +0000 | [diff] [blame] | 65 | sqlViewName?: string; |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 66 | |
| 67 | static create(args: NewBottomTabArgs): QueryResultTab { |
| 68 | return new QueryResultTab(args); |
| 69 | } |
| 70 | |
| 71 | constructor(args: NewBottomTabArgs) { |
| 72 | super(args); |
| 73 | |
Harkiran Bolaria | 3f53852 | 2023-04-19 16:18:57 +0000 | [diff] [blame] | 74 | this.initTrack(args); |
| 75 | } |
| 76 | |
| 77 | async initTrack(args: NewBottomTabArgs) { |
| 78 | let uuid = ''; |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 79 | if (this.config.prefetchedResponse !== undefined) { |
| 80 | this.queryResponse = this.config.prefetchedResponse; |
Harkiran Bolaria | 3f53852 | 2023-04-19 16:18:57 +0000 | [diff] [blame] | 81 | uuid = args.uuid; |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 82 | } else { |
Harkiran Bolaria | 3f53852 | 2023-04-19 16:18:57 +0000 | [diff] [blame] | 83 | const result = await runQuery(this.config.query, this.engine); |
| 84 | this.queryResponse = result; |
Hector Dearman | ba396db | 2023-07-03 18:25:08 +0100 | [diff] [blame] | 85 | raf.scheduleFullRedraw(); |
Harkiran Bolaria | 3f53852 | 2023-04-19 16:18:57 +0000 | [diff] [blame] | 86 | if (result.error !== undefined) { |
| 87 | return; |
| 88 | } |
Alexander Timin | 2369c4c | 2023-03-21 17:20:46 +0000 | [diff] [blame] | 89 | |
Harkiran Bolaria | 3f53852 | 2023-04-19 16:18:57 +0000 | [diff] [blame] | 90 | uuid = uuidv4(); |
| 91 | } |
Alexander Timin | 2369c4c | 2023-03-21 17:20:46 +0000 | [diff] [blame] | 92 | |
Harkiran Bolaria | 3f53852 | 2023-04-19 16:18:57 +0000 | [diff] [blame] | 93 | if (uuid !== '') { |
| 94 | this.sqlViewName = await this.createViewForDebugTrack(uuid); |
| 95 | if (this.sqlViewName) { |
Hector Dearman | ba396db | 2023-07-03 18:25:08 +0100 | [diff] [blame] | 96 | raf.scheduleFullRedraw(); |
Harkiran Bolaria | 3f53852 | 2023-04-19 16:18:57 +0000 | [diff] [blame] | 97 | } |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 98 | } |
| 99 | } |
| 100 | |
| 101 | getTitle(): string { |
| 102 | const suffix = |
| 103 | this.queryResponse ? ` (${this.queryResponse.rows.length})` : ''; |
| 104 | return `${this.config.title}${suffix}`; |
| 105 | } |
| 106 | |
Alexander Timin | 2369c4c | 2023-03-21 17:20:46 +0000 | [diff] [blame] | 107 | viewTab(): m.Child { |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 108 | return m(QueryTable, { |
| 109 | query: this.config.query, |
| 110 | resp: this.queryResponse, |
Steve Golton | c4ca612 | 2023-06-20 07:18:41 +0100 | [diff] [blame] | 111 | fillParent: true, |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 112 | onClose: () => closeTab(this.uuid), |
Alexander Timin | 2369c4c | 2023-03-21 17:20:46 +0000 | [diff] [blame] | 113 | contextButtons: [ |
| 114 | this.sqlViewName === undefined ? |
| 115 | null : |
Steve Golton | 1d61fca | 2023-07-21 07:27:21 +0100 | [diff] [blame] | 116 | m(PopupMenu2, |
Alexander Timin | 2369c4c | 2023-03-21 17:20:46 +0000 | [diff] [blame] | 117 | { |
| 118 | trigger: m(Button, {label: 'Show debug track', minimal: true}), |
Steve Golton | 1d61fca | 2023-07-21 07:27:21 +0100 | [diff] [blame] | 119 | popupPosition: PopupPosition.Top, |
Alexander Timin | 2369c4c | 2023-03-21 17:20:46 +0000 | [diff] [blame] | 120 | }, |
| 121 | m(AddDebugTrackMenu, { |
Alexander Timin | 2a32d68 | 2023-07-20 12:07:48 -0700 | [diff] [blame] | 122 | dataSource: { |
| 123 | sqlSource: `select * from ${this.sqlViewName}`, |
| 124 | columns: assertExists(this.queryResponse).columns, |
| 125 | }, |
Alexander Timin | 2369c4c | 2023-03-21 17:20:46 +0000 | [diff] [blame] | 126 | engine: this.engine, |
| 127 | })), |
| 128 | ], |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 129 | }); |
| 130 | } |
| 131 | |
Alexander Timin | 00035a0 | 2023-03-17 15:32:45 +0000 | [diff] [blame] | 132 | isLoading() { |
| 133 | return this.queryResponse === undefined; |
| 134 | } |
| 135 | |
Harkiran Bolaria | 3f53852 | 2023-04-19 16:18:57 +0000 | [diff] [blame] | 136 | async createViewForDebugTrack(uuid: string): Promise<string> { |
| 137 | const viewId = uuidToViewName(uuid); |
| 138 | // Assuming that the query results come from a SELECT query, try creating a |
| 139 | // view to allow us to reuse it for further queries. |
Igor Kraskevich | 5e87c59 | 2023-06-22 08:37:38 +0000 | [diff] [blame] | 140 | const hasValidQueryResponse = |
| 141 | this.queryResponse && this.queryResponse.error === undefined; |
| 142 | const sqlQuery = hasValidQueryResponse ? |
| 143 | this.queryResponse!.lastStatementSql : |
| 144 | this.config.query; |
Harkiran Bolaria | 3f53852 | 2023-04-19 16:18:57 +0000 | [diff] [blame] | 145 | try { |
Igor Kraskevich | 5e87c59 | 2023-06-22 08:37:38 +0000 | [diff] [blame] | 146 | const createViewResult = |
| 147 | await this.engine.query(`create view ${viewId} as ${sqlQuery}`); |
Harkiran Bolaria | 3f53852 | 2023-04-19 16:18:57 +0000 | [diff] [blame] | 148 | if (createViewResult.error()) { |
| 149 | // If it failed, do nothing. |
| 150 | return ''; |
| 151 | } |
| 152 | } catch (e) { |
| 153 | if (e instanceof QueryError) { |
| 154 | // If it failed, do nothing. |
| 155 | return ''; |
| 156 | } |
| 157 | throw e; |
| 158 | } |
| 159 | return viewId; |
| 160 | } |
Alexander Timin | 833fbe1 | 2023-03-17 15:19:31 +0000 | [diff] [blame] | 161 | } |
| 162 | |
| 163 | bottomTabRegistry.register(QueryResultTab); |