blob: d3867b77b78acabee6a3dd61248b216da8b795ff [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 {QuerySlot, type QueryResult} from '../../../base/query_slot';
import type {Engine} from '../../../trace_processor/engine';
import {
NUM,
STR,
type QueryResult as TPQueryResult,
} from '../../../trace_processor/query_result';
import type {AggregateFunction} from '../datagrid/model';
import {sqlAggregateExpr} from '../datagrid/sql_utils';
import type {HeatmapData} from './heatmap';
import {validateColumnName} from './chart_utils';
/**
* Configuration for heatmap loaders.
*/
export interface HeatmapLoaderConfig {
/** Aggregation function for cell values. Defaults to 'SUM'. */
readonly aggregation?: AggregateFunction;
/** Maximum number of X-axis categories. */
readonly xLimit?: number;
/** Maximum number of Y-axis categories. */
readonly yLimit?: number;
}
/**
* Configuration for SQLHeatmapLoader.
*/
export interface SQLHeatmapLoaderOpts {
/** The trace processor engine to run queries against. */
readonly engine: Engine;
/** SQL query that provides the data. */
readonly query: string;
/** Column name for the X axis (columns of the heatmap). */
readonly xColumn: string;
/** Column name for the Y axis (rows of the heatmap). */
readonly yColumn: string;
/** Column name containing the numeric values to aggregate. */
readonly valueColumn: string;
}
/**
* SQL-based heatmap loader.
*
* Groups data by two categorical dimensions (x, y) and aggregates a
* numeric value, producing a grid suitable for the HeatmapChart widget.
*/
export class SQLHeatmapLoader {
private readonly engine: Engine;
private readonly query: string;
private readonly xColumn: string;
private readonly yColumn: string;
private readonly valueColumn: string;
private readonly querySlot = new QuerySlot<HeatmapData>();
constructor(opts: SQLHeatmapLoaderOpts) {
validateColumnName(opts.xColumn);
validateColumnName(opts.yColumn);
validateColumnName(opts.valueColumn);
this.engine = opts.engine;
this.query = opts.query;
this.xColumn = opts.xColumn;
this.yColumn = opts.yColumn;
this.valueColumn = opts.valueColumn;
}
use(config: HeatmapLoaderConfig): QueryResult<HeatmapData> {
const agg = config.aggregation ?? 'SUM';
const xLimit = config.xLimit ?? 20;
const yLimit = config.yLimit ?? 20;
const aggExpr = sqlAggregateExpr(agg, this.valueColumn);
const sql = `
WITH _src AS (
SELECT
CAST(${this.xColumn} AS TEXT) AS _x,
CAST(${this.yColumn} AS TEXT) AS _y,
${aggExpr} AS _val
FROM (${this.query})
WHERE ${this.valueColumn} IS NOT NULL
GROUP BY _x, _y
),
_top_x AS (
SELECT _x, SUM(_val) AS _total
FROM _src
GROUP BY _x
ORDER BY _total DESC
LIMIT ${xLimit}
),
_top_y AS (
SELECT _y, SUM(_val) AS _total
FROM _src
GROUP BY _y
ORDER BY _total DESC
LIMIT ${yLimit}
)
SELECT _x, _y, _val
FROM _src
WHERE _x IN (SELECT _x FROM _top_x)
AND _y IN (SELECT _y FROM _top_y)
ORDER BY _x, _y`.trim();
return this.querySlot.use({
key: {sql},
retainOn: ['sql'],
queryFn: async () => {
const queryResult = await this.engine.query(sql);
return parseHeatmapResult(queryResult);
},
});
}
dispose(): void {
this.querySlot.dispose();
}
}
function parseHeatmapResult(queryResult: TPQueryResult): HeatmapData {
const xSet = new Map<string, number>();
const ySet = new Map<string, number>();
const rawValues: Array<{x: string; y: string; val: number}> = [];
const iter = queryResult.iter({
_x: STR,
_y: STR,
_val: NUM,
});
for (; iter.valid(); iter.next()) {
rawValues.push({x: iter._x, y: iter._y, val: iter._val});
if (!xSet.has(iter._x)) {
xSet.set(iter._x, xSet.size);
}
if (!ySet.has(iter._y)) {
ySet.set(iter._y, ySet.size);
}
}
const xLabels = Array.from(xSet.keys());
const yLabels = Array.from(ySet.keys());
let min = Infinity;
let max = -Infinity;
const values: Array<readonly [number, number, number]> = rawValues.map(
({x, y, val}) => {
min = Math.min(min, val);
max = Math.max(max, val);
const xIdx = xSet.get(x) ?? 0;
const yIdx = ySet.get(y) ?? 0;
return [xIdx, yIdx, val] as const;
},
);
if (min === Infinity) {
min = 0;
max = 0;
}
return {xLabels, yLabels, values, min, max};
}