• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2024 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';
16
17import {DetailsShell} from '../../../widgets/details_shell';
18import {uuidv4} from '../../../base/uuid';
19import {BottomTab, NewBottomTabArgs} from '../../bottom_tab';
20import {VegaView} from '../../../widgets/vega_view';
21import {addEphemeralTab} from '../../../common/addEphemeralTab';
22import {HistogramState} from './state';
23import {stringifyJsonWithBigints} from '../../../base/json_utils';
24import {Engine} from '../../../public';
25import {Filter} from '../../sql_table/state';
26import {isString} from '../../../base/object_utils';
27
28interface HistogramTabConfig {
29  columnTitle: string; // Human readable column name (ex: Duration)
30  sqlColumn: string; // SQL column name (ex: dur)
31  filters?: Filter[]; // Filters applied to SQL table
32  tableDisplay?: string; // Human readable table name (ex: slices)
33  query: string; // SQL query for the underlying data
34}
35
36export function addHistogramTab(
37  config: HistogramTabConfig,
38  engine: Engine,
39): void {
40  const histogramTab = new HistogramTab({
41    config,
42    engine,
43    uuid: uuidv4(),
44  });
45
46  addEphemeralTab(histogramTab, 'histogramTab');
47}
48
49export class HistogramTab extends BottomTab<HistogramTabConfig> {
50  static readonly kind = 'dev.perfetto.HistogramTab';
51
52  private state: HistogramState;
53
54  constructor(args: NewBottomTabArgs<HistogramTabConfig>) {
55    super(args);
56
57    this.state = new HistogramState(
58      this.engine,
59      this.config.query,
60      this.config.sqlColumn,
61    );
62  }
63
64  static create(args: NewBottomTabArgs<HistogramTabConfig>): HistogramTab {
65    return new HistogramTab(args);
66  }
67
68  viewTab() {
69    return m(
70      DetailsShell,
71      {
72        title: this.getTitle(),
73        description: this.getDescription(),
74      },
75      m(
76        '.histogram',
77        m(VegaView, {
78          spec: `
79            {
80              "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
81              "mark": "bar",
82              "data": {
83                "values": ${
84                  this.state.data
85                    ? stringifyJsonWithBigints(this.state.data)
86                    : []
87                }
88              },
89              "encoding": {
90                "${this.state.chartConfig.binAxis}": {
91                  "bin": ${this.state.chartConfig.isBinned},
92                  "field": "${this.config.sqlColumn}",
93                  "type": "${this.state.chartConfig.binAxisType}",
94                  "title": "${this.config.columnTitle}",
95                  "sort": ${this.state.chartConfig.sort},
96                  "axis": {
97                    "labelLimit": ${this.state.chartConfig.labelLimit}
98                  }
99                },
100                "${this.state.chartConfig.countAxis}": {
101                  "aggregate": "count",
102                  "title": "Count"
103                }
104              }
105            }
106          `,
107          data: {},
108        }),
109      ),
110    );
111  }
112
113  getTitle(): string {
114    return `${this.toTitleCase(this.config.columnTitle)} ${
115      this.state.chartConfig.binAxisType === 'quantitative'
116        ? 'Histogram'
117        : 'Counts'
118    }`;
119  }
120
121  getDescription(): string {
122    let desc = `Count distribution for ${
123      this.config.tableDisplay ? this.config.tableDisplay : ''
124    } table`;
125
126    if (this.config.filters) {
127      const filterStrings: string[] = [];
128      desc += ' where ';
129
130      for (const f of this.config.filters) {
131        filterStrings.push(`${isString(f) ? f : `${f.argName} ${f.op}`}`);
132      }
133
134      desc += filterStrings.join(', ');
135    }
136
137    return desc;
138  }
139
140  toTitleCase(s: string): string {
141    const words = s.split(/\s/);
142
143    for (let i = 0; i < words.length; ++i) {
144      words[i] = words[i][0].toUpperCase() + words[i].substring(1);
145    }
146
147    return words.join(' ');
148  }
149
150  isLoading(): boolean {
151    return this.state.isLoading;
152  }
153}
154