• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2025 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 {
17  ColumnController,
18  ColumnControllerDiff,
19  ColumnControllerRow,
20} from '../column_controller';
21import {Section} from '../../../../widgets/section';
22import {Select} from '../../../../widgets/select';
23import {TextInput} from '../../../../widgets/text_input';
24import {Button} from '../../../../widgets/button';
25import protos from '../../../../protos';
26
27export interface GroupByAgg {
28  column?: ColumnControllerRow;
29  aggregationOp: string;
30  newColumnName?: string;
31}
32
33export interface GroupByAttrs {
34  groupByColumns: ColumnControllerRow[];
35  aggregations: GroupByAgg[];
36}
37
38const AGGREGATION_OPS = [
39  'COUNT',
40  'SUM',
41  'MIN',
42  'MAX',
43  'MEAN',
44  'DURATION_WEIGHTED_MEAN',
45] as const;
46
47export class GroupByOperation implements m.ClassComponent<GroupByAttrs> {
48  view({attrs}: m.CVnode<GroupByAttrs>) {
49    if (attrs.groupByColumns.length === 0) {
50      return;
51    }
52
53    const selectGroupByColumns = (): m.Child => {
54      return m(ColumnController, {
55        options: attrs.groupByColumns,
56        allowAlias: false,
57        onChange: (diffs: ColumnControllerDiff[]) => {
58          for (const diff of diffs) {
59            const column = attrs.groupByColumns.find((c) => c.id === diff.id);
60            if (column) {
61              column.checked = diff.checked;
62              if (!diff.checked) {
63                attrs.aggregations = attrs.aggregations?.filter(
64                  (agg) => agg.column?.id !== diff.id,
65                );
66              }
67            }
68          }
69        },
70      });
71    };
72
73    const selectAggregationForColumn = (
74      agg: GroupByAgg,
75      index: number,
76    ): m.Child => {
77      const columnOptions = attrs.groupByColumns.map((col) =>
78        m(
79          'option',
80          {
81            value: col.id,
82            selected: agg.column?.id === col.id,
83          },
84          col.id,
85        ),
86      );
87
88      return m(
89        Section,
90        {
91          title: `Aggregation ${index + 1}`,
92          key: index,
93        },
94        m(Button, {
95          label: 'X',
96          onclick: () => {
97            attrs.aggregations?.splice(index, 1);
98          },
99        }),
100        m(
101          'Column:',
102          m(
103            Select,
104            {
105              onchange: (e: Event) => {
106                const target = e.target as HTMLSelectElement;
107                const selectedColumn = attrs.groupByColumns.find(
108                  (c) => c.id === target.value,
109                );
110                agg.column = selectedColumn;
111              },
112            },
113            m(
114              'option',
115              {disabled: true, selected: !agg.column},
116              'Select a column',
117            ),
118            columnOptions,
119          ),
120        ),
121        m(
122          Select,
123          {
124            title: 'Aggregation type: ',
125            onchange: (e: Event) => {
126              agg.aggregationOp = (e.target as HTMLSelectElement).value;
127            },
128          },
129          AGGREGATION_OPS.map((op) =>
130            m(
131              'option',
132              {
133                value: op,
134                selected: op === agg.aggregationOp,
135              },
136              op,
137            ),
138          ),
139        ),
140        m(TextInput, {
141          title: 'New column name',
142          placeholder: agg.column
143            ? placeholderNewColumnName(agg)
144            : 'Enter column name',
145          onchange: (e: Event) => {
146            agg.newColumnName = (e.target as HTMLInputElement).value.trim();
147          },
148          value: agg.newColumnName,
149        }),
150      );
151    };
152
153    const onAddAggregation = () => {
154      attrs.aggregations.push({
155        aggregationOp: AGGREGATION_OPS[0],
156        column: undefined,
157        newColumnName: undefined,
158      });
159    };
160
161    const selectAggregations = (): m.Child => {
162      return m(
163        '',
164        attrs.aggregations.map((agg, index) =>
165          selectAggregationForColumn(agg, index),
166        ),
167        m(Button, {
168          label: 'Add Aggregation',
169          onclick: onAddAggregation,
170        }),
171      );
172    };
173
174    return m(
175      '',
176      m(Section, {title: 'Columns for group by'}, selectGroupByColumns()),
177      m(Section, {title: 'Aggregations'}, selectAggregations()),
178    );
179  }
180}
181
182function stringToAggregateOp(
183  s: string,
184): protos.PerfettoSqlStructuredQuery.GroupBy.Aggregate.Op {
185  if (AGGREGATION_OPS.includes(s as (typeof AGGREGATION_OPS)[number])) {
186    return protos.PerfettoSqlStructuredQuery.GroupBy.Aggregate.Op[
187      s as keyof typeof protos.PerfettoSqlStructuredQuery.GroupBy.Aggregate.Op
188    ];
189  }
190  throw new Error(`Invalid AggregateOp '${s}'`);
191}
192
193export function GroupByAggregationAttrsToProto(
194  agg: GroupByAgg,
195): protos.PerfettoSqlStructuredQuery.GroupBy.Aggregate {
196  const newAgg = new protos.PerfettoSqlStructuredQuery.GroupBy.Aggregate();
197  newAgg.columnName = agg.column!.column.name;
198  newAgg.op = stringToAggregateOp(agg.aggregationOp);
199  newAgg.resultColumnName = agg.newColumnName ?? placeholderNewColumnName(agg);
200  return newAgg;
201}
202
203export function placeholderNewColumnName(agg: GroupByAgg) {
204  return agg.column
205    ? `${agg.column.id}_${agg.aggregationOp}`
206    : `agg_${agg.aggregationOp}`;
207}
208