blob: 07196bf9243a5464b5abc5eb7a7af8ad5893b0ee [file]
// 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};
}
}