@parca/profile 0.16.228 → 0.16.229

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,10 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.16.229](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.228...@parca/profile@0.16.229) (2023-08-23)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
6
10
  ## [0.16.228](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.227...@parca/profile@0.16.228) (2023-08-23)
7
11
 
8
12
  **Note:** Version bump only for package @parca/profile
@@ -1,5 +1,5 @@
1
- import { Table } from 'apache-arrow';
2
- import { Callgraph as CallgraphType, Flamegraph, QueryServiceClient, Source, Top } from '@parca/client';
1
+ import { Table as ArrowTable } from 'apache-arrow';
2
+ import { Callgraph as CallgraphType, Flamegraph, QueryServiceClient, Source, TableArrow, Top } from '@parca/client';
3
3
  import { ProfileSource } from '../ProfileSource';
4
4
  type NavigateFunction = (path: string, queryParams: any, options?: {
5
5
  replace?: boolean;
@@ -7,13 +7,14 @@ type NavigateFunction = (path: string, queryParams: any, options?: {
7
7
  export interface FlamegraphData {
8
8
  loading: boolean;
9
9
  data?: Flamegraph;
10
- table?: Table<any>;
10
+ table?: ArrowTable<any>;
11
11
  total?: bigint;
12
12
  filtered?: bigint;
13
13
  error?: any;
14
14
  }
15
15
  export interface TopTableData {
16
16
  loading: boolean;
17
+ arrow?: TableArrow;
17
18
  data?: Top;
18
19
  total?: bigint;
19
20
  filtered?: bigint;
@@ -25,7 +25,7 @@ import { Callgraph } from '../';
25
25
  import { jsonToDot } from '../Callgraph/utils';
26
26
  import ProfileIcicleGraph from '../ProfileIcicleGraph';
27
27
  import { SourceView } from '../SourceView';
28
- import { TopTable } from '../TopTable';
28
+ import Table from '../Table';
29
29
  import ProfileShareButton from '../components/ProfileShareButton';
30
30
  import useDelayedLoader from '../useDelayedLoader';
31
31
  import FilterByFunctionButton from './FilterByFunctionButton';
@@ -141,7 +141,7 @@ export const ProfileView = ({ total, filtered, flamegraphData, topTableData, cal
141
141
  dimensions?.width !== undefined ? (_jsx(Callgraph, { data: callgraphData.data, svgString: callgraphSVG, sampleUnit: sampleUnit, width: isHalfScreen ? dimensions?.width / 2 : dimensions?.width })) : (_jsx(_Fragment, {}));
142
142
  }
143
143
  case 'table': {
144
- return topTableData != null ? (_jsx(TopTable, { loading: topTableData.loading, data: topTableData.data, sampleUnit: sampleUnit, navigateTo: navigateTo, setActionButtons: setActionButtons, currentSearchString: currentSearchString })) : (_jsx(_Fragment, {}));
144
+ return topTableData != null ? (_jsx(Table, { loading: topTableData.loading, data: topTableData.arrow?.record, sampleUnit: sampleUnit, navigateTo: navigateTo, setActionButtons: setActionButtons, currentSearchString: currentSearchString })) : (_jsx(_Fragment, {}));
145
145
  }
146
146
  case 'source': {
147
147
  return sourceData != null ? (_jsx(SourceView, { loading: sourceData.loading, data: sourceData.data, total: total, filtered: filtered, setActionButtons: setActionButtons })) : (_jsx(_Fragment, {}));
@@ -52,7 +52,7 @@ export const ProfileViewWithData = ({ queryClient, profileSource, navigateTo, })
52
52
  groupBy: groupByParam,
53
53
  });
54
54
  const { perf } = useParcaContext();
55
- const { isLoading: topTableLoading, response: topTableResponse, error: topTableError, } = useQuery(queryClient, profileSource, QueryRequest_ReportType.TOP, {
55
+ const { isLoading: tableLoading, response: tableResponse, error: tableError, } = useQuery(queryClient, profileSource, QueryRequest_ReportType.TABLE_ARROW, {
56
56
  skip: !dashboardItems.includes('table'),
57
57
  });
58
58
  const { isLoading: callgraphLoading, response: callgraphResponse, error: callgraphError, } = useQuery(queryClient, profileSource, QueryRequest_ReportType.CALLGRAPH, {
@@ -68,8 +68,8 @@ export const ProfileViewWithData = ({ queryClient, profileSource, navigateTo, })
68
68
  flamegraphResponse?.report.oneofKind === 'flamegraphArrow') {
69
69
  perf?.markInteraction('Flamegraph render', flamegraphResponse.total);
70
70
  }
71
- if (!topTableLoading && topTableResponse?.report.oneofKind === 'top') {
72
- perf?.markInteraction('Top table render', topTableResponse.total);
71
+ if (!tableLoading && tableResponse?.report.oneofKind === 'tableArrow') {
72
+ perf?.markInteraction('table render', tableResponse.total);
73
73
  }
74
74
  if (!callgraphLoading && callgraphResponse?.report.oneofKind === 'callgraph') {
75
75
  perf?.markInteraction('Callgraph render', callgraphResponse.total);
@@ -82,8 +82,8 @@ export const ProfileViewWithData = ({ queryClient, profileSource, navigateTo, })
82
82
  flamegraphResponse,
83
83
  callgraphResponse,
84
84
  callgraphLoading,
85
- topTableLoading,
86
- topTableResponse,
85
+ tableLoading,
86
+ tableResponse,
87
87
  sourceLoading,
88
88
  sourceResponse,
89
89
  perf,
@@ -112,9 +112,9 @@ export const ProfileViewWithData = ({ queryClient, profileSource, navigateTo, })
112
112
  total = BigInt(flamegraphResponse.total);
113
113
  filtered = BigInt(flamegraphResponse.filtered);
114
114
  }
115
- else if (topTableResponse !== null) {
116
- total = BigInt(topTableResponse.total);
117
- filtered = BigInt(topTableResponse.filtered);
115
+ else if (tableResponse !== null) {
116
+ total = BigInt(tableResponse.total);
117
+ filtered = BigInt(tableResponse.filtered);
118
118
  }
119
119
  else if (callgraphResponse !== null) {
120
120
  total = BigInt(callgraphResponse.total);
@@ -136,9 +136,11 @@ export const ProfileViewWithData = ({ queryClient, profileSource, navigateTo, })
136
136
  filtered: BigInt(flamegraphResponse?.filtered ?? '0'),
137
137
  error: flamegraphError,
138
138
  }, topTableData: {
139
- loading: topTableLoading,
140
- data: topTableResponse?.report.oneofKind === 'top' ? topTableResponse.report.top : undefined,
141
- error: topTableError,
139
+ loading: tableLoading,
140
+ arrow: tableResponse?.report.oneofKind === 'tableArrow'
141
+ ? tableResponse.report.tableArrow
142
+ : undefined,
143
+ error: tableError,
142
144
  }, callgraphData: {
143
145
  loading: callgraphLoading,
144
146
  data: callgraphResponse?.report.oneofKind === 'callgraph'
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { Table as ArrowTable } from 'apache-arrow';
3
+ import { type NavigateFunction } from '@parca/utilities';
4
+ interface TableProps {
5
+ data?: Uint8Array;
6
+ sampleUnit: string;
7
+ navigateTo?: NavigateFunction;
8
+ loading: boolean;
9
+ currentSearchString?: string;
10
+ setActionButtons?: (buttons: React.JSX.Element) => void;
11
+ }
12
+ export declare const Table: React.NamedExoticComponent<TableProps>;
13
+ export declare const RowName: (table: ArrowTable, row: number) => string;
14
+ export default Table;
@@ -0,0 +1,184 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // Copyright 2022 The Parca Authors
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
+ import React, { useCallback, useEffect, useMemo } from 'react';
15
+ import { createColumnHelper } from '@tanstack/react-table';
16
+ import { tableFromIPC } from 'apache-arrow';
17
+ import { Button, Table as TableComponent, useURLState } from '@parca/components';
18
+ import { getLastItem, isSearchMatch, parseParams, valueFormatter, } from '@parca/utilities';
19
+ import { hexifyAddress } from '../utils';
20
+ const columnHelper = createColumnHelper();
21
+ export const Table = React.memo(function Table({ data, sampleUnit: unit, navigateTo, loading, currentSearchString, setActionButtons, }) {
22
+ const router = parseParams(window?.location.search);
23
+ const [rawDashboardItems] = useURLState({ param: 'dashboard_items' });
24
+ const [rawcompareMode] = useURLState({ param: 'compare_a' });
25
+ const compareMode = rawcompareMode === undefined ? false : rawcompareMode === 'true';
26
+ const dashboardItems = useMemo(() => {
27
+ if (rawDashboardItems !== undefined) {
28
+ return rawDashboardItems;
29
+ }
30
+ return ['icicle'];
31
+ }, [rawDashboardItems]);
32
+ const columns = useMemo(() => {
33
+ const cols = [
34
+ columnHelper.accessor('flat', {
35
+ header: () => 'Flat',
36
+ cell: info => valueFormatter(info.getValue(), unit, 2),
37
+ size: 80,
38
+ meta: {
39
+ align: 'right',
40
+ },
41
+ invertSorting: true,
42
+ }),
43
+ columnHelper.accessor('flatDiff', {
44
+ header: () => 'Flat Diff',
45
+ cell: info => addPlusSign(valueFormatter(info.getValue(), unit, 2)),
46
+ size: 120,
47
+ meta: {
48
+ align: 'right',
49
+ },
50
+ invertSorting: true,
51
+ }),
52
+ columnHelper.accessor('cumulative', {
53
+ header: () => 'Cumulative',
54
+ cell: info => valueFormatter(info.getValue(), unit, 2),
55
+ size: 130,
56
+ meta: {
57
+ align: 'right',
58
+ },
59
+ invertSorting: true,
60
+ }),
61
+ columnHelper.accessor('cumulativeDiff', {
62
+ header: () => 'Cumulative Diff',
63
+ cell: info => addPlusSign(valueFormatter(info.getValue(), unit, 2)),
64
+ size: 170,
65
+ meta: {
66
+ align: 'right',
67
+ },
68
+ invertSorting: true,
69
+ }),
70
+ columnHelper.accessor('name', {
71
+ header: () => _jsx("span", { className: "text-left", children: "Name" }),
72
+ cell: info => info.getValue(),
73
+ }),
74
+ ];
75
+ return cols;
76
+ }, [unit]);
77
+ const selectSpan = useCallback((span) => {
78
+ if (navigateTo != null) {
79
+ navigateTo('/', {
80
+ ...router,
81
+ ...{ search_string: span.trim() },
82
+ }, { replace: true });
83
+ }
84
+ }, [navigateTo, router]);
85
+ const onRowClick = useCallback((row) => {
86
+ // If there is only one dashboard item, we don't want to select a span
87
+ if (dashboardItems.length <= 1) {
88
+ return;
89
+ }
90
+ selectSpan(row.name);
91
+ }, [selectSpan, dashboardItems.length]);
92
+ const shouldHighlightRow = useCallback((row) => {
93
+ const name = row.name;
94
+ return isSearchMatch(currentSearchString, name);
95
+ }, [currentSearchString]);
96
+ const enableHighlighting = useMemo(() => {
97
+ return currentSearchString != null && currentSearchString?.length > 0;
98
+ }, [currentSearchString]);
99
+ const clearSelection = useCallback(() => {
100
+ if (navigateTo != null) {
101
+ navigateTo('/', {
102
+ ...router,
103
+ ...{ search_string: '' },
104
+ }, { replace: true });
105
+ }
106
+ }, [navigateTo, router]);
107
+ useEffect(() => {
108
+ if (setActionButtons === undefined) {
109
+ return;
110
+ }
111
+ setActionButtons(dashboardItems.length > 1 ? (_jsx(Button, { color: "neutral", onClick: clearSelection, className: "w-auto", variant: "neutral", disabled: currentSearchString === undefined || currentSearchString.length === 0, children: "Clear selection" })) : (_jsx(_Fragment, {})));
112
+ }, [dashboardItems, clearSelection, currentSearchString, setActionButtons]);
113
+ const initialSorting = useMemo(() => {
114
+ return [
115
+ {
116
+ id: compareMode ? 'flatDiff' : 'flat',
117
+ desc: false, // columns sorting are inverted - so this is actually descending
118
+ },
119
+ ];
120
+ }, [compareMode]);
121
+ const columnVisibility = useMemo(() => {
122
+ // TODO: Make this configurable via the UI and add more columns.
123
+ return {
124
+ flat: true,
125
+ flatDiff: compareMode,
126
+ cumulative: true,
127
+ cumulativeDiff: compareMode,
128
+ name: true,
129
+ };
130
+ }, [compareMode]);
131
+ if (loading)
132
+ return _jsx(_Fragment, { children: "Loading..." });
133
+ if (data === undefined)
134
+ return _jsx(_Fragment, { children: "Profile has no samples" });
135
+ const table = tableFromIPC(data);
136
+ const flatColumn = table.getChild('flat');
137
+ const flatDiffColumn = table.getChild('flat_diff');
138
+ const cumulativeColumn = table.getChild('cumulative');
139
+ const cumulativeDiffColumn = table.getChild('cumulative_diff');
140
+ if (table.numRows === 0)
141
+ return _jsx(_Fragment, { children: "Profile has no samples" });
142
+ const rows = [];
143
+ // TODO: Figure out how to only read the data of the columns we need for the virtualized table
144
+ for (let i = 0; i < table.numRows; i++) {
145
+ const flat = flatColumn?.get(i) ?? 0n;
146
+ const flatDiff = flatDiffColumn?.get(i) ?? 0n;
147
+ const cumulative = cumulativeColumn?.get(i) ?? 0n;
148
+ const cumulativeDiff = cumulativeDiffColumn?.get(i) ?? 0n;
149
+ rows.push({
150
+ name: RowName(table, i),
151
+ flat,
152
+ flatDiff,
153
+ cumulative,
154
+ cumulativeDiff,
155
+ });
156
+ }
157
+ return (_jsx("div", { className: "relative", children: _jsx("div", { className: "font-robotoMono h-[80vh] w-full", children: _jsx(TableComponent, { data: rows, columns: columns, initialSorting: initialSorting, columnVisibility: columnVisibility, onRowClick: onRowClick, enableHighlighting: enableHighlighting, shouldHighlightRow: shouldHighlightRow, usePointerCursor: dashboardItems.length > 1 }) }) }));
158
+ });
159
+ const addPlusSign = (num) => {
160
+ if (num.charAt(0) === '0' || num.charAt(0) === '-') {
161
+ return num;
162
+ }
163
+ return `+${num}`;
164
+ };
165
+ export const RowName = (table, row) => {
166
+ const mappingFileColumn = table.getChild('mapping_file');
167
+ if (mappingFileColumn === null) {
168
+ console.error('mapping_file column not found');
169
+ return '';
170
+ }
171
+ const mappingFile = mappingFileColumn?.get(row);
172
+ let mapping = '';
173
+ // Show the last item in the mapping file only if there are more than 1 mappings
174
+ if (mappingFile != null && mappingFileColumn.data.length > 1) {
175
+ mapping = `[${getLastItem(mappingFile) ?? ''}]`;
176
+ }
177
+ const functionName = table.getChild('function_name')?.get(row) ?? '';
178
+ if (functionName !== null) {
179
+ return `${mapping} ${functionName}`;
180
+ }
181
+ const address = table.getChild('location_address')?.get(row) ?? 0;
182
+ return hexifyAddress(address);
183
+ };
184
+ export default Table;
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.16.228",
3
+ "version": "0.16.229",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
- "@parca/client": "^0.16.85",
7
- "@parca/components": "^0.16.181",
6
+ "@parca/client": "^0.16.86",
7
+ "@parca/components": "^0.16.182",
8
8
  "@parca/dynamicsize": "^0.16.54",
9
- "@parca/hooks": "^0.0.20",
9
+ "@parca/hooks": "^0.0.21",
10
10
  "@parca/parser": "^0.16.55",
11
- "@parca/store": "^0.16.98",
12
- "@parca/utilities": "^0.0.27",
11
+ "@parca/store": "^0.16.99",
12
+ "@parca/utilities": "^0.0.28",
13
13
  "@tanstack/react-query": "^4.0.5",
14
14
  "@types/react-beautiful-dnd": "^13.1.3",
15
15
  "apache-arrow": "^12.0.0",
@@ -49,5 +49,5 @@
49
49
  "access": "public",
50
50
  "registry": "https://registry.npmjs.org/"
51
51
  },
52
- "gitHead": "18567fe5daa44cf7a7f214124e01178a82b1e5a1"
52
+ "gitHead": "6cc8241e1dba86446268b2c427330e46fe815fb0"
53
53
  }
@@ -13,7 +13,7 @@
13
13
 
14
14
  import {Profiler, ProfilerProps, useEffect, useMemo, useState} from 'react';
15
15
 
16
- import {Table} from 'apache-arrow';
16
+ import {Table as ArrowTable} from 'apache-arrow';
17
17
  import cx from 'classnames';
18
18
  import {scaleLinear} from 'd3';
19
19
  import graphviz from 'graphviz-wasm';
@@ -30,6 +30,7 @@ import {
30
30
  Flamegraph,
31
31
  QueryServiceClient,
32
32
  Source,
33
+ TableArrow,
33
34
  Top,
34
35
  } from '@parca/client';
35
36
  import {
@@ -49,7 +50,7 @@ import {jsonToDot} from '../Callgraph/utils';
49
50
  import ProfileIcicleGraph from '../ProfileIcicleGraph';
50
51
  import {ProfileSource} from '../ProfileSource';
51
52
  import {SourceView} from '../SourceView';
52
- import {TopTable} from '../TopTable';
53
+ import Table from '../Table';
53
54
  import ProfileShareButton from '../components/ProfileShareButton';
54
55
  import useDelayedLoader from '../useDelayedLoader';
55
56
  import FilterByFunctionButton from './FilterByFunctionButton';
@@ -62,7 +63,7 @@ type NavigateFunction = (path: string, queryParams: any, options?: {replace?: bo
62
63
  export interface FlamegraphData {
63
64
  loading: boolean;
64
65
  data?: Flamegraph;
65
- table?: Table<any>;
66
+ table?: ArrowTable<any>;
66
67
  total?: bigint;
67
68
  filtered?: bigint;
68
69
  error?: any;
@@ -70,7 +71,8 @@ export interface FlamegraphData {
70
71
 
71
72
  export interface TopTableData {
72
73
  loading: boolean;
73
- data?: Top;
74
+ arrow?: TableArrow;
75
+ data?: Top; // TODO: Remove this once we only have arrow support
74
76
  total?: bigint;
75
77
  filtered?: bigint;
76
78
  error?: any;
@@ -294,9 +296,9 @@ export const ProfileView = ({
294
296
  }
295
297
  case 'table': {
296
298
  return topTableData != null ? (
297
- <TopTable
299
+ <Table
298
300
  loading={topTableData.loading}
299
- data={topTableData.data}
301
+ data={topTableData.arrow?.record}
300
302
  sampleUnit={sampleUnit}
301
303
  navigateTo={navigateTo}
302
304
  setActionButtons={setActionButtons}
@@ -82,10 +82,10 @@ export const ProfileViewWithData = ({
82
82
  const {perf} = useParcaContext();
83
83
 
84
84
  const {
85
- isLoading: topTableLoading,
86
- response: topTableResponse,
87
- error: topTableError,
88
- } = useQuery(queryClient, profileSource, QueryRequest_ReportType.TOP, {
85
+ isLoading: tableLoading,
86
+ response: tableResponse,
87
+ error: tableError,
88
+ } = useQuery(queryClient, profileSource, QueryRequest_ReportType.TABLE_ARROW, {
89
89
  skip: !dashboardItems.includes('table'),
90
90
  });
91
91
 
@@ -115,8 +115,8 @@ export const ProfileViewWithData = ({
115
115
  perf?.markInteraction('Flamegraph render', flamegraphResponse.total);
116
116
  }
117
117
 
118
- if (!topTableLoading && topTableResponse?.report.oneofKind === 'top') {
119
- perf?.markInteraction('Top table render', topTableResponse.total);
118
+ if (!tableLoading && tableResponse?.report.oneofKind === 'tableArrow') {
119
+ perf?.markInteraction('table render', tableResponse.total);
120
120
  }
121
121
 
122
122
  if (!callgraphLoading && callgraphResponse?.report.oneofKind === 'callgraph') {
@@ -131,8 +131,8 @@ export const ProfileViewWithData = ({
131
131
  flamegraphResponse,
132
132
  callgraphResponse,
133
133
  callgraphLoading,
134
- topTableLoading,
135
- topTableResponse,
134
+ tableLoading,
135
+ tableResponse,
136
136
  sourceLoading,
137
137
  sourceResponse,
138
138
  perf,
@@ -163,9 +163,9 @@ export const ProfileViewWithData = ({
163
163
  if (flamegraphResponse !== null) {
164
164
  total = BigInt(flamegraphResponse.total);
165
165
  filtered = BigInt(flamegraphResponse.filtered);
166
- } else if (topTableResponse !== null) {
167
- total = BigInt(topTableResponse.total);
168
- filtered = BigInt(topTableResponse.filtered);
166
+ } else if (tableResponse !== null) {
167
+ total = BigInt(tableResponse.total);
168
+ filtered = BigInt(tableResponse.filtered);
169
169
  } else if (callgraphResponse !== null) {
170
170
  total = BigInt(callgraphResponse.total);
171
171
  filtered = BigInt(callgraphResponse.filtered);
@@ -193,10 +193,12 @@ export const ProfileViewWithData = ({
193
193
  error: flamegraphError,
194
194
  }}
195
195
  topTableData={{
196
- loading: topTableLoading,
197
- data:
198
- topTableResponse?.report.oneofKind === 'top' ? topTableResponse.report.top : undefined,
199
- error: topTableError,
196
+ loading: tableLoading,
197
+ arrow:
198
+ tableResponse?.report.oneofKind === 'tableArrow'
199
+ ? tableResponse.report.tableArrow
200
+ : undefined,
201
+ error: tableError,
200
202
  }}
201
203
  callgraphData={{
202
204
  loading: callgraphLoading,
@@ -0,0 +1,285 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ import React, {useCallback, useEffect, useMemo} from 'react';
15
+
16
+ import {createColumnHelper, type ColumnDef} from '@tanstack/react-table';
17
+ import {Table as ArrowTable, tableFromIPC} from 'apache-arrow';
18
+
19
+ import {Button, Table as TableComponent, useURLState} from '@parca/components';
20
+ import {
21
+ getLastItem,
22
+ isSearchMatch,
23
+ parseParams,
24
+ valueFormatter,
25
+ type NavigateFunction,
26
+ } from '@parca/utilities';
27
+
28
+ import {hexifyAddress} from '../utils';
29
+
30
+ const columnHelper = createColumnHelper<row>();
31
+
32
+ interface row {
33
+ name: string;
34
+ flat: bigint;
35
+ flatDiff: bigint;
36
+ cumulative: bigint;
37
+ cumulativeDiff: bigint;
38
+ }
39
+
40
+ interface TableProps {
41
+ data?: Uint8Array;
42
+ sampleUnit: string;
43
+ navigateTo?: NavigateFunction;
44
+ loading: boolean;
45
+ currentSearchString?: string;
46
+ setActionButtons?: (buttons: React.JSX.Element) => void;
47
+ }
48
+
49
+ export const Table = React.memo(function Table({
50
+ data,
51
+ sampleUnit: unit,
52
+ navigateTo,
53
+ loading,
54
+ currentSearchString,
55
+ setActionButtons,
56
+ }: TableProps): React.JSX.Element {
57
+ const router = parseParams(window?.location.search);
58
+ const [rawDashboardItems] = useURLState({param: 'dashboard_items'});
59
+ const [rawcompareMode] = useURLState({param: 'compare_a'});
60
+
61
+ const compareMode: boolean = rawcompareMode === undefined ? false : rawcompareMode === 'true';
62
+
63
+ const dashboardItems = useMemo(() => {
64
+ if (rawDashboardItems !== undefined) {
65
+ return rawDashboardItems as string[];
66
+ }
67
+ return ['icicle'];
68
+ }, [rawDashboardItems]);
69
+
70
+ const columns = useMemo(() => {
71
+ const cols: Array<ColumnDef<row, any>> = [
72
+ columnHelper.accessor('flat', {
73
+ header: () => 'Flat',
74
+ cell: info => valueFormatter(info.getValue(), unit, 2),
75
+ size: 80,
76
+ meta: {
77
+ align: 'right',
78
+ },
79
+ invertSorting: true,
80
+ }),
81
+ columnHelper.accessor('flatDiff', {
82
+ header: () => 'Flat Diff',
83
+ cell: info => addPlusSign(valueFormatter(info.getValue(), unit, 2)),
84
+ size: 120,
85
+ meta: {
86
+ align: 'right',
87
+ },
88
+ invertSorting: true,
89
+ }),
90
+ columnHelper.accessor('cumulative', {
91
+ header: () => 'Cumulative',
92
+ cell: info => valueFormatter(info.getValue(), unit, 2),
93
+ size: 130,
94
+ meta: {
95
+ align: 'right',
96
+ },
97
+ invertSorting: true,
98
+ }),
99
+ columnHelper.accessor('cumulativeDiff', {
100
+ header: () => 'Cumulative Diff',
101
+ cell: info => addPlusSign(valueFormatter(info.getValue(), unit, 2)),
102
+ size: 170,
103
+ meta: {
104
+ align: 'right',
105
+ },
106
+ invertSorting: true,
107
+ }),
108
+ columnHelper.accessor('name', {
109
+ header: () => <span className="text-left">Name</span>,
110
+ cell: info => info.getValue(),
111
+ }),
112
+ ];
113
+ return cols;
114
+ }, [unit]);
115
+
116
+ const selectSpan = useCallback(
117
+ (span: string): void => {
118
+ if (navigateTo != null) {
119
+ navigateTo(
120
+ '/',
121
+ {
122
+ ...router,
123
+ ...{search_string: span.trim()},
124
+ },
125
+ {replace: true}
126
+ );
127
+ }
128
+ },
129
+ [navigateTo, router]
130
+ );
131
+
132
+ const onRowClick = useCallback(
133
+ (row: row) => {
134
+ // If there is only one dashboard item, we don't want to select a span
135
+ if (dashboardItems.length <= 1) {
136
+ return;
137
+ }
138
+ selectSpan(row.name);
139
+ },
140
+ [selectSpan, dashboardItems.length]
141
+ );
142
+
143
+ const shouldHighlightRow = useCallback(
144
+ (row: row) => {
145
+ const name = row.name;
146
+ return isSearchMatch(currentSearchString as string, name);
147
+ },
148
+ [currentSearchString]
149
+ );
150
+
151
+ const enableHighlighting = useMemo(() => {
152
+ return currentSearchString != null && currentSearchString?.length > 0;
153
+ }, [currentSearchString]);
154
+
155
+ const clearSelection = useCallback((): void => {
156
+ if (navigateTo != null) {
157
+ navigateTo(
158
+ '/',
159
+ {
160
+ ...router,
161
+ ...{search_string: ''},
162
+ },
163
+ {replace: true}
164
+ );
165
+ }
166
+ }, [navigateTo, router]);
167
+
168
+ useEffect(() => {
169
+ if (setActionButtons === undefined) {
170
+ return;
171
+ }
172
+ setActionButtons(
173
+ dashboardItems.length > 1 ? (
174
+ <Button
175
+ color="neutral"
176
+ onClick={clearSelection}
177
+ className="w-auto"
178
+ variant="neutral"
179
+ disabled={currentSearchString === undefined || currentSearchString.length === 0}
180
+ >
181
+ Clear selection
182
+ </Button>
183
+ ) : (
184
+ <></>
185
+ )
186
+ );
187
+ }, [dashboardItems, clearSelection, currentSearchString, setActionButtons]);
188
+
189
+ const initialSorting = useMemo(() => {
190
+ return [
191
+ {
192
+ id: compareMode ? 'flatDiff' : 'flat',
193
+ desc: false, // columns sorting are inverted - so this is actually descending
194
+ },
195
+ ];
196
+ }, [compareMode]);
197
+
198
+ const columnVisibility = useMemo(() => {
199
+ // TODO: Make this configurable via the UI and add more columns.
200
+ return {
201
+ flat: true,
202
+ flatDiff: compareMode,
203
+ cumulative: true,
204
+ cumulativeDiff: compareMode,
205
+ name: true,
206
+ };
207
+ }, [compareMode]);
208
+
209
+ if (loading) return <>Loading...</>;
210
+ if (data === undefined) return <>Profile has no samples</>;
211
+
212
+ const table = tableFromIPC(data);
213
+ const flatColumn = table.getChild('flat');
214
+ const flatDiffColumn = table.getChild('flat_diff');
215
+ const cumulativeColumn = table.getChild('cumulative');
216
+ const cumulativeDiffColumn = table.getChild('cumulative_diff');
217
+
218
+ if (table.numRows === 0) return <>Profile has no samples</>;
219
+
220
+ const rows: row[] = [];
221
+ // TODO: Figure out how to only read the data of the columns we need for the virtualized table
222
+ for (let i = 0; i < table.numRows; i++) {
223
+ const flat: bigint = flatColumn?.get(i) ?? 0n;
224
+ const flatDiff: bigint = flatDiffColumn?.get(i) ?? 0n;
225
+ const cumulative: bigint = cumulativeColumn?.get(i) ?? 0n;
226
+ const cumulativeDiff: bigint = cumulativeDiffColumn?.get(i) ?? 0n;
227
+ rows.push({
228
+ name: RowName(table, i),
229
+ flat,
230
+ flatDiff,
231
+ cumulative,
232
+ cumulativeDiff,
233
+ });
234
+ }
235
+
236
+ return (
237
+ <div className="relative">
238
+ <div className="font-robotoMono h-[80vh] w-full">
239
+ <TableComponent
240
+ data={rows}
241
+ columns={columns}
242
+ initialSorting={initialSorting}
243
+ columnVisibility={columnVisibility}
244
+ onRowClick={onRowClick}
245
+ enableHighlighting={enableHighlighting}
246
+ shouldHighlightRow={shouldHighlightRow}
247
+ usePointerCursor={dashboardItems.length > 1}
248
+ />
249
+ </div>
250
+ </div>
251
+ );
252
+ });
253
+
254
+ const addPlusSign = (num: string): string => {
255
+ if (num.charAt(0) === '0' || num.charAt(0) === '-') {
256
+ return num;
257
+ }
258
+
259
+ return `+${num}`;
260
+ };
261
+
262
+ export const RowName = (table: ArrowTable, row: number): string => {
263
+ const mappingFileColumn = table.getChild('mapping_file');
264
+ if (mappingFileColumn === null) {
265
+ console.error('mapping_file column not found');
266
+ return '';
267
+ }
268
+
269
+ const mappingFile: string | null = mappingFileColumn?.get(row);
270
+ let mapping = '';
271
+ // Show the last item in the mapping file only if there are more than 1 mappings
272
+ if (mappingFile != null && mappingFileColumn.data.length > 1) {
273
+ mapping = `[${getLastItem(mappingFile) ?? ''}]`;
274
+ }
275
+ const functionName: string | null = table.getChild('function_name')?.get(row) ?? '';
276
+ if (functionName !== null) {
277
+ return `${mapping} ${functionName}`;
278
+ }
279
+
280
+ const address: bigint = table.getChild('location_address')?.get(row) ?? 0;
281
+
282
+ return hexifyAddress(address);
283
+ };
284
+
285
+ export default Table;