| // 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, |
| type QueryResult, |
| } from '../../../trace_processor/query_result'; |
| import type {LineChartData} from './line_chart'; |
| import { |
| ChartSource, |
| SQLChartLoader, |
| type QueryConfig, |
| type ChartLoaderResult, |
| rangeFilters, |
| } from './chart_sql_source'; |
| |
| /** |
| * Configuration for CDF loaders. |
| */ |
| export interface CdfLoaderConfig { |
| /** |
| * Range filter to apply to the data (e.g., from brush selection). |
| * Only values within [min, max] are included. |
| */ |
| readonly filter?: { |
| readonly min: number; |
| readonly max: number; |
| }; |
| |
| /** |
| * Maximum number of points to return. When the dataset is larger, |
| * results are capped via SQL LIMIT. Defaults to 500. |
| */ |
| readonly maxPoints?: number; |
| } |
| |
| /** |
| * Configuration for SQLCdfLoader. |
| */ |
| export interface SQLCdfLoaderOpts { |
| /** The trace processor engine to run queries against. */ |
| readonly engine: Engine; |
| |
| /** SQL query that provides the data. */ |
| readonly query: string; |
| |
| /** Column name containing the values to compute CDF over. */ |
| readonly valueColumn: string; |
| |
| /** Optional series/breakdown column name. */ |
| readonly seriesColumn?: string; |
| } |
| |
| /** Result returned by the CDF loader. */ |
| export type CdfLoaderResult = ChartLoaderResult<LineChartData>; |
| |
| /** |
| * SQL-based CDF (Cumulative Distribution Function) loader. |
| * |
| * Fetches sorted values from SQL and computes cumulative percentages |
| * in JS. The result is a LineChartData that can be rendered directly |
| * with the LineChart widget. |
| * |
| * Each point has x = value, y = cumulative percentage (0-100). |
| */ |
| export class SQLCdfLoader extends SQLChartLoader< |
| CdfLoaderConfig, |
| LineChartData |
| > { |
| private readonly valCol: string; |
| private readonly seriesCol: string | undefined; |
| |
| constructor(opts: SQLCdfLoaderOpts) { |
| const valCol = opts.valueColumn; |
| const seriesCol = opts.seriesColumn; |
| |
| const schema: Record<string, 'real' | 'text'> = { |
| [valCol]: 'real', |
| }; |
| if (seriesCol !== undefined) { |
| schema[seriesCol] = 'text'; |
| } |
| |
| super(opts.engine, new ChartSource({query: opts.query, schema})); |
| this.valCol = valCol; |
| this.seriesCol = seriesCol; |
| } |
| |
| protected buildQueryConfig(config: CdfLoaderConfig): QueryConfig { |
| return { |
| type: 'points', |
| columns: [{column: this.valCol, alias: '_x', cast: 'real' as const}], |
| breakdown: this.seriesCol, |
| filters: rangeFilters(this.valCol, config.filter), |
| orderBy: [{column: '_x', direction: 'asc'}], |
| maxPointsPerSeries: config.maxPoints ?? 500, |
| }; |
| } |
| |
| protected parseResult( |
| queryResult: QueryResult, |
| _config: CdfLoaderConfig, |
| ): LineChartData { |
| return parseCdfResult(queryResult, this.seriesCol !== undefined); |
| } |
| } |
| |
| function parseCdfResult( |
| queryResult: QueryResult, |
| hasSeries: boolean, |
| ): LineChartData { |
| const seriesMap = new Map<string, number[]>(); |
| |
| if (hasSeries) { |
| const iter = queryResult.iter({_x: NUM, _series: STR}); |
| for (; iter.valid(); iter.next()) { |
| const name = iter._series; |
| let values = seriesMap.get(name); |
| if (values === undefined) { |
| values = []; |
| seriesMap.set(name, values); |
| } |
| values.push(iter._x); |
| } |
| } else { |
| const values: number[] = []; |
| seriesMap.set('CDF', values); |
| const iter = queryResult.iter({_x: NUM}); |
| for (; iter.valid(); iter.next()) { |
| values.push(iter._x); |
| } |
| } |
| |
| const series = Array.from(seriesMap.entries()).map(([name, values]) => { |
| // Values are already sorted (ORDER BY in SQL) |
| const n = values.length; |
| const points = values.map((x, i) => ({ |
| x, |
| y: ((i + 1) / n) * 100, |
| })); |
| return {name, points}; |
| }); |
| |
| return {series}; |
| } |