• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2023 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 {copyToClipboard} from '../../base/clipboard';
18import {isString} from '../../base/object_utils';
19import {Icons} from '../../base/semantic_icons';
20import {sqliteString} from '../../base/string_utils';
21import {Duration, Time} from '../../base/time';
22import {Row} from '../../trace_processor/query_result';
23import {
24  SqlValue,
25  sqlValueToReadableString,
26} from '../../trace_processor/sql_utils';
27import {Anchor} from '../../widgets/anchor';
28import {renderError} from '../../widgets/error';
29import {MenuItem, PopupMenu2} from '../../widgets/menu';
30import {SliceRef} from '../sql/slice';
31import {asSliceSqlId} from '../sql_types';
32import {DurationWidget} from '../widgets/duration';
33import {Timestamp} from '../widgets/timestamp';
34
35import {Column} from './column';
36import {SqlTableState} from './state';
37import {SliceIdDisplayConfig} from './table_description';
38
39// This file is responsible for rendering a value in a given sell based on the
40// column type.
41
42function filterOptionMenuItem(
43  label: string,
44  filter: string,
45  state: SqlTableState,
46): m.Child {
47  return m(MenuItem, {
48    label,
49    onclick: () => {
50      state.addFilter(filter);
51    },
52  });
53}
54
55function getStandardFilters(
56  c: Column,
57  value: SqlValue,
58  state: SqlTableState,
59): m.Child[] {
60  if (value === null) {
61    return [
62      filterOptionMenuItem('is null', `${c.expression} is null`, state),
63      filterOptionMenuItem('is not null', `${c.expression} is not null`, state),
64    ];
65  }
66  if (isString(value)) {
67    return [
68      filterOptionMenuItem(
69        'equals to',
70        `${c.expression} = ${sqliteString(value)}`,
71        state,
72      ),
73      filterOptionMenuItem(
74        'not equals to',
75        `${c.expression} != ${sqliteString(value)}`,
76        state,
77      ),
78    ];
79  }
80  if (typeof value === 'bigint' || typeof value === 'number') {
81    return [
82      filterOptionMenuItem('equals to', `${c.expression} = ${value}`, state),
83      filterOptionMenuItem(
84        'not equals to',
85        `${c.expression} != ${value}`,
86        state,
87      ),
88      filterOptionMenuItem('greater than', `${c.expression} > ${value}`, state),
89      filterOptionMenuItem(
90        'greater or equals than',
91        `${c.expression} >= ${value}`,
92        state,
93      ),
94      filterOptionMenuItem('less than', `${c.expression} < ${value}`, state),
95      filterOptionMenuItem(
96        'less or equals than',
97        `${c.expression} <= ${value}`,
98        state,
99      ),
100    ];
101  }
102  return [];
103}
104
105function displayValue(value: SqlValue): m.Child {
106  if (value === null) {
107    return m('i', 'NULL');
108  }
109  return sqlValueToReadableString(value);
110}
111
112function display(column: Column, row: Row): m.Children {
113  const value = row[column.alias];
114  return displayValue(value);
115}
116
117function copyMenuItem(label: string, value: string): m.Child {
118  return m(MenuItem, {
119    icon: Icons.Copy,
120    label,
121    onclick: () => {
122      copyToClipboard(value);
123    },
124  });
125}
126
127function getContextMenuItems(
128  column: Column,
129  row: Row,
130  state: SqlTableState,
131): m.Child[] {
132  const result: m.Child[] = [];
133  const value = row[column.alias];
134
135  if (isString(value)) {
136    result.push(copyMenuItem('Copy', value));
137  }
138
139  const filters = getStandardFilters(column, value, state);
140  if (filters.length > 0) {
141    result.push(
142      m(MenuItem, {label: 'Add filter', icon: Icons.Filter}, ...filters),
143    );
144  }
145
146  return result;
147}
148
149function renderStandardColumn(
150  column: Column,
151  row: Row,
152  state: SqlTableState,
153): m.Children {
154  const displayValue = display(column, row);
155  const contextMenuItems: m.Child[] = getContextMenuItems(column, row, state);
156  return m(
157    PopupMenu2,
158    {
159      trigger: m(Anchor, displayValue),
160    },
161    ...contextMenuItems,
162  );
163}
164
165function renderTimestampColumn(
166  column: Column,
167  row: Row,
168  state: SqlTableState,
169): m.Children {
170  const value = row[column.alias];
171  if (typeof value !== 'bigint') {
172    return renderStandardColumn(column, row, state);
173  }
174
175  return m(Timestamp, {
176    ts: Time.fromRaw(value),
177    extraMenuItems: getContextMenuItems(column, row, state),
178  });
179}
180
181function renderDurationColumn(
182  column: Column,
183  row: Row,
184  state: SqlTableState,
185): m.Children {
186  const value = row[column.alias];
187  if (typeof value !== 'bigint') {
188    return renderStandardColumn(column, row, state);
189  }
190
191  return m(DurationWidget, {
192    dur: Duration.fromRaw(value),
193    extraMenuItems: getContextMenuItems(column, row, state),
194  });
195}
196
197function renderSliceIdColumn(
198  column: {alias: string; display: SliceIdDisplayConfig},
199  row: Row,
200): m.Children {
201  const config = column.display;
202  const id = row[column.alias];
203  const ts = row[config.ts];
204  const dur = row[config.dur] === null ? -1n : row[config.dur];
205  const trackId = row[config.trackId];
206
207  const columnNotFoundError = (type: string, name: string) =>
208    renderError(`${type} column ${name} not found`);
209  const wrongTypeError = (type: string, name: string, value: SqlValue) =>
210    renderError(
211      `Wrong type for ${type} column ${name}: bigint expected, ${typeof value} found`,
212    );
213
214  if (typeof id !== 'bigint') {
215    return sqlValueToReadableString(id);
216  }
217  if (ts === undefined) return columnNotFoundError('Timestamp', config.ts);
218  if (typeof ts !== 'bigint') return wrongTypeError('timestamp', config.ts, ts);
219  if (dur === undefined) return columnNotFoundError('Duration', config.dur);
220  if (typeof dur !== 'bigint') {
221    return wrongTypeError('duration', config.dur, ts);
222  }
223  if (trackId === undefined) return columnNotFoundError('Track id', trackId);
224  if (typeof trackId !== 'bigint') {
225    return wrongTypeError('track id', config.trackId, trackId);
226  }
227
228  return m(SliceRef, {
229    id: asSliceSqlId(Number(id)),
230    name: `${id}`,
231    ts: Time.fromRaw(ts),
232    dur: dur,
233    sqlTrackId: Number(trackId),
234    switchToCurrentSelectionTab: false,
235  });
236}
237
238export function renderCell(
239  column: Column,
240  row: Row,
241  state: SqlTableState,
242): m.Children {
243  if (column.display) {
244    switch (column.display?.type) {
245      case 'slice_id':
246        return renderSliceIdColumn(
247          {alias: column.alias, display: column.display},
248          row,
249        );
250      case 'timestamp':
251        return renderTimestampColumn(column, row, state);
252      case 'duration':
253      case 'thread_duration':
254        return renderDurationColumn(column, row, state);
255    }
256  }
257  return renderStandardColumn(column, row, state);
258}
259