• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2025 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';
16
17import {PageWithTraceAttrs} from '../../../public/page';
18import {TextParagraph} from '../../../widgets/text_paragraph';
19import {QueryTable} from '../../../components/query_table/query_table';
20import {runQueryForQueryTable} from '../../../components/query_table/queries';
21import {AsyncLimiter} from '../../../base/async_limiter';
22import {QueryResponse} from '../../../components/query_table/queries';
23import {SegmentedButtons} from '../../../widgets/segmented_buttons';
24import {NodeType, QueryNode} from '../query_node';
25import {ColumnController, ColumnControllerDiff} from './column_controller';
26import {Section} from '../../../widgets/section';
27import {Engine} from '../../../trace_processor/engine';
28import protos from '../../../protos';
29import {copyToClipboard} from '../../../base/clipboard';
30import {Button} from '../../../widgets/button';
31import {Icons} from '../../../base/semantic_icons';
32
33export interface DataSourceAttrs extends PageWithTraceAttrs {
34  readonly queryNode: QueryNode;
35}
36
37enum SelectedView {
38  COLUMNS = 0,
39  SQL = 1,
40  PROTO = 2,
41}
42
43export class DataSourceViewer implements m.ClassComponent<DataSourceAttrs> {
44  private readonly tableAsyncLimiter = new AsyncLimiter();
45
46  private queryResult: QueryResponse | undefined;
47  private showDataSourceInfoPanel: number = 0;
48
49  private prevSqString?: string;
50  private curSqString?: string;
51
52  private currentSql?: Query;
53
54  view({attrs}: m.CVnode<DataSourceAttrs>) {
55    function renderPickColumns(node: QueryNode): m.Child {
56      return m(ColumnController, {
57        options: node.finalCols,
58        onChange: (diffs: ColumnControllerDiff[]) => {
59          diffs.forEach(({id, checked, alias}) => {
60            if (node.finalCols === undefined) {
61              return;
62            }
63            for (const option of node.finalCols) {
64              if (option.id === id) {
65                option.checked = checked;
66                option.alias = alias;
67              }
68            }
69          });
70        },
71      });
72    }
73
74    const renderTable = () => {
75      if (this.queryResult === undefined) {
76        return;
77      }
78      if (this.queryResult.error !== undefined) {
79        return m(TextParagraph, {text: `Error: ${this.queryResult.error}`});
80      }
81      return (
82        this.currentSql &&
83        m(QueryTable, {
84          trace: attrs.trace,
85          query: queryToRun(this.currentSql),
86          resp: this.queryResult,
87          fillParent: false,
88        })
89      );
90    };
91
92    const renderButtons = (): m.Child => {
93      return m(SegmentedButtons, {
94        ...attrs,
95        options: [
96          {label: 'Show columns'},
97          {label: 'Show SQL'},
98          {label: 'Show proto'},
99        ],
100        selectedOption: this.showDataSourceInfoPanel,
101        onOptionSelected: (num) => {
102          this.showDataSourceInfoPanel = num;
103        },
104      });
105    };
106
107    const sq = attrs.queryNode.getStructuredQuery();
108    if (sq === undefined) return;
109
110    this.curSqString = JSON.stringify(sq.toJSON(), null, 2);
111
112    if (this.curSqString !== this.prevSqString) {
113      this.tableAsyncLimiter.schedule(async () => {
114        this.currentSql = await analyzeNode(
115          attrs.queryNode,
116          attrs.trace.engine,
117        );
118        if (this.currentSql === undefined) {
119          return;
120        }
121        this.queryResult = await runQueryForQueryTable(
122          attrs.queryNode.type === NodeType.kSqlSource
123            ? queryToRun(this.currentSql)
124            : `${queryToRun(this.currentSql)} LIMIT 50`,
125          attrs.trace.engine,
126        );
127        this.prevSqString = this.curSqString;
128      });
129    }
130
131    if (this.currentSql === undefined) return;
132    const sql = queryToRun(this.currentSql);
133    return [
134      m(
135        Section,
136        {title: attrs.queryNode.getTitle()},
137        attrs.queryNode.getDetails(),
138        renderButtons(),
139        this.showDataSourceInfoPanel === SelectedView.SQL &&
140          m(
141            '.code-snippet',
142            m(Button, {
143              title: 'Copy to clipboard',
144              onclick: () => copyToClipboard(sql ?? ''),
145              icon: Icons.Copy,
146            }),
147            m('code', sql),
148          ),
149        this.showDataSourceInfoPanel === SelectedView.COLUMNS &&
150          renderPickColumns(attrs.queryNode),
151        this.showDataSourceInfoPanel === SelectedView.PROTO &&
152          m(
153            '.code-snippet',
154            m(Button, {
155              title: 'Copy to clipboard',
156              onclick: () => copyToClipboard(this.currentSql?.textproto ?? ''),
157              icon: Icons.Copy,
158            }),
159            m('code', this.currentSql.textproto),
160          ),
161      ),
162      m(Section, {title: 'Sample data'}, renderTable()),
163    ];
164  }
165}
166
167function getStructuredQueries(
168  finalNode: QueryNode,
169): protos.PerfettoSqlStructuredQuery[] | undefined {
170  if (finalNode.finalCols === undefined) {
171    return;
172  }
173  const revStructuredQueries: protos.PerfettoSqlStructuredQuery[] = [];
174  let curNode: QueryNode | undefined = finalNode;
175  while (curNode) {
176    const curSq = curNode.getStructuredQuery();
177    if (curSq === undefined) {
178      return;
179    }
180    revStructuredQueries.push(curSq);
181    if (curNode.prevNode && !curNode.prevNode.validate()) {
182      return;
183    }
184    curNode = curNode.prevNode;
185  }
186  return revStructuredQueries.reverse();
187}
188
189export interface Query {
190  sql: string;
191  textproto: string;
192  modules: string[];
193  preambles: string[];
194}
195
196export function queryToRun(sql: Query): string {
197  const includes = sql.modules.map((c) => `INCLUDE PERFETTO MODULE ${c};\n`);
198  return includes + sql.sql;
199}
200
201export async function analyzeNode(
202  node: QueryNode,
203  engine: Engine,
204): Promise<Query | undefined> {
205  const structuredQueries = getStructuredQueries(node);
206  if (structuredQueries === undefined) return;
207
208  const res = await engine.analyzeStructuredQuery(structuredQueries);
209
210  if (res.error) throw Error(res.error);
211  if (res.results.length === 0) throw Error('No structured query results');
212  if (res.results.length !== structuredQueries.length) {
213    throw Error(
214      `Wrong structured query results. Asked for ${structuredQueries.length}, received ${res.results.length}`,
215    );
216  }
217
218  const lastRes = res.results[res.results.length - 1];
219  if (lastRes.sql === null || lastRes.sql === undefined) {
220    return;
221  }
222  if (!lastRes.textproto) {
223    throw Error('No textproto in structured query results');
224  }
225
226  const sql: Query = {
227    sql: lastRes.sql,
228    textproto: lastRes.textproto ?? '',
229    modules: lastRes.modules ?? [],
230    preambles: lastRes.preambles ?? [],
231  };
232  return sql;
233}
234