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