• 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';
16import {sqliteString} from '../../../../base/string_utils';
17import {Duration, Time} from '../../../../base/time';
18import {SqlValue, STR} from '../../../../trace_processor/query_result';
19import {
20  asSchedSqlId,
21  asSliceSqlId,
22  asThreadStateSqlId,
23  asUpid,
24  asUtid,
25} from '../../../sql_utils/core_types';
26import {Anchor} from '../../../../widgets/anchor';
27import {renderError} from '../../../../widgets/error';
28import {PopupMenu} from '../../../../widgets/menu';
29import {DurationWidget} from '../../duration';
30import {showProcessDetailsMenuItem} from '../../process';
31import {SchedRef} from '../../sched';
32import {SliceRef} from '../../slice';
33import {showThreadDetailsMenuItem} from '../../thread';
34import {ThreadStateRef} from '../../thread_state';
35import {Timestamp} from '../../timestamp';
36import {TableColumn, TableManager} from './table_column';
37import {
38  getStandardContextMenuItems,
39  renderStandardCell,
40} from './render_cell_utils';
41import {SqlColumn, sqlColumnId, SqlExpression} from './sql_column';
42
43function wrongTypeError(type: string, name: SqlColumn, value: SqlValue) {
44  return renderError(
45    `Wrong type for ${type} column ${sqlColumnId(name)}: bigint expected, ${typeof value} found`,
46  );
47}
48
49export type ColumnParams = {
50  alias?: string;
51  startsHidden?: boolean;
52  title?: string;
53};
54
55export type StandardColumnParams = ColumnParams;
56
57export interface IdColumnParams {
58  // Whether this column is a primary key (ID) for this table or whether it's a reference
59  // to another table's primary key.
60  type?: 'id' | 'joinid';
61  // Whether the column is guaranteed not to have null values.
62  // (this will allow us to upgrage the joins on this column to more performant INNER JOINs).
63  notNull?: boolean;
64}
65
66export class StandardColumn implements TableColumn {
67  constructor(
68    public readonly column: SqlColumn,
69    private params?: StandardColumnParams,
70  ) {}
71
72  renderCell(value: SqlValue, tableManager?: TableManager): m.Children {
73    return renderStandardCell(value, this.column, tableManager);
74  }
75
76  initialColumns(): TableColumn[] {
77    return this.params?.startsHidden ? [] : [this];
78  }
79}
80
81export class TimestampColumn implements TableColumn {
82  constructor(public readonly column: SqlColumn) {}
83
84  renderCell(value: SqlValue, tableManager?: TableManager): m.Children {
85    if (typeof value === 'number') {
86      value = BigInt(Math.round(value));
87    }
88    if (typeof value !== 'bigint') {
89      return renderStandardCell(value, this.column, tableManager);
90    }
91    return m(Timestamp, {
92      ts: Time.fromRaw(value),
93      extraMenuItems:
94        tableManager &&
95        getStandardContextMenuItems(value, this.column, tableManager),
96    });
97  }
98}
99
100export class DurationColumn implements TableColumn {
101  constructor(public column: SqlColumn) {}
102
103  renderCell(value: SqlValue, tableManager?: TableManager): m.Children {
104    if (typeof value === 'number') {
105      value = BigInt(Math.round(value));
106    }
107    if (typeof value !== 'bigint') {
108      return renderStandardCell(value, this.column, tableManager);
109    }
110
111    return m(DurationWidget, {
112      dur: Duration.fromRaw(value),
113      extraMenuItems:
114        tableManager &&
115        getStandardContextMenuItems(value, this.column, tableManager),
116    });
117  }
118}
119
120export class SliceIdColumn implements TableColumn {
121  constructor(
122    public readonly column: SqlColumn,
123    private params?: IdColumnParams,
124  ) {}
125
126  renderCell(value: SqlValue, manager?: TableManager): m.Children {
127    const id = value;
128
129    if (!manager || id === null) {
130      return renderStandardCell(id, this.column, manager);
131    }
132
133    return m(SliceRef, {
134      id: asSliceSqlId(Number(id)),
135      name: `${id}`,
136      switchToCurrentSelectionTab: false,
137    });
138  }
139
140  listDerivedColumns() {
141    if (this.params?.type === 'id') return undefined;
142    return async () =>
143      new Map<string, TableColumn>([
144        ['ts', new TimestampColumn(this.getChildColumn('ts'))],
145        ['dur', new DurationColumn(this.getChildColumn('dur'))],
146        ['name', new StandardColumn(this.getChildColumn('name'))],
147        ['parent_id', new SliceIdColumn(this.getChildColumn('parent_id'))],
148      ]);
149  }
150
151  private getChildColumn(name: string): SqlColumn {
152    return {
153      column: name,
154      source: {
155        table: 'slice',
156        joinOn: {id: this.column},
157      },
158    };
159  }
160}
161
162export class SchedIdColumn implements TableColumn {
163  constructor(public readonly column: SqlColumn) {}
164
165  renderCell(value: SqlValue, manager?: TableManager): m.Children {
166    const id = value;
167
168    if (!manager || id === null) {
169      return renderStandardCell(id, this.column, manager);
170    }
171    if (typeof id !== 'bigint') return wrongTypeError('id', this.column, id);
172
173    return m(SchedRef, {
174      id: asSchedSqlId(Number(id)),
175      name: `${id}`,
176      switchToCurrentSelectionTab: false,
177    });
178  }
179}
180
181export class ThreadStateIdColumn implements TableColumn {
182  constructor(public readonly column: SqlColumn) {}
183
184  renderCell(value: SqlValue, manager?: TableManager): m.Children {
185    const id = value;
186
187    if (!manager || id === null) {
188      return renderStandardCell(id, this.column, manager);
189    }
190    if (typeof id !== 'bigint') return wrongTypeError('id', this.column, id);
191
192    return m(ThreadStateRef, {
193      id: asThreadStateSqlId(Number(id)),
194      name: `${id}`,
195      switchToCurrentSelectionTab: false,
196    });
197  }
198}
199
200export class ThreadIdColumn implements TableColumn {
201  constructor(
202    public readonly column: SqlColumn,
203    private params?: IdColumnParams,
204  ) {}
205
206  renderCell(value: SqlValue, manager?: TableManager): m.Children {
207    const utid = value;
208
209    if (!manager || utid === null) {
210      return renderStandardCell(utid, this.column, manager);
211    }
212
213    if (typeof utid !== 'bigint') {
214      throw new Error(
215        `thread.utid is expected to be bigint, got ${typeof utid}`,
216      );
217    }
218
219    return m(
220      PopupMenu,
221      {
222        trigger: m(Anchor, `${utid}`),
223      },
224
225      showThreadDetailsMenuItem(asUtid(Number(utid))),
226      getStandardContextMenuItems(utid, this.column, manager),
227    );
228  }
229
230  listDerivedColumns() {
231    if (this.params?.type === 'id') return undefined;
232    return async () =>
233      new Map<string, TableColumn>([
234        ['tid', new StandardColumn(this.getChildColumn('tid'))],
235        ['name', new StandardColumn(this.getChildColumn('name'))],
236        ['start_ts', new TimestampColumn(this.getChildColumn('start_ts'))],
237        ['end_ts', new TimestampColumn(this.getChildColumn('end_ts'))],
238        ['upid', new ProcessIdColumn(this.getChildColumn('upid'))],
239        [
240          'is_main_thread',
241          new StandardColumn(this.getChildColumn('is_main_thread')),
242        ],
243      ]);
244  }
245
246  initialColumns(): TableColumn[] {
247    return [
248      this,
249      new StandardColumn(this.getChildColumn('tid')),
250      new StandardColumn(this.getChildColumn('name')),
251    ];
252  }
253
254  private getChildColumn(name: string): SqlColumn {
255    return {
256      column: name,
257      source: {
258        table: 'thread',
259        joinOn: {id: this.column},
260        // If the column is guaranteed not to have null values, we can use an INNER JOIN.
261        innerJoin: this.params?.notNull === true,
262      },
263    };
264  }
265}
266
267export class ProcessIdColumn implements TableColumn {
268  constructor(
269    public readonly column: SqlColumn,
270    private params?: IdColumnParams,
271  ) {}
272
273  renderCell(value: SqlValue, manager?: TableManager): m.Children {
274    const upid = value;
275
276    if (!manager || upid === null) {
277      return renderStandardCell(upid, this.column, manager);
278    }
279
280    if (typeof upid !== 'bigint') {
281      throw new Error(
282        `thread.upid is expected to be bigint, got ${typeof upid}`,
283      );
284    }
285
286    return m(
287      PopupMenu,
288      {
289        trigger: m(Anchor, `${upid}`),
290      },
291
292      showProcessDetailsMenuItem(asUpid(Number(upid))),
293      getStandardContextMenuItems(upid, this.column, manager),
294    );
295  }
296
297  listDerivedColumns() {
298    if (this.params?.type === 'id') return undefined;
299    return async () =>
300      new Map<string, TableColumn>([
301        ['pid', new StandardColumn(this.getChildColumn('pid'))],
302        ['name', new StandardColumn(this.getChildColumn('name'))],
303        ['start_ts', new TimestampColumn(this.getChildColumn('start_ts'))],
304        ['end_ts', new TimestampColumn(this.getChildColumn('end_ts'))],
305        [
306          'parent_upid',
307          new ProcessIdColumn(this.getChildColumn('parent_upid')),
308        ],
309        [
310          'is_main_thread',
311          new StandardColumn(this.getChildColumn('is_main_thread')),
312        ],
313      ]);
314  }
315
316  initialColumns(): TableColumn[] {
317    return [
318      this,
319      new StandardColumn(this.getChildColumn('pid')),
320      new StandardColumn(this.getChildColumn('name')),
321    ];
322  }
323
324  private getChildColumn(name: string): SqlColumn {
325    return {
326      column: name,
327      source: {
328        table: 'process',
329        joinOn: {id: this.column},
330        // If the column is guaranteed not to have null values, we can use an INNER JOIN.
331        innerJoin: this.params?.notNull === true,
332      },
333    };
334  }
335}
336
337class ArgColumn implements TableColumn<{type: SqlColumn}> {
338  public readonly column: SqlColumn;
339  private id: string;
340
341  constructor(
342    private argSetId: SqlColumn,
343    private key: string,
344  ) {
345    this.id = `${sqlColumnId(this.argSetId)}[${this.key}]`;
346    this.column = new SqlExpression(
347      (cols: string[]) => `COALESCE(${cols[0]}, ${cols[1]}, ${cols[2]})`,
348      [
349        this.getRawColumn('string_value'),
350        this.getRawColumn('int_value'),
351        this.getRawColumn('real_value'),
352      ],
353      this.id,
354    );
355  }
356
357  supportingColumns() {
358    return {type: this.getRawColumn('value_type')};
359  }
360
361  private getRawColumn(
362    type: 'string_value' | 'int_value' | 'real_value' | 'id' | 'value_type',
363  ): SqlColumn {
364    return {
365      column: type,
366      source: {
367        table: 'args',
368        joinOn: {
369          arg_set_id: this.argSetId,
370          key: `${sqliteString(this.key)}`,
371        },
372      },
373      id: `${this.id}.${type.replace(/_value$/g, '')}`,
374    };
375  }
376
377  renderCell(
378    value: SqlValue,
379    tableManager?: TableManager,
380    values?: {type: SqlValue},
381  ): m.Children {
382    // If the value is NULL, then filters can check for id column for better performance.
383    if (value === null) {
384      return renderStandardCell(
385        value,
386        this.getRawColumn('value_type'),
387        tableManager,
388      );
389    }
390    if (values?.type === 'int') {
391      return renderStandardCell(
392        value,
393        this.getRawColumn('int_value'),
394        tableManager,
395      );
396    }
397    if (values?.type === 'string') {
398      return renderStandardCell(
399        value,
400        this.getRawColumn('string_value'),
401        tableManager,
402      );
403    }
404    if (values?.type === 'real') {
405      return renderStandardCell(
406        value,
407        this.getRawColumn('real_value'),
408        tableManager,
409      );
410    }
411    return renderStandardCell(value, this.column, tableManager);
412  }
413}
414
415export class ArgSetIdColumn implements TableColumn {
416  constructor(public readonly column: SqlColumn) {}
417
418  renderCell(value: SqlValue, tableManager: TableManager): m.Children {
419    return renderStandardCell(value, this.column, tableManager);
420  }
421
422  listDerivedColumns(manager: TableManager) {
423    return async () => {
424      const queryResult = await manager.trace.engine.query(`
425        SELECT
426          DISTINCT args.key
427        FROM (${manager.getSqlQuery({arg_set_id: this.column})}) data
428        JOIN args USING (arg_set_id)
429      `);
430      const result = new Map();
431      const it = queryResult.iter({key: STR});
432      for (; it.valid(); it.next()) {
433        result.set(it.key, argTableColumn(this.column, it.key));
434      }
435      return result;
436    };
437  }
438
439  initialColumns() {
440    return [];
441  }
442}
443
444export function argTableColumn(argSetId: SqlColumn, key: string): TableColumn {
445  return new ArgColumn(argSetId, key);
446}
447