1// Copyright (C) 2024 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 {AsyncLimiter} from '../base/async_limiter'; 17import {AsyncDisposableStack} from '../base/disposable_stack'; 18import {assertExists} from '../base/logging'; 19import {Monitor} from '../base/monitor'; 20import {uuidv4Sql} from '../base/uuid'; 21import {Engine} from '../trace_processor/engine'; 22import { 23 createPerfettoIndex, 24 createPerfettoTable, 25} from '../trace_processor/sql_utils'; 26import { 27 NUM, 28 NUM_NULL, 29 STR, 30 STR_NULL, 31 UNKNOWN, 32} from '../trace_processor/query_result'; 33import { 34 Flamegraph, 35 FlamegraphQueryData, 36 FlamegraphState, 37 FlamegraphView, 38} from '../widgets/flamegraph'; 39import {Trace} from '../public/trace'; 40 41export interface QueryFlamegraphColumn { 42 // The name of the column in SQL. 43 readonly name: string; 44 45 // The human readable name describing the contents of the column. 46 readonly displayName: string; 47} 48 49export interface AggQueryFlamegraphColumn extends QueryFlamegraphColumn { 50 // The aggregation to be run when nodes are merged together in the flamegraph. 51 // 52 // TODO(lalitm): consider adding extra functions here (e.g. a top 5 or similar). 53 readonly mergeAggregation: 'ONE_OR_NULL' | 'SUM'; 54} 55 56export interface QueryFlamegraphMetric { 57 // The human readable name of the metric: will be shown to the user to change 58 // between metrics. 59 readonly name: string; 60 61 // The human readable SI-style unit of `selfValue`. Values will be shown to 62 // the user suffixed with this. 63 readonly unit: string; 64 65 // SQL statement which need to be run in preparation for being able to execute 66 // `statement`. 67 readonly dependencySql?: string; 68 69 // A single SQL statement which returns the columns `id`, `parentId`, `name` 70 // `selfValue`, all columns specified by `unaggregatableProperties` and 71 // `aggregatableProperties`. 72 readonly statement: string; 73 74 // Additional contextual columns containing data which should not be merged 75 // between sibling nodes, even if they have the same name. 76 // 77 // Examples include the mapping that a name comes from, the heap graph root 78 // type etc. 79 // 80 // Note: the name is always unaggregatable and should not be specified here. 81 readonly unaggregatableProperties?: ReadonlyArray<QueryFlamegraphColumn>; 82 83 // Additional contextual columns containing data which will be displayed to 84 // the user if there is no merging. If there is merging, currently the value 85 // will not be shown. 86 // 87 // Examples include the source file and line number. 88 readonly aggregatableProperties?: ReadonlyArray<AggQueryFlamegraphColumn>; 89} 90 91export interface QueryFlamegraphState { 92 state: FlamegraphState; 93} 94 95// Given a table and columns on those table (corresponding to metrics), 96// returns an array of `QueryFlamegraphMetric` structs which can be passed 97// in QueryFlamegraph's attrs. 98// 99// `tableOrSubquery` should have the columns `id`, `parentId`, `name` and all 100// columns specified by `tableMetrics[].name`, `unaggregatableProperties` and 101// `aggregatableProperties`. 102export function metricsFromTableOrSubquery( 103 tableOrSubquery: string, 104 tableMetrics: ReadonlyArray<{name: string; unit: string; columnName: string}>, 105 dependencySql?: string, 106 unaggregatableProperties?: ReadonlyArray<QueryFlamegraphColumn>, 107 aggregatableProperties?: ReadonlyArray<AggQueryFlamegraphColumn>, 108): QueryFlamegraphMetric[] { 109 const metrics = []; 110 for (const {name, unit, columnName} of tableMetrics) { 111 metrics.push({ 112 name, 113 unit, 114 dependencySql, 115 statement: ` 116 select *, ${columnName} as value 117 from ${tableOrSubquery} 118 `, 119 unaggregatableProperties, 120 aggregatableProperties, 121 }); 122 } 123 return metrics; 124} 125 126// A Perfetto UI component which wraps the `Flamegraph` widget and fetches the 127// data for the widget by querying an `Engine`. 128export class QueryFlamegraph { 129 private data?: FlamegraphQueryData; 130 private readonly selMonitor = new Monitor([() => this.state.state]); 131 private readonly queryLimiter = new AsyncLimiter(); 132 133 constructor( 134 private readonly trace: Trace, 135 private readonly metrics: ReadonlyArray<QueryFlamegraphMetric>, 136 private state: QueryFlamegraphState, 137 ) {} 138 139 render() { 140 if (this.selMonitor.ifStateChanged()) { 141 const metric = assertExists( 142 this.metrics.find( 143 (x) => this.state.state.selectedMetricName === x.name, 144 ), 145 ); 146 const engine = this.trace.engine; 147 const state = this.state; 148 this.data = undefined; 149 this.queryLimiter.schedule(async () => { 150 this.data = undefined; 151 this.data = await computeFlamegraphTree(engine, metric, state.state); 152 }); 153 } 154 return m(Flamegraph, { 155 metrics: this.metrics, 156 data: this.data, 157 state: this.state.state, 158 onStateChange: (state) => { 159 this.state.state = state; 160 this.trace.scheduleFullRedraw(); 161 }, 162 }); 163 } 164} 165 166async function computeFlamegraphTree( 167 engine: Engine, 168 { 169 dependencySql, 170 statement, 171 unaggregatableProperties, 172 aggregatableProperties, 173 }: QueryFlamegraphMetric, 174 {filters, view}: FlamegraphState, 175): Promise<FlamegraphQueryData> { 176 const showStack = filters 177 .filter((x) => x.kind === 'SHOW_STACK') 178 .map((x) => x.filter); 179 const hideStack = filters 180 .filter((x) => x.kind === 'HIDE_STACK') 181 .map((x) => x.filter); 182 const showFromFrame = filters 183 .filter((x) => x.kind === 'SHOW_FROM_FRAME') 184 .map((x) => x.filter); 185 const hideFrame = filters 186 .filter((x) => x.kind === 'HIDE_FRAME') 187 .map((x) => x.filter); 188 189 // Pivot also essentially acts as a "show stack" filter so treat it like one. 190 const showStackAndPivot = [...showStack]; 191 if (view.kind === 'PIVOT') { 192 showStackAndPivot.push(view.pivot); 193 } 194 195 const showStackFilter = 196 showStackAndPivot.length === 0 197 ? '0' 198 : showStackAndPivot 199 .map( 200 (x, i) => `((name like '${makeSqlFilter(x)}' escape '\\') << ${i})`, 201 ) 202 .join(' | '); 203 const showStackBits = (1 << showStackAndPivot.length) - 1; 204 205 const hideStackFilter = 206 hideStack.length === 0 207 ? 'false' 208 : hideStack 209 .map((x) => `name like '${makeSqlFilter(x)}' escape '\\'`) 210 .join(' OR '); 211 212 const showFromFrameFilter = 213 showFromFrame.length === 0 214 ? '0' 215 : showFromFrame 216 .map( 217 (x, i) => `((name like '${makeSqlFilter(x)}' escape '\\') << ${i})`, 218 ) 219 .join(' | '); 220 const showFromFrameBits = (1 << showFromFrame.length) - 1; 221 222 const hideFrameFilter = 223 hideFrame.length === 0 224 ? 'false' 225 : hideFrame 226 .map((x) => `name like '${makeSqlFilter(x)}' escape '\\'`) 227 .join(' OR '); 228 229 const pivotFilter = getPivotFilter(view); 230 231 const unagg = unaggregatableProperties ?? []; 232 const unaggCols = unagg.map((x) => x.name); 233 234 const agg = aggregatableProperties ?? []; 235 const aggCols = agg.map((x) => x.name); 236 237 const groupingColumns = `(${(unaggCols.length === 0 ? ['groupingColumn'] : unaggCols).join()})`; 238 const groupedColumns = `(${(aggCols.length === 0 ? ['groupedColumn'] : aggCols).join()})`; 239 240 if (dependencySql !== undefined) { 241 await engine.query(dependencySql); 242 } 243 await engine.query(`include perfetto module viz.flamegraph;`); 244 245 const uuid = uuidv4Sql(); 246 await using disposable = new AsyncDisposableStack(); 247 248 disposable.use( 249 await createPerfettoTable( 250 engine, 251 `_flamegraph_materialized_statement_${uuid}`, 252 statement, 253 ), 254 ); 255 disposable.use( 256 await createPerfettoIndex( 257 engine, 258 `_flamegraph_materialized_statement_${uuid}_index`, 259 `_flamegraph_materialized_statement_${uuid}(parentId)`, 260 ), 261 ); 262 263 // TODO(lalitm): this doesn't need to be called unless we have 264 // a non-empty set of filters. 265 disposable.use( 266 await createPerfettoTable( 267 engine, 268 `_flamegraph_source_${uuid}`, 269 ` 270 select * 271 from _viz_flamegraph_prepare_filter!( 272 ( 273 select 274 s.id, 275 s.parentId, 276 s.name, 277 s.value, 278 ${(unaggCols.length === 0 279 ? [`'' as groupingColumn`] 280 : unaggCols.map((x) => `s.${x}`) 281 ).join()}, 282 ${(aggCols.length === 0 283 ? [`'' as groupedColumn`] 284 : aggCols.map((x) => `s.${x}`) 285 ).join()} 286 from _flamegraph_materialized_statement_${uuid} s 287 ), 288 (${showStackFilter}), 289 (${hideStackFilter}), 290 (${showFromFrameFilter}), 291 (${hideFrameFilter}), 292 (${pivotFilter}), 293 ${1 << showStackAndPivot.length}, 294 ${groupingColumns} 295 ) 296 `, 297 ), 298 ); 299 // TODO(lalitm): this doesn't need to be called unless we have 300 // a non-empty set of filters. 301 disposable.use( 302 await createPerfettoTable( 303 engine, 304 `_flamegraph_filtered_${uuid}`, 305 ` 306 select * 307 from _viz_flamegraph_filter_frames!( 308 _flamegraph_source_${uuid}, 309 ${showFromFrameBits} 310 ) 311 `, 312 ), 313 ); 314 disposable.use( 315 await createPerfettoTable( 316 engine, 317 `_flamegraph_accumulated_${uuid}`, 318 ` 319 select * 320 from _viz_flamegraph_accumulate!( 321 _flamegraph_filtered_${uuid}, 322 ${showStackBits} 323 ) 324 `, 325 ), 326 ); 327 disposable.use( 328 await createPerfettoTable( 329 engine, 330 `_flamegraph_hash_${uuid}`, 331 ` 332 select * 333 from _viz_flamegraph_downwards_hash!( 334 _flamegraph_source_${uuid}, 335 _flamegraph_filtered_${uuid}, 336 _flamegraph_accumulated_${uuid}, 337 ${groupingColumns}, 338 ${groupedColumns}, 339 ${view.kind === 'BOTTOM_UP' ? 'FALSE' : 'TRUE'} 340 ) 341 union all 342 select * 343 from _viz_flamegraph_upwards_hash!( 344 _flamegraph_source_${uuid}, 345 _flamegraph_filtered_${uuid}, 346 _flamegraph_accumulated_${uuid}, 347 ${groupingColumns}, 348 ${groupedColumns} 349 ) 350 order by hash 351 `, 352 ), 353 ); 354 disposable.use( 355 await createPerfettoTable( 356 engine, 357 `_flamegraph_merged_${uuid}`, 358 ` 359 select * 360 from _viz_flamegraph_merge_hashes!( 361 _flamegraph_hash_${uuid}, 362 ${groupingColumns}, 363 ${computeGroupedAggExprs(agg)} 364 ) 365 `, 366 ), 367 ); 368 disposable.use( 369 await createPerfettoTable( 370 engine, 371 `_flamegraph_layout_${uuid}`, 372 ` 373 select * 374 from _viz_flamegraph_local_layout!( 375 _flamegraph_merged_${uuid} 376 ); 377 `, 378 ), 379 ); 380 const res = await engine.query(` 381 select * 382 from _viz_flamegraph_global_layout!( 383 _flamegraph_merged_${uuid}, 384 _flamegraph_layout_${uuid}, 385 ${groupingColumns}, 386 ${groupedColumns} 387 ) 388 `); 389 390 const it = res.iter({ 391 id: NUM, 392 parentId: NUM, 393 depth: NUM, 394 name: STR, 395 selfValue: NUM, 396 cumulativeValue: NUM, 397 parentCumulativeValue: NUM_NULL, 398 xStart: NUM, 399 xEnd: NUM, 400 ...Object.fromEntries(unaggCols.map((m) => [m, STR_NULL])), 401 ...Object.fromEntries(aggCols.map((m) => [m, UNKNOWN])), 402 }); 403 let postiveRootsValue = 0; 404 let negativeRootsValue = 0; 405 let minDepth = 0; 406 let maxDepth = 0; 407 const nodes = []; 408 for (; it.valid(); it.next()) { 409 const properties = new Map<string, string>(); 410 for (const a of [...agg, ...unagg]) { 411 const r = it.get(a.name); 412 if (r !== null) { 413 properties.set(a.displayName, r as string); 414 } 415 } 416 nodes.push({ 417 id: it.id, 418 parentId: it.parentId, 419 depth: it.depth, 420 name: it.name, 421 selfValue: it.selfValue, 422 cumulativeValue: it.cumulativeValue, 423 parentCumulativeValue: it.parentCumulativeValue ?? undefined, 424 xStart: it.xStart, 425 xEnd: it.xEnd, 426 properties, 427 }); 428 if (it.depth === 1) { 429 postiveRootsValue += it.cumulativeValue; 430 } else if (it.depth === -1) { 431 negativeRootsValue += it.cumulativeValue; 432 } 433 minDepth = Math.min(minDepth, it.depth); 434 maxDepth = Math.max(maxDepth, it.depth); 435 } 436 const sumQuery = await engine.query( 437 `select sum(value) v from _flamegraph_source_${uuid}`, 438 ); 439 const unfilteredCumulativeValue = sumQuery.firstRow({v: NUM_NULL}).v ?? 0; 440 return { 441 nodes, 442 allRootsCumulativeValue: 443 view.kind === 'BOTTOM_UP' ? negativeRootsValue : postiveRootsValue, 444 unfilteredCumulativeValue, 445 minDepth, 446 maxDepth, 447 }; 448} 449 450function makeSqlFilter(x: string) { 451 if (x.startsWith('^') && x.endsWith('$')) { 452 return x.slice(1, -1); 453 } 454 return `%${x}%`; 455} 456 457function getPivotFilter(view: FlamegraphView) { 458 if (view.kind === 'PIVOT') { 459 return `name like '${makeSqlFilter(view.pivot)}'`; 460 } 461 if (view.kind === 'BOTTOM_UP') { 462 return 'value > 0'; 463 } 464 return '0'; 465} 466 467function computeGroupedAggExprs(agg: ReadonlyArray<AggQueryFlamegraphColumn>) { 468 const aggFor = (x: AggQueryFlamegraphColumn) => { 469 switch (x.mergeAggregation) { 470 case 'ONE_OR_NULL': 471 return `IIF(COUNT() = 1, ${x.name}, NULL) AS ${x.name}`; 472 case 'SUM': 473 return `SUM(${x.name}) AS ${x.name}`; 474 } 475 }; 476 return `(${agg.length === 0 ? 'groupedColumn' : agg.map((x) => aggFor(x)).join(',')})`; 477} 478