| // Copyright (C) 2026 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import type {Engine} from '../../../trace_processor/engine'; |
| import { |
| NUM, |
| STR_NULL, |
| type QueryResult, |
| } from '../../../trace_processor/query_result'; |
| import type {BarChartData, BarChartItem, BarChartSeries} from './bar_chart'; |
| import { |
| ChartSource, |
| type ColumnType, |
| SQLChartLoader, |
| type QueryConfig, |
| type ChartLoaderResult, |
| inFilter, |
| } from './chart_sql_source'; |
| import type {ChartAggregation} from './chart_utils'; |
| |
| /** |
| * Configuration for SQLBarChartLoader. |
| */ |
| export interface SQLBarChartLoaderOpts { |
| /** The trace processor engine to run queries against. */ |
| readonly engine: Engine; |
| |
| /** |
| * SQL query that provides the raw data. |
| * Must include both the dimension and measure columns in its output. |
| */ |
| readonly query: string; |
| |
| /** |
| * Column name for the dimension (grouping key / bar labels). |
| * Values can be strings or numbers. |
| */ |
| readonly dimensionColumn: string; |
| |
| /** |
| * Column name for the measure (numeric values to aggregate). |
| */ |
| readonly measureColumn: string; |
| |
| /** |
| * Optional column for stacked bar series. Each unique value in this column |
| * becomes a separate stacked series. |
| */ |
| readonly seriesColumn?: string; |
| } |
| |
| /** |
| * Per-use configuration for the bar chart loader. |
| */ |
| export interface BarChartLoaderConfig { |
| /** Aggregation function to apply to the measure column per group. */ |
| readonly aggregation: ChartAggregation; |
| |
| /** |
| * Maximum number of bars to return. Groups are sorted by aggregated |
| * value descending, and only the top N are kept. Defaults to no limit. |
| */ |
| readonly limit?: number; |
| |
| /** |
| * Filter to only include specific dimension values (e.g., from brush). |
| * NULL values in the array produce `column IS NULL` SQL conditions. |
| */ |
| readonly filter?: ReadonlyArray<string | number | null>; |
| } |
| |
| /** Result returned by the bar chart loader. */ |
| export type BarChartLoaderResult = ChartLoaderResult<BarChartData>; |
| |
| /** |
| * SQL-based bar chart loader with async loading and caching. |
| * |
| * Performs grouping and aggregation directly in SQL for efficiency with |
| * large datasets. Uses QuerySlot for caching and request deduplication. |
| */ |
| export class SQLBarChartLoader extends SQLChartLoader< |
| BarChartLoaderConfig, |
| BarChartData |
| > { |
| private readonly dimensionColumn: string; |
| private readonly measureColumn: string; |
| private readonly seriesColumn?: string; |
| |
| constructor(opts: SQLBarChartLoaderOpts) { |
| const schema: Record<string, ColumnType> = { |
| [opts.dimensionColumn]: 'text', |
| [opts.measureColumn]: 'real', |
| }; |
| if (opts.seriesColumn !== undefined) { |
| schema[opts.seriesColumn] = 'text'; |
| } |
| super( |
| opts.engine, |
| new ChartSource({ |
| query: opts.query, |
| schema, |
| }), |
| ); |
| this.dimensionColumn = opts.dimensionColumn; |
| this.measureColumn = opts.measureColumn; |
| this.seriesColumn = opts.seriesColumn; |
| } |
| |
| protected buildQueryConfig(config: BarChartLoaderConfig): QueryConfig { |
| const dimensions = [{column: this.dimensionColumn}]; |
| if (this.seriesColumn !== undefined) { |
| dimensions.push({column: this.seriesColumn}); |
| } |
| return { |
| type: 'aggregated', |
| dimensions, |
| measures: [{column: this.measureColumn, aggregation: config.aggregation}], |
| filters: inFilter(this.dimensionColumn, config.filter), |
| limit: config.limit, |
| }; |
| } |
| |
| protected parseResult(queryResult: QueryResult): BarChartData { |
| if (this.seriesColumn !== undefined) { |
| return this.parseStacked(queryResult); |
| } |
| const items: BarChartItem[] = []; |
| const iter = queryResult.iter({_dim: STR_NULL, _value: NUM}); |
| for (; iter.valid(); iter.next()) { |
| items.push({label: iter._dim ?? '(null)', value: iter._value}); |
| } |
| return {items}; |
| } |
| |
| private parseStacked(queryResult: QueryResult): BarChartData { |
| // ChartSource names dimension columns as _dim, _dim_1, _dim_2, … |
| // (see buildAggregatedQuery in chart_sql_source.ts). Here _dim is |
| // the primary category and _dim_1 is the series/group column. |
| const seriesMap = new Map<string, BarChartItem[]>(); |
| const iter = queryResult.iter({ |
| _dim: STR_NULL, |
| _dim_1: STR_NULL, |
| _value: NUM, |
| }); |
| for (; iter.valid(); iter.next()) { |
| const seriesName = iter._dim_1 ?? '(null)'; |
| const label = iter._dim ?? '(null)'; |
| let items = seriesMap.get(seriesName); |
| if (items === undefined) { |
| items = []; |
| seriesMap.set(seriesName, items); |
| } |
| items.push({label, value: iter._value}); |
| } |
| const series: BarChartSeries[] = []; |
| for (const [name, items] of seriesMap) { |
| series.push({name, items}); |
| } |
| return {items: [], series}; |
| } |
| } |