xref: /aosp_15_r20/external/perfetto/ui/src/components/widgets/sql/table/well_known_columns.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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 {Icons} from '../../../../base/semantic_icons';
17import {sqliteString} from '../../../../base/string_utils';
18import {Duration, Time} from '../../../../base/time';
19import {SqlValue, STR} from '../../../../trace_processor/query_result';
20import {
21  asSchedSqlId,
22  asSliceSqlId,
23  asThreadStateSqlId,
24  asUpid,
25  asUtid,
26} from '../../../sql_utils/core_types';
27import {getProcessName} from '../../../sql_utils/process';
28import {getThreadName} from '../../../sql_utils/thread';
29import {Anchor} from '../../../../widgets/anchor';
30import {renderError} from '../../../../widgets/error';
31import {MenuDivider, MenuItem, PopupMenu2} from '../../../../widgets/menu';
32import {DurationWidget} from '../../../widgets/duration';
33import {processRefMenuItems, showProcessDetailsMenuItem} from '../../process';
34import {SchedRef} from '../../sched';
35import {SliceRef} from '../../slice';
36import {
37  showThreadDetailsMenuItem,
38  threadRefMenuItems,
39} from '../../../widgets/thread';
40import {ThreadStateRef} from '../../thread_state';
41import {Timestamp} from '../../timestamp';
42import {
43  AggregationConfig,
44  SourceTable,
45  SqlColumn,
46  sqlColumnId,
47  TableColumn,
48  TableColumnSet,
49  TableManager,
50} from './column';
51import {
52  displayValue,
53  getStandardContextMenuItems,
54  getStandardFilters,
55  renderStandardCell,
56} from './render_cell_utils';
57
58export type ColumnParams = {
59  alias?: string;
60  startsHidden?: boolean;
61  title?: string;
62};
63
64export type StandardColumnParams = ColumnParams & {
65  aggregationType?: 'nominal' | 'quantitative';
66};
67
68export interface IdColumnParams {
69  // Whether the column is guaranteed not to have null values.
70  // (this will allow us to upgrage the joins on this column to more performant INNER JOINs).
71  notNull?: boolean;
72}
73
74type ColumnSetParams = {
75  title: string;
76  startsHidden?: boolean;
77};
78
79export class StandardColumn extends TableColumn {
80  constructor(
81    private column: SqlColumn,
82    private params?: StandardColumnParams,
83  ) {
84    super(params);
85  }
86
87  primaryColumn(): SqlColumn {
88    return this.column;
89  }
90
91  aggregation(): AggregationConfig {
92    return {dataType: this.params?.aggregationType};
93  }
94
95  getTitle() {
96    return this.params?.title;
97  }
98
99  renderCell(value: SqlValue, tableManager: TableManager): m.Children {
100    return renderStandardCell(value, this.column, tableManager);
101  }
102}
103
104export class TimestampColumn extends TableColumn {
105  constructor(
106    private column: SqlColumn,
107    private params?: ColumnParams,
108  ) {
109    super(params);
110  }
111
112  primaryColumn(): SqlColumn {
113    return this.column;
114  }
115
116  getTitle() {
117    return this.params?.title;
118  }
119
120  renderCell(value: SqlValue, tableManager: TableManager): m.Children {
121    if (typeof value !== 'bigint') {
122      return renderStandardCell(value, this.column, tableManager);
123    }
124    return m(Timestamp, {
125      ts: Time.fromRaw(value),
126      extraMenuItems: getStandardContextMenuItems(
127        value,
128        this.column,
129        tableManager,
130      ),
131    });
132  }
133}
134
135export class DurationColumn extends TableColumn {
136  constructor(
137    private column: SqlColumn,
138    private params?: ColumnParams,
139  ) {
140    super(params);
141  }
142
143  primaryColumn(): SqlColumn {
144    return this.column;
145  }
146
147  getTitle() {
148    return this.params?.title;
149  }
150
151  renderCell(value: SqlValue, tableManager: TableManager): m.Children {
152    if (typeof value !== 'bigint') {
153      return renderStandardCell(value, this.column, tableManager);
154    }
155
156    return m(DurationWidget, {
157      dur: Duration.fromRaw(value),
158      extraMenuItems: getStandardContextMenuItems(
159        value,
160        this.column,
161        tableManager,
162      ),
163    });
164  }
165}
166
167function wrongTypeError(type: string, name: SqlColumn, value: SqlValue) {
168  return renderError(
169    `Wrong type for ${type} column ${sqlColumnId(name)}: bigint expected, ${typeof value} found`,
170  );
171}
172
173export class SliceIdColumn extends TableColumn {
174  private columns: {ts: SqlColumn; dur: SqlColumn; trackId: SqlColumn};
175
176  constructor(
177    private id: SqlColumn,
178    private params?: ColumnParams & IdColumnParams,
179  ) {
180    super(params);
181
182    const sliceTable: SourceTable = {
183      table: 'slice',
184      joinOn: {id: this.id},
185      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
186      innerJoin: this.params?.notNull === true,
187    };
188
189    this.columns = {
190      ts: {
191        column: 'ts',
192        source: sliceTable,
193      },
194      dur: {
195        column: 'dur',
196        source: sliceTable,
197      },
198      trackId: {
199        column: 'track_id',
200        source: sliceTable,
201      },
202    };
203  }
204
205  primaryColumn(): SqlColumn {
206    return this.id;
207  }
208
209  getTitle() {
210    return this.params?.title;
211  }
212
213  dependentColumns() {
214    return this.columns;
215  }
216
217  renderCell(
218    value: SqlValue,
219    manager: TableManager,
220    data: {[key: string]: SqlValue},
221  ): m.Children {
222    const id = value;
223    const ts = data['ts'];
224    const dur = data['dur'] === null ? -1n : data['dur'];
225    const trackId = data['trackId'];
226
227    if (id === null) {
228      return renderStandardCell(id, this.id, manager);
229    }
230    if (ts === null || trackId === null) {
231      return renderError(`Slice with id ${id} not found`);
232    }
233    if (typeof id !== 'bigint') return wrongTypeError('id', this.id, id);
234    if (typeof ts !== 'bigint') {
235      return wrongTypeError('timestamp', this.columns.ts, ts);
236    }
237    if (typeof dur !== 'bigint') {
238      return wrongTypeError('duration', this.columns.dur, dur);
239    }
240    if (typeof trackId !== 'bigint') {
241      return wrongTypeError('track id', this.columns.trackId, trackId);
242    }
243
244    return m(SliceRef, {
245      id: asSliceSqlId(Number(id)),
246      name: `${id}`,
247      switchToCurrentSelectionTab: false,
248    });
249  }
250}
251
252export class SliceColumnSet extends TableColumnSet {
253  constructor(
254    private id: SqlColumn,
255    private params?: ColumnSetParams,
256  ) {
257    super();
258  }
259
260  getTitle(): string {
261    return this.params?.title ?? `${sqlColumnId(this.id)} (slice)`;
262  }
263
264  async discover(): Promise<
265    {key: string; column: TableColumn | TableColumnSet}[]
266  > {
267    const column: (name: string) => SqlColumn = (name) => {
268      return {
269        column: name,
270        source: {
271          table: 'slice',
272          joinOn: {id: this.id},
273        },
274      };
275    };
276
277    return [
278      {
279        key: 'id',
280        column: new SliceIdColumn(this.id),
281      },
282      {
283        key: 'ts',
284        column: new TimestampColumn(column('ts')),
285      },
286      {
287        key: 'dur',
288        column: new DurationColumn(column('dur')),
289      },
290      {
291        key: 'name',
292        column: new StandardColumn(column('name')),
293      },
294      {
295        key: 'thread_dur',
296        column: new StandardColumn(column('thread_dur')),
297      },
298      {
299        key: 'parent_id',
300        column: new SliceColumnSet(column('parent_id')),
301      },
302    ];
303  }
304
305  initialColumns(): TableColumn[] {
306    if (this.params?.startsHidden) return [];
307    return [new SliceIdColumn(this.id)];
308  }
309}
310
311export class SchedIdColumn extends TableColumn {
312  private columns: {ts: SqlColumn; dur: SqlColumn; cpu: SqlColumn};
313
314  constructor(
315    private id: SqlColumn,
316    private params?: ColumnParams & IdColumnParams,
317  ) {
318    super(params);
319
320    const schedTable: SourceTable = {
321      table: 'sched',
322      joinOn: {id: this.id},
323      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
324      innerJoin: this.params?.notNull === true,
325    };
326
327    this.columns = {
328      ts: {
329        column: 'ts',
330        source: schedTable,
331      },
332      dur: {
333        column: 'dur',
334        source: schedTable,
335      },
336      cpu: {
337        column: 'cpu',
338        source: schedTable,
339      },
340    };
341  }
342
343  primaryColumn(): SqlColumn {
344    return this.id;
345  }
346
347  getTitle() {
348    return this.params?.title;
349  }
350
351  dependentColumns() {
352    return {
353      ts: this.columns.ts,
354      dur: this.columns.dur,
355      cpu: this.columns.cpu,
356    };
357  }
358
359  renderCell(
360    value: SqlValue,
361    manager: TableManager,
362    data: {[key: string]: SqlValue},
363  ): m.Children {
364    const id = value;
365    const ts = data['ts'];
366    const dur = data['dur'] === null ? -1n : data['dur'];
367    const cpu = data['cpu'];
368
369    if (id === null) {
370      return renderStandardCell(id, this.id, manager);
371    }
372    if (ts === null || cpu === null) {
373      return renderError(`Sched with id ${id} not found`);
374    }
375    if (typeof id !== 'bigint') return wrongTypeError('id', this.id, id);
376    if (typeof ts !== 'bigint') {
377      return wrongTypeError('timestamp', this.columns.ts, ts);
378    }
379    if (typeof dur !== 'bigint') {
380      return wrongTypeError('duration', this.columns.dur, dur);
381    }
382    if (typeof cpu !== 'bigint') {
383      return wrongTypeError('track id', this.columns.cpu, cpu);
384    }
385
386    return m(SchedRef, {
387      id: asSchedSqlId(Number(id)),
388      name: `${id}`,
389      switchToCurrentSelectionTab: false,
390    });
391  }
392}
393
394export class ThreadStateIdColumn extends TableColumn {
395  private columns: {ts: SqlColumn; dur: SqlColumn; utid: SqlColumn};
396
397  constructor(
398    private id: SqlColumn,
399    private params?: ColumnParams & IdColumnParams,
400  ) {
401    super(params);
402
403    const threadStateTable: SourceTable = {
404      table: 'thread_state',
405      joinOn: {id: this.id},
406      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
407      innerJoin: this.params?.notNull === true,
408    };
409
410    this.columns = {
411      ts: {
412        column: 'ts',
413        source: threadStateTable,
414      },
415      dur: {
416        column: 'dur',
417        source: threadStateTable,
418      },
419      utid: {
420        column: 'utid',
421        source: threadStateTable,
422      },
423    };
424  }
425
426  primaryColumn(): SqlColumn {
427    return this.id;
428  }
429
430  getTitle() {
431    return this.params?.title;
432  }
433
434  dependentColumns() {
435    return {
436      ts: this.columns.ts,
437      dur: this.columns.dur,
438      utid: this.columns.utid,
439    };
440  }
441
442  renderCell(
443    value: SqlValue,
444    manager: TableManager,
445    data: {[key: string]: SqlValue},
446  ): m.Children {
447    const id = value;
448    const ts = data['ts'];
449    const dur = data['dur'] === null ? -1n : data['dur'];
450    const utid = data['utid'];
451
452    if (id === null) {
453      return renderStandardCell(id, this.id, manager);
454    }
455    if (ts === null || utid === null) {
456      return renderError(`Thread state with id ${id} not found`);
457    }
458    if (typeof id !== 'bigint') return wrongTypeError('id', this.id, id);
459    if (typeof ts !== 'bigint') {
460      return wrongTypeError('timestamp', this.columns.ts, ts);
461    }
462    if (typeof dur !== 'bigint') {
463      return wrongTypeError('duration', this.columns.dur, dur);
464    }
465    if (typeof utid !== 'bigint') {
466      return wrongTypeError('track id', this.columns.utid, utid);
467    }
468
469    return m(ThreadStateRef, {
470      id: asThreadStateSqlId(Number(id)),
471      name: `${id}`,
472      switchToCurrentSelectionTab: false,
473    });
474  }
475}
476
477export class ThreadColumn extends TableColumn {
478  private columns: {name: SqlColumn; tid: SqlColumn};
479
480  constructor(
481    private utid: SqlColumn,
482    private params?: ColumnParams & IdColumnParams,
483  ) {
484    // Both ThreadColumn and ThreadIdColumn are referencing the same underlying SQL column as primary,
485    // so we have to use tag to distinguish them.
486    super({tag: 'thread', ...params});
487
488    const threadTable: SourceTable = {
489      table: 'thread',
490      joinOn: {id: this.utid},
491      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
492      innerJoin: this.params?.notNull === true,
493    };
494
495    this.columns = {
496      name: {
497        column: 'name',
498        source: threadTable,
499      },
500      tid: {
501        column: 'tid',
502        source: threadTable,
503      },
504    };
505  }
506
507  primaryColumn(): SqlColumn {
508    return this.utid;
509  }
510
511  getTitle() {
512    if (this.params?.title !== undefined) return this.params.title;
513    return `${sqlColumnId(this.utid)} (thread)`;
514  }
515
516  dependentColumns() {
517    return {
518      tid: this.columns.tid,
519      name: this.columns.name,
520    };
521  }
522
523  renderCell(
524    value: SqlValue,
525    manager: TableManager,
526    data: {[key: string]: SqlValue},
527  ): m.Children {
528    const utid = value;
529    const rawTid = data['tid'];
530    const rawName = data['name'];
531
532    if (utid === null) {
533      return renderStandardCell(utid, this.utid, manager);
534    }
535    if (typeof utid !== 'bigint') {
536      return wrongTypeError('utid', this.utid, utid);
537    }
538    if (rawTid !== null && typeof rawTid !== 'bigint') {
539      return wrongTypeError('tid', this.columns.tid, rawTid);
540    }
541    if (rawName !== null && typeof rawName !== 'string') {
542      return wrongTypeError('name', this.columns.name, rawName);
543    }
544
545    const name: string | undefined = rawName ?? undefined;
546    const tid: number | undefined =
547      rawTid !== null ? Number(rawTid) : undefined;
548
549    return m(
550      PopupMenu2,
551      {
552        trigger: m(
553          Anchor,
554          getThreadName({
555            name: name ?? undefined,
556            tid: tid !== null ? Number(tid) : undefined,
557          }),
558        ),
559      },
560      threadRefMenuItems({utid: asUtid(Number(utid)), name, tid}),
561      m(MenuDivider),
562      m(
563        MenuItem,
564        {
565          label: 'Add filter',
566          icon: Icons.Filter,
567        },
568        m(
569          MenuItem,
570          {
571            label: 'utid',
572          },
573          getStandardFilters(utid, this.utid, manager),
574        ),
575        m(
576          MenuItem,
577          {
578            label: 'thread name',
579          },
580          getStandardFilters(rawName, this.columns.name, manager),
581        ),
582        m(
583          MenuItem,
584          {
585            label: 'tid',
586          },
587          getStandardFilters(rawTid, this.columns.tid, manager),
588        ),
589      ),
590    );
591  }
592
593  aggregation(): AggregationConfig {
594    return {
595      dataType: 'nominal',
596    };
597  }
598}
599
600// ThreadIdColumn is a column type for displaying primary key of the `thread` table.
601// All other references (foreign keys) should use `ThreadColumn` instead.
602export class ThreadIdColumn extends TableColumn {
603  private columns: {tid: SqlColumn};
604
605  constructor(private utid: SqlColumn) {
606    super({});
607
608    const threadTable: SourceTable = {
609      table: 'thread',
610      joinOn: {id: this.utid},
611      innerJoin: true,
612    };
613
614    this.columns = {
615      tid: {
616        column: 'tid',
617        source: threadTable,
618      },
619    };
620  }
621
622  primaryColumn(): SqlColumn {
623    return this.utid;
624  }
625
626  getTitle() {
627    return 'utid';
628  }
629
630  dependentColumns() {
631    return {
632      tid: this.columns.tid,
633    };
634  }
635
636  renderCell(
637    value: SqlValue,
638    manager: TableManager,
639    data: {[key: string]: SqlValue},
640  ): m.Children {
641    const utid = value;
642    const rawTid = data['tid'];
643
644    if (utid === null) {
645      return renderStandardCell(utid, this.utid, manager);
646    }
647
648    if (typeof utid !== 'bigint') {
649      throw new Error(
650        `thread.utid is expected to be bigint, got ${typeof utid}`,
651      );
652    }
653
654    return m(
655      PopupMenu2,
656      {
657        trigger: m(Anchor, `${utid}`),
658      },
659
660      showThreadDetailsMenuItem(
661        asUtid(Number(utid)),
662        rawTid === null ? undefined : Number(rawTid),
663      ),
664      getStandardContextMenuItems(utid, this.utid, manager),
665    );
666  }
667
668  aggregation(): AggregationConfig {
669    return {dataType: 'nominal'};
670  }
671}
672
673export class ThreadColumnSet extends TableColumnSet {
674  constructor(
675    private id: SqlColumn,
676    private params: ColumnSetParams & {
677      notNull?: boolean;
678    },
679  ) {
680    super();
681  }
682
683  getTitle(): string {
684    return `${this.params.title} (thread)`;
685  }
686
687  initialColumns(): TableColumn[] {
688    if (this.params.startsHidden === true) return [];
689    return [new ThreadColumn(this.id)];
690  }
691
692  async discover() {
693    const column: (name: string) => SqlColumn = (name) => ({
694      column: name,
695      source: {
696        table: 'thread',
697        joinOn: {id: this.id},
698      },
699      innerJoin: this.params.notNull === true,
700    });
701
702    return [
703      {
704        key: 'thread',
705        column: new ThreadColumn(this.id),
706      },
707      {
708        key: 'utid',
709        column: new ThreadIdColumn(this.id),
710      },
711      {
712        key: 'tid',
713        column: new StandardColumn(column('tid'), {aggregationType: 'nominal'}),
714      },
715      {
716        key: 'name',
717        column: new StandardColumn(column('name')),
718      },
719      {
720        key: 'start_ts',
721        column: new TimestampColumn(column('start_ts')),
722      },
723      {
724        key: 'end_ts',
725        column: new TimestampColumn(column('end_ts')),
726      },
727      {
728        key: 'upid',
729        column: new ProcessColumnSet(column('upid'), {title: 'upid'}),
730      },
731      {
732        key: 'is_main_thread',
733        column: new StandardColumn(column('is_main_thread'), {
734          aggregationType: 'nominal',
735        }),
736      },
737    ];
738  }
739}
740
741export class ProcessColumn extends TableColumn {
742  private columns: {name: SqlColumn; pid: SqlColumn};
743
744  constructor(
745    private upid: SqlColumn,
746    private params?: ColumnParams & IdColumnParams,
747  ) {
748    // Both ProcessColumn and ProcessIdColumn are referencing the same underlying SQL column as primary,
749    // so we have to use tag to distinguish them.
750    super({tag: 'process', ...params});
751
752    const processTable: SourceTable = {
753      table: 'process',
754      joinOn: {id: this.upid},
755      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
756      innerJoin: this.params?.notNull === true,
757    };
758
759    this.columns = {
760      name: {
761        column: 'name',
762        source: processTable,
763      },
764      pid: {
765        column: 'pid',
766        source: processTable,
767      },
768    };
769  }
770
771  primaryColumn(): SqlColumn {
772    return this.upid;
773  }
774
775  getTitle() {
776    if (this.params?.title !== undefined) return this.params.title;
777    return `${sqlColumnId(this.upid)} (process)`;
778  }
779
780  dependentColumns() {
781    return this.columns;
782  }
783
784  renderCell(
785    value: SqlValue,
786    manager: TableManager,
787    data: {[key: string]: SqlValue},
788  ): m.Children {
789    const upid = value;
790    const rawPid = data['pid'];
791    const rawName = data['name'];
792
793    if (upid === null) {
794      return renderStandardCell(upid, this.upid, manager);
795    }
796    if (typeof upid !== 'bigint') {
797      return wrongTypeError('upid', this.upid, upid);
798    }
799    if (rawPid !== null && typeof rawPid !== 'bigint') {
800      return wrongTypeError('pid', this.columns.pid, rawPid);
801    }
802    if (rawName !== null && typeof rawName !== 'string') {
803      return wrongTypeError('name', this.columns.name, rawName);
804    }
805
806    const name: string | undefined = rawName ?? undefined;
807    const pid: number | undefined =
808      rawPid !== null ? Number(rawPid) : undefined;
809
810    return m(
811      PopupMenu2,
812      {
813        trigger: m(
814          Anchor,
815          getProcessName({
816            name: name ?? undefined,
817            pid: pid !== null ? Number(pid) : undefined,
818          }),
819        ),
820      },
821      processRefMenuItems({upid: asUpid(Number(upid)), name, pid}),
822      m(MenuDivider),
823      m(
824        MenuItem,
825        {
826          label: 'Add filter',
827          icon: Icons.Filter,
828        },
829        m(
830          MenuItem,
831          {
832            label: 'upid',
833          },
834          getStandardFilters(upid, this.upid, manager),
835        ),
836        m(
837          MenuItem,
838          {
839            label: 'process name',
840          },
841          getStandardFilters(rawName, this.columns.name, manager),
842        ),
843        m(
844          MenuItem,
845          {
846            label: 'tid',
847          },
848          getStandardFilters(rawPid, this.columns.pid, manager),
849        ),
850      ),
851    );
852  }
853
854  aggregation(): AggregationConfig {
855    return {
856      dataType: 'nominal',
857    };
858  }
859}
860
861// ProcessIdColumn is a column type for displaying primary key of the `process` table.
862// All other references (foreign keys) should use `ProcessColumn` instead.
863export class ProcessIdColumn extends TableColumn {
864  private columns: {pid: SqlColumn};
865
866  constructor(private upid: SqlColumn) {
867    super({});
868
869    const processTable: SourceTable = {
870      table: 'process',
871      joinOn: {id: this.upid},
872      innerJoin: true,
873    };
874
875    this.columns = {
876      pid: {
877        column: 'pid',
878        source: processTable,
879      },
880    };
881  }
882
883  primaryColumn(): SqlColumn {
884    return this.upid;
885  }
886
887  getTitle() {
888    return 'upid';
889  }
890
891  dependentColumns() {
892    return {
893      pid: this.columns.pid,
894    };
895  }
896
897  renderCell(
898    value: SqlValue,
899    manager: TableManager,
900    data: {[key: string]: SqlValue},
901  ): m.Children {
902    const upid = value;
903    const rawPid = data['pid'];
904
905    if (upid === null) {
906      return renderStandardCell(upid, this.upid, manager);
907    }
908
909    if (typeof upid !== 'bigint') {
910      throw new Error(
911        `process.upid is expected to be bigint, got ${typeof upid}`,
912      );
913    }
914
915    return m(
916      PopupMenu2,
917      {
918        trigger: m(Anchor, `${upid}`),
919      },
920
921      showProcessDetailsMenuItem(
922        asUpid(Number(upid)),
923        rawPid === null ? undefined : Number(rawPid),
924      ),
925      getStandardContextMenuItems(upid, this.upid, manager),
926    );
927  }
928
929  aggregation(): AggregationConfig {
930    return {dataType: 'nominal'};
931  }
932}
933
934export class ProcessColumnSet extends TableColumnSet {
935  constructor(
936    private id: SqlColumn,
937    private params: ColumnSetParams & {
938      notNull?: boolean;
939    },
940  ) {
941    super();
942  }
943
944  getTitle(): string {
945    return `${this.params.title} (process)`;
946  }
947
948  initialColumns(): TableColumn[] {
949    if (this.params.startsHidden === true) return [];
950    return [new ProcessColumn(this.id)];
951  }
952
953  async discover() {
954    const column: (name: string) => SqlColumn = (name) => ({
955      column: name,
956      source: {
957        table: 'process',
958        joinOn: {id: this.id},
959      },
960      innerJoin: this.params.notNull === true,
961    });
962
963    return [
964      {
965        key: 'process',
966        column: new ProcessColumn(this.id),
967      },
968      {
969        key: 'upid',
970        column: new ProcessIdColumn(this.id),
971      },
972      {
973        key: 'pid',
974        column: new StandardColumn(column('pid'), {aggregationType: 'nominal'}),
975      },
976      {
977        key: 'name',
978        column: new StandardColumn(column('name')),
979      },
980      {
981        key: 'start_ts',
982        column: new TimestampColumn(column('start_ts')),
983      },
984      {
985        key: 'end_ts',
986        column: new TimestampColumn(column('end_ts')),
987      },
988      {
989        key: 'parent_upid',
990        column: new ProcessColumnSet(column('parent_upid'), {
991          title: 'parent_upid',
992        }),
993      },
994      {
995        key: 'uid',
996        column: new StandardColumn(column('uid'), {aggregationType: 'nominal'}),
997      },
998      {
999        key: 'android_appid',
1000        column: new StandardColumn(column('android_appid'), {
1001          aggregationType: 'nominal',
1002        }),
1003      },
1004      {
1005        key: 'cmdline',
1006        column: new StandardColumn(column('cmdline')),
1007      },
1008      {
1009        key: 'arg_set_id (args)',
1010        column: new ArgSetColumnSet(column('arg_set_id')),
1011      },
1012    ];
1013  }
1014}
1015
1016class ArgColumn extends TableColumn {
1017  private displayValue: SqlColumn;
1018  private stringValue: SqlColumn;
1019  private intValue: SqlColumn;
1020  private realValue: SqlColumn;
1021
1022  constructor(
1023    private argSetId: SqlColumn,
1024    private key: string,
1025  ) {
1026    super();
1027
1028    const argTable: SourceTable = {
1029      table: 'args',
1030      joinOn: {
1031        arg_set_id: argSetId,
1032        key: sqliteString(key),
1033      },
1034    };
1035
1036    this.displayValue = {
1037      column: 'display_value',
1038      source: argTable,
1039    };
1040    this.stringValue = {
1041      column: 'string_value',
1042      source: argTable,
1043    };
1044    this.intValue = {
1045      column: 'int_value',
1046      source: argTable,
1047    };
1048    this.realValue = {
1049      column: 'real_value',
1050      source: argTable,
1051    };
1052  }
1053
1054  override primaryColumn(): SqlColumn {
1055    return this.displayValue;
1056  }
1057
1058  override sortColumns(): SqlColumn[] {
1059    return [this.stringValue, this.intValue, this.realValue];
1060  }
1061
1062  override dependentColumns() {
1063    return {
1064      stringValue: this.stringValue,
1065      intValue: this.intValue,
1066      realValue: this.realValue,
1067    };
1068  }
1069
1070  getTitle() {
1071    return `${sqlColumnId(this.argSetId)}[${this.key}]`;
1072  }
1073
1074  renderCell(
1075    value: SqlValue,
1076    tableManager: TableManager,
1077    dependentColumns: {[key: string]: SqlValue},
1078  ): m.Children {
1079    const strValue = dependentColumns['stringValue'];
1080    const intValue = dependentColumns['intValue'];
1081    const realValue = dependentColumns['realValue'];
1082
1083    let contextMenuItems: m.Child[] = [];
1084    if (strValue !== null) {
1085      contextMenuItems = getStandardContextMenuItems(
1086        strValue,
1087        this.stringValue,
1088        tableManager,
1089      );
1090    } else if (intValue !== null) {
1091      contextMenuItems = getStandardContextMenuItems(
1092        intValue,
1093        this.intValue,
1094        tableManager,
1095      );
1096    } else if (realValue !== null) {
1097      contextMenuItems = getStandardContextMenuItems(
1098        realValue,
1099        this.realValue,
1100        tableManager,
1101      );
1102    } else {
1103      contextMenuItems = getStandardContextMenuItems(
1104        value,
1105        this.displayValue,
1106        tableManager,
1107      );
1108    }
1109    return m(
1110      PopupMenu2,
1111      {
1112        trigger: m(Anchor, displayValue(value)),
1113      },
1114      ...contextMenuItems,
1115    );
1116  }
1117}
1118
1119export class ArgSetColumnSet extends TableColumnSet {
1120  constructor(
1121    private column: SqlColumn,
1122    private title?: string,
1123  ) {
1124    super();
1125  }
1126
1127  getTitle(): string {
1128    return this.title ?? sqlColumnId(this.column);
1129  }
1130
1131  async discover(
1132    manager: TableManager,
1133  ): Promise<{key: string; column: TableColumn}[]> {
1134    const queryResult = await manager.trace.engine.query(`
1135      -- Encapsulate the query in a CTE to avoid clashes between filters
1136      -- and columns of the 'args' table.
1137      SELECT
1138        DISTINCT args.key
1139      FROM (${manager.getSqlQuery({arg_set_id: this.column})}) data
1140      JOIN args USING (arg_set_id)
1141    `);
1142    const result = [];
1143    const it = queryResult.iter({key: STR});
1144    for (; it.valid(); it.next()) {
1145      result.push({
1146        key: it.key,
1147        column: argTableColumn(this.column, it.key),
1148      });
1149    }
1150    return result;
1151  }
1152}
1153
1154export function argSqlColumn(argSetId: SqlColumn, key: string): SqlColumn {
1155  return {
1156    column: 'display_value',
1157    source: {
1158      table: 'args',
1159      joinOn: {
1160        arg_set_id: argSetId,
1161        key: sqliteString(key),
1162      },
1163    },
1164  };
1165}
1166
1167export function argTableColumn(argSetId: SqlColumn, key: string) {
1168  return new ArgColumn(argSetId, key);
1169}
1170