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