• 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 {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