@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 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: "flex gap-2", 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
+ 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?: 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.230",
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.183",
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": "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="flex gap-2">
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 {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;