@parca/profile 0.16.228 → 0.16.230
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 +8 -0
- package/dist/GraphTooltipArrow/Content.js +4 -3
- package/dist/ProfileView/ViewSelector.js +5 -2
- package/dist/ProfileView/index.d.ts +4 -3
- package/dist/ProfileView/index.js +2 -2
- package/dist/ProfileViewWithData.js +13 -11
- package/dist/Table/index.d.ts +14 -0
- package/dist/Table/index.js +184 -0
- package/package.json +7 -7
- package/src/GraphTooltipArrow/Content.tsx +4 -3
- package/src/ProfileView/ViewSelector.tsx +5 -2
- package/src/ProfileView/index.tsx +8 -6
- package/src/ProfileViewWithData.tsx +17 -15
- package/src/Table/index.tsx +285 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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.230](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.229...@parca/profile@0.16.230) (2023-08-24)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @parca/profile
|
|
9
|
+
|
|
10
|
+
## [0.16.229](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.228...@parca/profile@0.16.229) (2023-08-23)
|
|
11
|
+
|
|
12
|
+
**Note:** Version bump only for package @parca/profile
|
|
13
|
+
|
|
6
14
|
## [0.16.228](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.227...@parca/profile@0.16.228) (2023-08-23)
|
|
7
15
|
|
|
8
16
|
**Note:** Version bump only for package @parca/profile
|
|
@@ -12,6 +12,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
import { useState } from 'react';
|
|
15
|
+
import cx from 'classnames';
|
|
15
16
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
|
16
17
|
import { Tooltip } from 'react-tooltip';
|
|
17
18
|
import { QueryRequest_ReportType } from '@parca/client';
|
|
@@ -70,10 +71,10 @@ onCopy, row, navigateTo, }) => {
|
|
|
70
71
|
const functionStartLine = table.getChild(FIELD_FUNCTION_START_LINE)?.get(row) ?? 0n;
|
|
71
72
|
const pprofLabelPrefix = 'pprof_labels.';
|
|
72
73
|
const labelColumnNames = table.schema.fields.filter(field => field.name.startsWith(pprofLabelPrefix));
|
|
73
|
-
const { queryServiceClient } = useParcaContext();
|
|
74
|
+
const { queryServiceClient, enableSourcesView } = useParcaContext();
|
|
74
75
|
const { profileSource } = useProfileViewContext();
|
|
75
76
|
const { isLoading: sourceLoading, response: sourceResponse } = useQuery(queryServiceClient, profileSource, QueryRequest_ReportType.SOURCE, {
|
|
76
|
-
skip: profileSource === undefined,
|
|
77
|
+
skip: enableSourcesView === false || profileSource === undefined,
|
|
77
78
|
sourceBuildID: mappingBuildID,
|
|
78
79
|
sourceFilename: functionFilename,
|
|
79
80
|
sourceOnly: true,
|
|
@@ -119,6 +120,6 @@ onCopy, row, navigateTo, }) => {
|
|
|
119
120
|
setSourceFilename(functionFilename);
|
|
120
121
|
setSourceLine(locationLine.toString());
|
|
121
122
|
};
|
|
122
|
-
return (_jsxs(_Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "File" }), _jsx("td", { className: "w-3/4 break-all", children: functionFilename === '' ? (_jsx(NoData, {})) : (_jsxs("div", { className: "flex gap-4", children: [_jsx(CopyToClipboard, { onCopy: onCopy, text: file, children: _jsx("button", { className: "cursor-pointer whitespace-nowrap text-left", children: _jsx(ExpandOnHover, { value: file, displayValue: truncateStringReverse(file, 30) }) }) }), _jsxs("div", { className:
|
|
123
|
+
return (_jsxs(_Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "File" }), _jsx("td", { className: "w-3/4 break-all", children: functionFilename === '' ? (_jsx(NoData, {})) : (_jsxs("div", { className: "flex gap-4", children: [_jsx(CopyToClipboard, { onCopy: onCopy, text: file, children: _jsx("button", { className: "cursor-pointer whitespace-nowrap text-left", children: _jsx(ExpandOnHover, { value: file, displayValue: truncateStringReverse(file, 30) }) }) }), _jsxs("div", { className: cx('flex gap-2', { hidden: enableSourcesView === false }), children: [_jsx("div", { "data-tooltip-id": "open-source-button-help", "data-tooltip-content": "There is no source code uploaded for this build", children: _jsx(Button, { variant: 'neutral', onClick: () => openFile(), className: "shrink-0", disabled: !isSourceAvailable, children: "open" }) }), !isSourceAvailable ? _jsx(Tooltip, { id: "open-source-button-help" }) : null] })] })) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Address" }), _jsx("td", { className: "w-3/4 break-all", children: locationAddress === 0n ? (_jsx(NoData, {})) : (_jsx(CopyToClipboard, { onCopy: onCopy, text: hexifyAddress(locationAddress), children: _jsx("button", { className: "cursor-pointer", children: hexifyAddress(locationAddress) }) })) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Binary" }), _jsx("td", { className: "w-3/4 break-all", children: mappingFile === '' ? (_jsx(NoData, {})) : (_jsx(CopyToClipboard, { onCopy: onCopy, text: mappingFile, children: _jsx("button", { className: "cursor-pointer", children: getLastItem(mappingFile) }) })) })] }), _jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Build Id" }), _jsx("td", { className: "w-3/4 break-all", children: mappingBuildID === '' ? (_jsx(NoData, {})) : (_jsx(CopyToClipboard, { onCopy: onCopy, text: mappingBuildID, children: _jsx("button", { className: "cursor-pointer", children: truncateString(getLastItem(mappingBuildID), 28) }) })) })] }), labelPairs.length > 0 && (_jsxs("tr", { children: [_jsx("td", { className: "w-1/4", children: "Labels" }), _jsx("td", { className: "w-3/4 break-all", children: labels })] }))] }));
|
|
123
124
|
};
|
|
124
125
|
export default GraphTooltipArrowContent;
|
|
@@ -11,7 +11,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
|
-
import { Select, useURLState } from '@parca/components';
|
|
14
|
+
import { Select, useParcaContext, useURLState } from '@parca/components';
|
|
15
15
|
import { useUIFeatureFlag } from '@parca/hooks';
|
|
16
16
|
const ViewSelector = ({ defaultValue, navigateTo, position, placeholderText, primary = false, addView = false, disabled = false, }) => {
|
|
17
17
|
const [callgraphEnabled] = useUIFeatureFlag('callgraph');
|
|
@@ -19,11 +19,14 @@ const ViewSelector = ({ defaultValue, navigateTo, position, placeholderText, pri
|
|
|
19
19
|
param: 'dashboard_items',
|
|
20
20
|
navigateTo,
|
|
21
21
|
});
|
|
22
|
+
const { enableSourcesView } = useParcaContext();
|
|
22
23
|
const allItems = [
|
|
23
24
|
{ key: 'table', canBeSelected: !dashboardItems.includes('table') },
|
|
24
25
|
{ key: 'icicle', canBeSelected: !dashboardItems.includes('icicle') },
|
|
25
|
-
{ key: 'source', canBeSelected: false },
|
|
26
26
|
];
|
|
27
|
+
if (enableSourcesView === true) {
|
|
28
|
+
allItems.push({ key: 'source', canBeSelected: false });
|
|
29
|
+
}
|
|
27
30
|
if (callgraphEnabled) {
|
|
28
31
|
allItems.push({
|
|
29
32
|
key: 'callgraph',
|
|
@@ -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?:
|
|
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
|
|
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(
|
|
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:
|
|
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 (!
|
|
72
|
-
perf?.markInteraction('
|
|
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
|
-
|
|
86
|
-
|
|
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 (
|
|
116
|
-
total = BigInt(
|
|
117
|
-
filtered = BigInt(
|
|
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:
|
|
140
|
-
|
|
141
|
-
|
|
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.
|
|
3
|
+
"version": "0.16.230",
|
|
4
4
|
"description": "Profile viewing libraries",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@parca/client": "^0.16.
|
|
7
|
-
"@parca/components": "^0.16.
|
|
6
|
+
"@parca/client": "^0.16.86",
|
|
7
|
+
"@parca/components": "^0.16.183",
|
|
8
8
|
"@parca/dynamicsize": "^0.16.54",
|
|
9
|
-
"@parca/hooks": "^0.0.
|
|
9
|
+
"@parca/hooks": "^0.0.21",
|
|
10
10
|
"@parca/parser": "^0.16.55",
|
|
11
|
-
"@parca/store": "^0.16.
|
|
12
|
-
"@parca/utilities": "^0.0.
|
|
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": "
|
|
52
|
+
"gitHead": "79fbf41f87bbd157c49568591abe9bcea3a38afa"
|
|
53
53
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import React, {useState} from 'react';
|
|
15
15
|
|
|
16
16
|
import {Table} from 'apache-arrow';
|
|
17
|
+
import cx from 'classnames';
|
|
17
18
|
import {CopyToClipboard} from 'react-copy-to-clipboard';
|
|
18
19
|
import {Tooltip} from 'react-tooltip';
|
|
19
20
|
|
|
@@ -200,7 +201,7 @@ const TooltipMetaInfo = ({
|
|
|
200
201
|
field.name.startsWith(pprofLabelPrefix)
|
|
201
202
|
);
|
|
202
203
|
|
|
203
|
-
const {queryServiceClient} = useParcaContext();
|
|
204
|
+
const {queryServiceClient, enableSourcesView} = useParcaContext();
|
|
204
205
|
const {profileSource} = useProfileViewContext();
|
|
205
206
|
|
|
206
207
|
const {isLoading: sourceLoading, response: sourceResponse} = useQuery(
|
|
@@ -208,7 +209,7 @@ const TooltipMetaInfo = ({
|
|
|
208
209
|
profileSource as ProfileSource,
|
|
209
210
|
QueryRequest_ReportType.SOURCE,
|
|
210
211
|
{
|
|
211
|
-
skip: profileSource === undefined,
|
|
212
|
+
skip: enableSourcesView === false || profileSource === undefined,
|
|
212
213
|
sourceBuildID: mappingBuildID,
|
|
213
214
|
sourceFilename: functionFilename,
|
|
214
215
|
sourceOnly: true,
|
|
@@ -289,7 +290,7 @@ const TooltipMetaInfo = ({
|
|
|
289
290
|
<ExpandOnHover value={file} displayValue={truncateStringReverse(file, 30)} />
|
|
290
291
|
</button>
|
|
291
292
|
</CopyToClipboard>
|
|
292
|
-
<div className=
|
|
293
|
+
<div className={cx('flex gap-2', {hidden: enableSourcesView === false})}>
|
|
293
294
|
<div
|
|
294
295
|
data-tooltip-id="open-source-button-help"
|
|
295
296
|
data-tooltip-content="There is no source code uploaded for this build"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {Select, useURLState, type SelectElement} from '@parca/components';
|
|
14
|
+
import {Select, useParcaContext, useURLState, type SelectElement} from '@parca/components';
|
|
15
15
|
import {useUIFeatureFlag} from '@parca/hooks';
|
|
16
16
|
import type {NavigateFunction} from '@parca/utilities';
|
|
17
17
|
|
|
@@ -39,12 +39,15 @@ const ViewSelector = ({
|
|
|
39
39
|
param: 'dashboard_items',
|
|
40
40
|
navigateTo,
|
|
41
41
|
});
|
|
42
|
+
const {enableSourcesView} = useParcaContext();
|
|
42
43
|
|
|
43
44
|
const allItems: Array<{key: string; canBeSelected: boolean; supportingText?: string}> = [
|
|
44
45
|
{key: 'table', canBeSelected: !dashboardItems.includes('table')},
|
|
45
46
|
{key: 'icicle', canBeSelected: !dashboardItems.includes('icicle')},
|
|
46
|
-
{key: 'source', canBeSelected: false},
|
|
47
47
|
];
|
|
48
|
+
if (enableSourcesView === true) {
|
|
49
|
+
allItems.push({key: 'source', canBeSelected: false});
|
|
50
|
+
}
|
|
48
51
|
if (callgraphEnabled) {
|
|
49
52
|
allItems.push({
|
|
50
53
|
key: 'callgraph',
|
|
@@ -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
|
|
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?:
|
|
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
|
-
|
|
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
|
-
<
|
|
299
|
+
<Table
|
|
298
300
|
loading={topTableData.loading}
|
|
299
|
-
data={topTableData.
|
|
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:
|
|
86
|
-
response:
|
|
87
|
-
error:
|
|
88
|
-
} = useQuery(queryClient, profileSource, QueryRequest_ReportType.
|
|
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 (!
|
|
119
|
-
perf?.markInteraction('
|
|
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
|
-
|
|
135
|
-
|
|
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 (
|
|
167
|
-
total = BigInt(
|
|
168
|
-
filtered = BigInt(
|
|
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:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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;
|