xref: /aosp_15_r20/external/perfetto/ui/src/components/query_table/query_result_tab.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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