@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 +4 -0
- 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/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,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?:
|
|
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.229",
|
|
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.182",
|
|
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": "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
|
|
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;
|