• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {QueryError} from '../common/query_result';
21import {
22  AddDebugTrackMenu,
23  uuidToViewName,
24} from '../tracks/debug/add_debug_track_menu';
25
26import {
27  addTab,
28  BottomTab,
29  bottomTabRegistry,
30  closeTab,
31  NewBottomTabArgs,
32} from './bottom_tab';
33import {globals} from './globals';
34import {QueryTable} from './query_table';
35import {Button} from './widgets/button';
36import {Popup, PopupPosition} from './widgets/popup';
37
38
39export function runQueryInNewTab(query: string, title: string, tag?: string) {
40  return addTab({
41    kind: QueryResultTab.kind,
42    tag,
43    config: {
44      query,
45      title,
46    },
47  });
48}
49
50interface QueryResultTabConfig {
51  readonly query: string;
52  readonly title: string;
53  // Optional data to display in this tab instead of fetching it again
54  // (e.g. when duplicating an existing tab which already has the data).
55  readonly prefetchedResponse?: QueryResponse;
56}
57
58export class QueryResultTab extends BottomTab<QueryResultTabConfig> {
59  static readonly kind = 'org.perfetto.QueryResultTab';
60
61  queryResponse?: QueryResponse;
62  sqlViewName?: string;
63
64  static create(args: NewBottomTabArgs): QueryResultTab {
65    return new QueryResultTab(args);
66  }
67
68  constructor(args: NewBottomTabArgs) {
69    super(args);
70
71    this.initTrack(args);
72  }
73
74  async initTrack(args: NewBottomTabArgs) {
75    let uuid = '';
76    if (this.config.prefetchedResponse !== undefined) {
77      this.queryResponse = this.config.prefetchedResponse;
78      uuid = args.uuid;
79    } else {
80      const result = await runQuery(this.config.query, this.engine);
81      this.queryResponse = result;
82      globals.rafScheduler.scheduleFullRedraw();
83      if (result.error !== undefined) {
84        return;
85      }
86
87      uuid = uuidv4();
88    }
89
90    if (uuid !== '') {
91      this.sqlViewName = await this.createViewForDebugTrack(uuid);
92      if (this.sqlViewName) {
93        globals.rafScheduler.scheduleFullRedraw();
94      }
95    }
96  }
97
98  getTitle(): string {
99    const suffix =
100        this.queryResponse ? ` (${this.queryResponse.rows.length})` : '';
101    return `${this.config.title}${suffix}`;
102  }
103
104  viewTab(): m.Child {
105    return m(QueryTable, {
106      query: this.config.query,
107      resp: this.queryResponse,
108      onClose: () => closeTab(this.uuid),
109      contextButtons: [
110        this.sqlViewName === undefined ?
111            null :
112            m(Popup,
113              {
114                trigger: m(Button, {label: 'Show debug track', minimal: true}),
115                position: PopupPosition.Top,
116              },
117              m(AddDebugTrackMenu, {
118                sqlViewName: this.sqlViewName,
119                columns: assertExists(this.queryResponse).columns,
120                engine: this.engine,
121              })),
122      ],
123    });
124  }
125
126  isLoading() {
127    return this.queryResponse === undefined;
128  }
129
130  renderTabCanvas() {}
131
132  async createViewForDebugTrack(uuid: string): Promise<string> {
133    const viewId = uuidToViewName(uuid);
134    // Assuming that the query results come from a SELECT query, try creating a
135    // view to allow us to reuse it for further queries.
136    // TODO(altimin): This should get the actual query that was used to
137    // generate the results from the SQL query iterator.
138    try {
139      const createViewResult = await this.engine.query(
140          `create view ${viewId} as ${this.config.query}`);
141      if (createViewResult.error()) {
142        // If it failed, do nothing.
143        return '';
144      }
145    } catch (e) {
146      if (e instanceof QueryError) {
147        // If it failed, do nothing.
148        return '';
149      }
150      throw e;
151    }
152    return viewId;
153  }
154}
155
156bottomTabRegistry.register(QueryResultTab);
157