@parca/profile 0.19.33 → 0.19.35

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.19.35](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.34...@parca/profile@0.19.35) (2025-08-12)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
10
+ ## [0.19.34](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.33...@parca/profile@0.19.34) (2025-08-06)
11
+
12
+ **Note:** Version bump only for package @parca/profile
13
+
6
14
  ## [0.19.33](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.32...@parca/profile@0.19.33) (2025-08-06)
7
15
 
8
16
  **Note:** Version bump only for package @parca/profile
@@ -1,9 +1,14 @@
1
1
  import 'react-contexify/dist/ReactContexify.css';
2
2
  import { type Row } from '.';
3
+ import { type ColumnName } from './utils/functions';
3
4
  interface TableContextMenuProps {
4
5
  menuId: string;
5
6
  row: Row | null;
7
+ unit?: string;
8
+ total?: bigint;
9
+ totalUnfiltered?: bigint;
10
+ columnVisibility?: Record<ColumnName, boolean>;
6
11
  }
7
- declare const TableContextMenu: ({ menuId, row }: TableContextMenuProps) => React.JSX.Element;
12
+ declare const TableContextMenu: ({ menuId, row, unit, total, totalUnfiltered, columnVisibility, }: TableContextMenuProps) => React.JSX.Element;
8
13
  export default TableContextMenu;
9
14
  //# sourceMappingURL=TableContextMenu.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TableContextMenu.d.ts","sourceRoot":"","sources":["../../src/Table/TableContextMenu.tsx"],"names":[],"mappings":"AAiBA,OAAO,yCAAyC,CAAC;AAIjD,OAAO,EAAC,KAAK,GAAG,EAAC,MAAM,GAAG,CAAC;AAE3B,UAAU,qBAAqB;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;CACjB;AAED,QAAA,MAAM,gBAAgB,GAAI,iBAAe,qBAAqB,KAAG,KAAK,CAAC,GAAG,CAAC,OAuC1E,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"TableContextMenu.d.ts","sourceRoot":"","sources":["../../src/Table/TableContextMenu.tsx"],"names":[],"mappings":"AAiBA,OAAO,yCAAyC,CAAC;AAKjD,OAAO,EAAC,KAAK,GAAG,EAAC,MAAM,GAAG,CAAC;AAG3B,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,mBAAmB,CAAC;AAElD,UAAU,qBAAqB;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;CAChD;AAED,QAAA,MAAM,gBAAgB,GAAI,kEAOvB,qBAAqB,KAAG,KAAK,CAAC,GAAG,CAAC,OA+LpC,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
@@ -13,10 +13,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  // limitations under the License.
14
14
  import { Icon } from '@iconify/react';
15
15
  import cx from 'classnames';
16
- import { Item, Menu } from 'react-contexify';
16
+ import { Item, Menu, Submenu } from 'react-contexify';
17
17
  import 'react-contexify/dist/ReactContexify.css';
18
18
  import { useParcaContext, useURLState } from '@parca/components';
19
- const TableContextMenu = ({ menuId, row }) => {
19
+ import { valueFormatter } from '@parca/utilities';
20
+ import { getTextForCumulative } from '../ProfileFlameGraph/FlameGraphArrow/utils';
21
+ import { truncateString } from '../utils';
22
+ const TableContextMenu = ({ menuId, row, unit, total, totalUnfiltered, columnVisibility, }) => {
20
23
  const [_, setSandwichFunctionName] = useURLState('sandwich_function_name');
21
24
  const [dashboardItems, setDashboardItems] = useURLState('dashboard_items', {
22
25
  alwaysReturnArray: true,
@@ -30,9 +33,125 @@ const TableContextMenu = ({ menuId, row }) => {
30
33
  }
31
34
  }
32
35
  };
36
+ const handleCopyItem = (text) => {
37
+ void navigator.clipboard.writeText(text);
38
+ };
39
+ const isColumnVisible = (columnName) => {
40
+ return columnVisibility?.[columnName] ?? true;
41
+ };
42
+ const valuesToCopy = row !== null
43
+ ? [
44
+ ...(isColumnVisible('flat')
45
+ ? [
46
+ {
47
+ id: 'Flat',
48
+ value: total !== null &&
49
+ total !== undefined &&
50
+ totalUnfiltered !== null &&
51
+ totalUnfiltered !== undefined
52
+ ? getTextForCumulative(row.flat, total, totalUnfiltered, unit ?? '')
53
+ : valueFormatter(row.flat, unit ?? '', 1),
54
+ },
55
+ ]
56
+ : []),
57
+ ...(isColumnVisible('flatPercentage')
58
+ ? [
59
+ {
60
+ id: 'Flat (%)',
61
+ value: total !== null &&
62
+ total !== undefined &&
63
+ totalUnfiltered !== null &&
64
+ totalUnfiltered !== undefined
65
+ ? getTextForCumulative(row.flat, total, totalUnfiltered, unit ?? '')
66
+ : valueFormatter(row.flat, unit ?? '', 1),
67
+ },
68
+ ]
69
+ : []),
70
+ ...(isColumnVisible('flatDiff')
71
+ ? [
72
+ {
73
+ id: 'Flat Diff',
74
+ value: row.flatDiff !== 0n ? valueFormatter(row.flatDiff, unit ?? '', 1) : '',
75
+ },
76
+ ]
77
+ : []),
78
+ ...(isColumnVisible('flatDiffPercentage')
79
+ ? [
80
+ {
81
+ id: 'Flat Diff (%)',
82
+ value: row.flatDiff !== 0n ? valueFormatter(row.flatDiff, unit ?? '', 1) : '',
83
+ },
84
+ ]
85
+ : []),
86
+ ...(isColumnVisible('cumulative')
87
+ ? [
88
+ {
89
+ id: 'Cumulative',
90
+ value: total !== null &&
91
+ total !== undefined &&
92
+ totalUnfiltered !== null &&
93
+ totalUnfiltered !== undefined
94
+ ? getTextForCumulative(row.cumulative, total, totalUnfiltered, unit ?? '')
95
+ : valueFormatter(row.cumulative, unit ?? '', 1),
96
+ },
97
+ ]
98
+ : []),
99
+ ...(isColumnVisible('cumulativePercentage')
100
+ ? [
101
+ {
102
+ id: 'Cumulative (%)',
103
+ value: total !== null &&
104
+ total !== undefined &&
105
+ totalUnfiltered !== null &&
106
+ totalUnfiltered !== undefined
107
+ ? getTextForCumulative(row.cumulative, total, totalUnfiltered, unit ?? '')
108
+ : valueFormatter(row.cumulative, unit ?? '', 1),
109
+ },
110
+ ]
111
+ : []),
112
+ ...(isColumnVisible('cumulativeDiff')
113
+ ? [
114
+ {
115
+ id: 'Cumulative Diff',
116
+ value: row.cumulativeDiff !== 0n
117
+ ? valueFormatter(row.cumulativeDiff, unit ?? '', 1)
118
+ : '',
119
+ },
120
+ ]
121
+ : []),
122
+ ...(isColumnVisible('cumulativeDiffPercentage')
123
+ ? [
124
+ {
125
+ id: 'Cumulative Diff (%)',
126
+ value: row.cumulativeDiff !== 0n
127
+ ? valueFormatter(row.cumulativeDiff, unit ?? '', 1)
128
+ : '',
129
+ },
130
+ ]
131
+ : []),
132
+ ...(isColumnVisible('name')
133
+ ? [
134
+ {
135
+ id: 'Name',
136
+ value: row.name ?? '',
137
+ },
138
+ ]
139
+ : []),
140
+ ...(isColumnVisible('functionSystemName')
141
+ ? [{ id: 'Function System Name', value: row.functionSystemName ?? '' }]
142
+ : []),
143
+ ...(isColumnVisible('functionFileName')
144
+ ? [{ id: 'Function File Name', value: row.functionFileName ?? '' }]
145
+ : []),
146
+ ...(isColumnVisible('mappingFile')
147
+ ? [{ id: 'Mapping File', value: row.mappingFile ?? '' }]
148
+ : []),
149
+ ].flat()
150
+ : [];
151
+ const nonEmptyValuesToCopy = valuesToCopy.filter(({ value }) => value !== '');
33
152
  const isMenuDisabled = row === null || enableSandwichView !== true;
34
- return (_jsx(Menu, { id: menuId, theme: isDarkMode ? 'dark' : '', className: cx(dashboardItems.includes('sandwich') ? 'min-w-[350px] w-[350px]' : 'min-w-[260px] w-[260px]'), children: _jsx(Item, { id: "sandwich-view", onClick: onSandwichViewSelect, disabled: isMenuDisabled, children: _jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx(Icon, { icon: "tdesign:sandwich-filled" }), _jsxs("div", { className: "relative", children: [dashboardItems.includes('sandwich')
35
- ? 'Focus sandwich on this frame.'
36
- : 'Show in sandwich', _jsx("span", { className: "absolute top-[-2px] text-xs lowercase text-red-500", children: "\u00A0alpha" })] })] }) }) }));
153
+ return (_jsxs(Menu, { id: menuId, theme: isDarkMode ? 'dark' : '', className: cx(dashboardItems.includes('sandwich') ? 'min-w-[350px] w-[350px]' : 'min-w-[260px] w-[260px]'), children: [_jsx(Item, { id: "sandwich-view", onClick: onSandwichViewSelect, disabled: isMenuDisabled, children: _jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx(Icon, { icon: "tdesign:sandwich-filled" }), _jsxs("div", { className: "relative", children: [dashboardItems.includes('sandwich')
154
+ ? 'Focus sandwich on this frame.'
155
+ : 'Show in sandwich', _jsx("span", { className: "absolute top-[-2px] text-xs lowercase text-red-500", children: "\u00A0alpha" })] })] }) }), _jsx(Submenu, { label: _jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx(Icon, { icon: "ph:copy" }), _jsx("div", { children: "Copy" })] }), disabled: row === null, children: _jsx("div", { className: "max-h-[300px] overflow-scroll", children: nonEmptyValuesToCopy.map(({ id, value }) => (_jsx(Item, { id: id, onClick: () => handleCopyItem(value), className: "dark:bg-gray-800", children: _jsxs("div", { className: "flex flex-col dark:text-gray-300 hover:dark:text-gray-100", children: [_jsx("div", { className: "text-sm", children: id }), _jsx("div", { className: "text-xs", children: truncateString(value, 30) })] }) }, id))) }) })] }));
37
156
  };
38
157
  export default TableContextMenu;
@@ -1,6 +1,11 @@
1
1
  import { type Row } from '.';
2
+ import { type ColumnName } from './utils/functions';
2
3
  interface TableContextMenuWrapperProps {
3
4
  menuId: string;
5
+ unit?: string;
6
+ total?: bigint;
7
+ totalUnfiltered?: bigint;
8
+ columnVisibility?: Record<ColumnName, boolean>;
4
9
  }
5
10
  export interface TableContextMenuWrapperRef {
6
11
  setRow: (row: Row | null, callback?: () => void) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"TableContextMenuWrapper.d.ts","sourceRoot":"","sources":["../../src/Table/TableContextMenuWrapper.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,KAAK,GAAG,EAAC,MAAM,GAAG,CAAC;AAG3B,UAAU,4BAA4B;IACpC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CAC1D;AAED,QAAA,MAAM,uBAAuB,qIAiB3B,CAAC;AAIH,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"TableContextMenuWrapper.d.ts","sourceRoot":"","sources":["../../src/Table/TableContextMenuWrapper.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,KAAK,GAAG,EAAC,MAAM,GAAG,CAAC;AAE3B,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,mBAAmB,CAAC;AAElD,UAAU,4BAA4B;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CAC1D;AAED,QAAA,MAAM,uBAAuB,qIA0B3B,CAAC;AAIH,eAAe,uBAAuB,CAAC"}
@@ -13,7 +13,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
13
13
  // limitations under the License.
14
14
  import { forwardRef, useImperativeHandle, useState } from 'react';
15
15
  import TableContextMenu from './TableContextMenu';
16
- const TableContextMenuWrapper = forwardRef(({ menuId }, ref) => {
16
+ const TableContextMenuWrapper = forwardRef(({ menuId, unit, total, totalUnfiltered, columnVisibility }, ref) => {
17
17
  const [row, setRow] = useState(null);
18
18
  useImperativeHandle(ref, () => ({
19
19
  setRow: (newRow, callback) => {
@@ -24,7 +24,7 @@ const TableContextMenuWrapper = forwardRef(({ menuId }, ref) => {
24
24
  }
25
25
  },
26
26
  }));
27
- return _jsx(TableContextMenu, { menuId: menuId, row: row });
27
+ return (_jsx(TableContextMenu, { menuId: menuId, row: row, unit: unit, total: total, totalUnfiltered: totalUnfiltered, columnVisibility: columnVisibility }));
28
28
  });
29
29
  TableContextMenuWrapper.displayName = 'TableContextMenuWrapper';
30
30
  export default TableContextMenuWrapper;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Table/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAgD,MAAM,OAAO,CAAC;AAarE,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAS1C,OAAO,EAAC,OAAO,EAAmC,MAAM,mBAAmB,CAAC;AAE5E,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,0BAA0B,yBAAyB,CAAC;AACjE,eAAO,MAAM,wBAAwB,uBAAuB,CAAC;AAC7D,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAC7C,eAAO,MAAM,qBAAqB,oBAAoB,CAAC;AACvD,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,eAAO,MAAM,aAAa,YAAY,CAAC;AAEvC,MAAM,MAAM,GAAG,GAAG,OAAO,CAAC;AAE1B,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,KAAK,IAAI,CAAC;IACxD,YAAY,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,eAAO,MAAM,KAAK,wCAoPhB,CAAC;AAEH,eAAe,KAAK,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Table/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAgD,MAAM,OAAO,CAAC;AAarE,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAS1C,OAAO,EAAC,OAAO,EAAmC,MAAM,mBAAmB,CAAC;AAE5E,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,0BAA0B,yBAAyB,CAAC;AACjE,eAAO,MAAM,wBAAwB,uBAAuB,CAAC;AAC7D,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAC7C,eAAO,MAAM,qBAAqB,oBAAoB,CAAC;AACvD,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,eAAO,MAAM,aAAa,YAAY,CAAC;AAEvC,MAAM,MAAM,GAAG,GAAG,OAAO,CAAC;AAE1B,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,KAAK,IAAI,CAAC;IACxD,YAAY,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,eAAO,MAAM,KAAK,wCAsMhB,CAAC;AAEH,eAAe,KAAK,CAAC"}
@@ -91,6 +91,14 @@ export const Table = React.memo(function Table({ data, total, filtered, profileT
91
91
  }
92
92
  selectSpan(row.name);
93
93
  }, [selectSpan, dashboardItems.length]);
94
+ const onRowContextMenu = useCallback((e, row) => {
95
+ e.preventDefault();
96
+ contextMenuRef.current?.setRow(row, () => {
97
+ show({
98
+ event: e,
99
+ });
100
+ });
101
+ }, [show]);
94
102
  const rows = useMemo(() => {
95
103
  if (table == null || table.numRows === 0) {
96
104
  return [];
@@ -131,59 +139,12 @@ export const Table = React.memo(function Table({ data, total, filtered, profileT
131
139
  const rows = Array.from({ length: table.numRows }, (_, i) => getRow(i));
132
140
  return rows;
133
141
  }, [table, colorByColors, colorBy]);
134
- const handleTableContextMenu = useCallback((e) => {
135
- e.preventDefault();
136
- // Find the closest table row element
137
- const target = e.target;
138
- const rowElement = target.closest('tr');
139
- if (rowElement !== null) {
140
- // Look for a data attribute that might contain the actual row ID
141
- const rowId = rowElement.getAttribute('data-row-id') ?? rowElement.getAttribute('data-id');
142
- if (rowId != null && rowId.length > 0) {
143
- // Find the row by ID
144
- const actualRowIndex = parseInt(rowId, 10);
145
- if (actualRowIndex >= 0 && actualRowIndex < rows.length) {
146
- const row = rows[actualRowIndex];
147
- contextMenuRef.current?.setRow(row, () => {
148
- show({
149
- event: e,
150
- });
151
- });
152
- return;
153
- }
154
- }
155
- // Fallback: try to find row by matching text content
156
- const nameCell = rowElement.querySelector('td:last-child'); // Name is usually the last column
157
- if (nameCell !== null) {
158
- const cellText = nameCell.textContent?.trim();
159
- if (cellText != null && cellText.length > 0) {
160
- // First try exact match
161
- let matchingRow = rows.find(row => row.name === cellText);
162
- // If no exact match, try partial match (in case of truncation)
163
- if (matchingRow == null) {
164
- matchingRow = rows.find(row => row.name.includes(cellText) || cellText.includes(row.name));
165
- }
166
- // If still no match, try matching the end of the name (for cases like package.function)
167
- if (matchingRow == null) {
168
- matchingRow = rows.find(row => row.name.endsWith(cellText) || cellText.endsWith(row.name.split('.').pop() ?? ''));
169
- }
170
- if (matchingRow != null) {
171
- contextMenuRef.current?.setRow(matchingRow, () => {
172
- show({
173
- event: e,
174
- });
175
- });
176
- }
177
- }
178
- }
179
- }
180
- }, [rows, show]);
181
142
  if (loading) {
182
143
  return (_jsx("div", { className: "overflow-clip h-[700px] min-h-[700px]", children: _jsx(TableSkeleton, { isHalfScreen: isHalfScreen, isDarkMode: isDarkMode }) }));
183
144
  }
184
145
  if (rows.length === 0) {
185
146
  return _jsx("div", { className: "mx-auto text-center", children: "Profile has no samples" });
186
147
  }
187
- return (_jsx(AnimatePresence, { children: _jsx(motion.div, { className: "h-full w-full", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: _jsxs("div", { className: "relative", children: [_jsx(TableContextMenuWrapper, { ref: contextMenuRef, menuId: MENU_ID }), _jsx("div", { className: "font-robotoMono h-[80vh] w-full", onContextMenu: handleTableContextMenu, children: _jsx(TableComponent, { data: rows, columns: columns, initialSorting: initialSorting, columnVisibility: columnVisibility, onRowClick: onRowClick, usePointerCursor: dashboardItems.length > 1, estimatedRowHeight: ROW_HEIGHT }) })] }) }, "table-loaded") }));
148
+ return (_jsx(AnimatePresence, { children: _jsx(motion.div, { className: "h-full w-full", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: _jsxs("div", { className: "relative", children: [_jsx(TableContextMenuWrapper, { ref: contextMenuRef, menuId: MENU_ID, unit: unit, total: total, totalUnfiltered: total, columnVisibility: columnVisibility }), _jsx("div", { className: "font-robotoMono h-[80vh] w-full", children: _jsx(TableComponent, { data: rows, columns: columns, initialSorting: initialSorting, columnVisibility: columnVisibility, onRowClick: onRowClick, onRowContextMenu: onRowContextMenu, usePointerCursor: dashboardItems.length > 1, estimatedRowHeight: ROW_HEIGHT }) })] }) }, "table-loaded") }));
188
149
  });
189
150
  export default Table;
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.19.33",
3
+ "version": "0.19.35",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@floating-ui/react": "^0.27.12",
7
7
  "@headlessui/react": "^1.7.19",
8
8
  "@iconify/react": "^4.0.0",
9
9
  "@parca/client": "0.17.3",
10
- "@parca/components": "0.16.354",
10
+ "@parca/components": "0.16.356",
11
11
  "@parca/dynamicsize": "0.16.65",
12
12
  "@parca/hooks": "0.0.99",
13
13
  "@parca/icons": "0.16.72",
@@ -78,5 +78,5 @@
78
78
  "access": "public",
79
79
  "registry": "https://registry.npmjs.org/"
80
80
  },
81
- "gitHead": "856f4482ef94445b2e7a05c5618652fd0079fc6e"
81
+ "gitHead": "c0da0b21aecf50944c7502483b8216ea11634ef2"
82
82
  }
@@ -13,20 +13,35 @@
13
13
 
14
14
  import {Icon} from '@iconify/react';
15
15
  import cx from 'classnames';
16
- import {Item, Menu} from 'react-contexify';
16
+ import {Item, Menu, Submenu} from 'react-contexify';
17
17
 
18
18
  import 'react-contexify/dist/ReactContexify.css';
19
19
 
20
20
  import {useParcaContext, useURLState} from '@parca/components';
21
+ import {valueFormatter} from '@parca/utilities';
21
22
 
22
23
  import {type Row} from '.';
24
+ import {getTextForCumulative} from '../ProfileFlameGraph/FlameGraphArrow/utils';
25
+ import {truncateString} from '../utils';
26
+ import {type ColumnName} from './utils/functions';
23
27
 
24
28
  interface TableContextMenuProps {
25
29
  menuId: string;
26
30
  row: Row | null;
31
+ unit?: string;
32
+ total?: bigint;
33
+ totalUnfiltered?: bigint;
34
+ columnVisibility?: Record<ColumnName, boolean>;
27
35
  }
28
36
 
29
- const TableContextMenu = ({menuId, row}: TableContextMenuProps): React.JSX.Element => {
37
+ const TableContextMenu = ({
38
+ menuId,
39
+ row,
40
+ unit,
41
+ total,
42
+ totalUnfiltered,
43
+ columnVisibility,
44
+ }: TableContextMenuProps): React.JSX.Element => {
30
45
  const [_, setSandwichFunctionName] = useURLState<string | undefined>('sandwich_function_name');
31
46
  const [dashboardItems, setDashboardItems] = useURLState<string[]>('dashboard_items', {
32
47
  alwaysReturnArray: true,
@@ -42,6 +57,133 @@ const TableContextMenu = ({menuId, row}: TableContextMenuProps): React.JSX.Eleme
42
57
  }
43
58
  };
44
59
 
60
+ const handleCopyItem = (text: string): void => {
61
+ void navigator.clipboard.writeText(text);
62
+ };
63
+
64
+ const isColumnVisible = (columnName: ColumnName): boolean => {
65
+ return columnVisibility?.[columnName] ?? true;
66
+ };
67
+
68
+ const valuesToCopy =
69
+ row !== null
70
+ ? [
71
+ ...(isColumnVisible('flat')
72
+ ? [
73
+ {
74
+ id: 'Flat',
75
+ value:
76
+ total !== null &&
77
+ total !== undefined &&
78
+ totalUnfiltered !== null &&
79
+ totalUnfiltered !== undefined
80
+ ? getTextForCumulative(row.flat, total, totalUnfiltered, unit ?? '')
81
+ : valueFormatter(row.flat, unit ?? '', 1),
82
+ },
83
+ ]
84
+ : []),
85
+ ...(isColumnVisible('flatPercentage')
86
+ ? [
87
+ {
88
+ id: 'Flat (%)',
89
+ value:
90
+ total !== null &&
91
+ total !== undefined &&
92
+ totalUnfiltered !== null &&
93
+ totalUnfiltered !== undefined
94
+ ? getTextForCumulative(row.flat, total, totalUnfiltered, unit ?? '')
95
+ : valueFormatter(row.flat, unit ?? '', 1),
96
+ },
97
+ ]
98
+ : []),
99
+ ...(isColumnVisible('flatDiff')
100
+ ? [
101
+ {
102
+ id: 'Flat Diff',
103
+ value: row.flatDiff !== 0n ? valueFormatter(row.flatDiff, unit ?? '', 1) : '',
104
+ },
105
+ ]
106
+ : []),
107
+ ...(isColumnVisible('flatDiffPercentage')
108
+ ? [
109
+ {
110
+ id: 'Flat Diff (%)',
111
+ value: row.flatDiff !== 0n ? valueFormatter(row.flatDiff, unit ?? '', 1) : '',
112
+ },
113
+ ]
114
+ : []),
115
+ ...(isColumnVisible('cumulative')
116
+ ? [
117
+ {
118
+ id: 'Cumulative',
119
+ value:
120
+ total !== null &&
121
+ total !== undefined &&
122
+ totalUnfiltered !== null &&
123
+ totalUnfiltered !== undefined
124
+ ? getTextForCumulative(row.cumulative, total, totalUnfiltered, unit ?? '')
125
+ : valueFormatter(row.cumulative, unit ?? '', 1),
126
+ },
127
+ ]
128
+ : []),
129
+ ...(isColumnVisible('cumulativePercentage')
130
+ ? [
131
+ {
132
+ id: 'Cumulative (%)',
133
+ value:
134
+ total !== null &&
135
+ total !== undefined &&
136
+ totalUnfiltered !== null &&
137
+ totalUnfiltered !== undefined
138
+ ? getTextForCumulative(row.cumulative, total, totalUnfiltered, unit ?? '')
139
+ : valueFormatter(row.cumulative, unit ?? '', 1),
140
+ },
141
+ ]
142
+ : []),
143
+ ...(isColumnVisible('cumulativeDiff')
144
+ ? [
145
+ {
146
+ id: 'Cumulative Diff',
147
+ value:
148
+ row.cumulativeDiff !== 0n
149
+ ? valueFormatter(row.cumulativeDiff, unit ?? '', 1)
150
+ : '',
151
+ },
152
+ ]
153
+ : []),
154
+ ...(isColumnVisible('cumulativeDiffPercentage')
155
+ ? [
156
+ {
157
+ id: 'Cumulative Diff (%)',
158
+ value:
159
+ row.cumulativeDiff !== 0n
160
+ ? valueFormatter(row.cumulativeDiff, unit ?? '', 1)
161
+ : '',
162
+ },
163
+ ]
164
+ : []),
165
+ ...(isColumnVisible('name')
166
+ ? [
167
+ {
168
+ id: 'Name',
169
+ value: row.name ?? '',
170
+ },
171
+ ]
172
+ : []),
173
+ ...(isColumnVisible('functionSystemName')
174
+ ? [{id: 'Function System Name', value: row.functionSystemName ?? ''}]
175
+ : []),
176
+ ...(isColumnVisible('functionFileName')
177
+ ? [{id: 'Function File Name', value: row.functionFileName ?? ''}]
178
+ : []),
179
+ ...(isColumnVisible('mappingFile')
180
+ ? [{id: 'Mapping File', value: row.mappingFile ?? ''}]
181
+ : []),
182
+ ].flat()
183
+ : [];
184
+
185
+ const nonEmptyValuesToCopy = valuesToCopy.filter(({value}) => value !== '');
186
+
45
187
  const isMenuDisabled = row === null || enableSandwichView !== true;
46
188
 
47
189
  return (
@@ -63,6 +205,31 @@ const TableContextMenu = ({menuId, row}: TableContextMenuProps): React.JSX.Eleme
63
205
  </div>
64
206
  </div>
65
207
  </Item>
208
+ <Submenu
209
+ label={
210
+ <div className="flex w-full items-center gap-2">
211
+ <Icon icon="ph:copy" />
212
+ <div>Copy</div>
213
+ </div>
214
+ }
215
+ disabled={row === null}
216
+ >
217
+ <div className="max-h-[300px] overflow-scroll">
218
+ {nonEmptyValuesToCopy.map(({id, value}: {id: string; value: string}) => (
219
+ <Item
220
+ key={id}
221
+ id={id}
222
+ onClick={() => handleCopyItem(value)}
223
+ className="dark:bg-gray-800"
224
+ >
225
+ <div className="flex flex-col dark:text-gray-300 hover:dark:text-gray-100">
226
+ <div className="text-sm">{id}</div>
227
+ <div className="text-xs">{truncateString(value, 30)}</div>
228
+ </div>
229
+ </Item>
230
+ ))}
231
+ </div>
232
+ </Submenu>
66
233
  </Menu>
67
234
  );
68
235
  };
@@ -15,9 +15,14 @@ import {forwardRef, useImperativeHandle, useState} from 'react';
15
15
 
16
16
  import {type Row} from '.';
17
17
  import TableContextMenu from './TableContextMenu';
18
+ import {type ColumnName} from './utils/functions';
18
19
 
19
20
  interface TableContextMenuWrapperProps {
20
21
  menuId: string;
22
+ unit?: string;
23
+ total?: bigint;
24
+ totalUnfiltered?: bigint;
25
+ columnVisibility?: Record<ColumnName, boolean>;
21
26
  }
22
27
 
23
28
  export interface TableContextMenuWrapperRef {
@@ -27,7 +32,7 @@ export interface TableContextMenuWrapperRef {
27
32
  const TableContextMenuWrapper = forwardRef<
28
33
  TableContextMenuWrapperRef,
29
34
  TableContextMenuWrapperProps
30
- >(({menuId}, ref) => {
35
+ >(({menuId, unit, total, totalUnfiltered, columnVisibility}, ref) => {
31
36
  const [row, setRow] = useState<Row | null>(null);
32
37
 
33
38
  useImperativeHandle(ref, () => ({
@@ -40,7 +45,16 @@ const TableContextMenuWrapper = forwardRef<
40
45
  },
41
46
  }));
42
47
 
43
- return <TableContextMenu menuId={menuId} row={row} />;
48
+ return (
49
+ <TableContextMenu
50
+ menuId={menuId}
51
+ row={row}
52
+ unit={unit}
53
+ total={total}
54
+ totalUnfiltered={totalUnfiltered}
55
+ columnVisibility={columnVisibility}
56
+ />
57
+ );
44
58
  });
45
59
 
46
60
  TableContextMenuWrapper.displayName = 'TableContextMenuWrapper';
@@ -149,6 +149,18 @@ export const Table = React.memo(function Table({
149
149
  [selectSpan, dashboardItems.length]
150
150
  );
151
151
 
152
+ const onRowContextMenu = useCallback(
153
+ (e: React.MouseEvent, row: Row) => {
154
+ e.preventDefault();
155
+ contextMenuRef.current?.setRow(row, () => {
156
+ show({
157
+ event: e,
158
+ });
159
+ });
160
+ },
161
+ [show]
162
+ );
163
+
152
164
  const rows: DataRow[] = useMemo(() => {
153
165
  if (table == null || table.numRows === 0) {
154
166
  return [];
@@ -201,72 +213,6 @@ export const Table = React.memo(function Table({
201
213
  return rows;
202
214
  }, [table, colorByColors, colorBy]);
203
215
 
204
- const handleTableContextMenu = useCallback(
205
- (e: React.MouseEvent) => {
206
- e.preventDefault();
207
-
208
- // Find the closest table row element
209
- const target = e.target as Element;
210
- const rowElement = target.closest('tr');
211
-
212
- if (rowElement !== null) {
213
- // Look for a data attribute that might contain the actual row ID
214
- const rowId = rowElement.getAttribute('data-row-id') ?? rowElement.getAttribute('data-id');
215
-
216
- if (rowId != null && rowId.length > 0) {
217
- // Find the row by ID
218
- const actualRowIndex = parseInt(rowId, 10);
219
-
220
- if (actualRowIndex >= 0 && actualRowIndex < rows.length) {
221
- const row = rows[actualRowIndex];
222
-
223
- contextMenuRef.current?.setRow(row, () => {
224
- show({
225
- event: e,
226
- });
227
- });
228
- return;
229
- }
230
- }
231
-
232
- // Fallback: try to find row by matching text content
233
- const nameCell = rowElement.querySelector('td:last-child'); // Name is usually the last column
234
- if (nameCell !== null) {
235
- const cellText = nameCell.textContent?.trim();
236
-
237
- if (cellText != null && cellText.length > 0) {
238
- // First try exact match
239
- let matchingRow = rows.find(row => row.name === cellText);
240
-
241
- // If no exact match, try partial match (in case of truncation)
242
- if (matchingRow == null) {
243
- matchingRow = rows.find(
244
- row => row.name.includes(cellText) || cellText.includes(row.name)
245
- );
246
- }
247
-
248
- // If still no match, try matching the end of the name (for cases like package.function)
249
- if (matchingRow == null) {
250
- matchingRow = rows.find(
251
- row =>
252
- row.name.endsWith(cellText) || cellText.endsWith(row.name.split('.').pop() ?? '')
253
- );
254
- }
255
-
256
- if (matchingRow != null) {
257
- contextMenuRef.current?.setRow(matchingRow, () => {
258
- show({
259
- event: e,
260
- });
261
- });
262
- }
263
- }
264
- }
265
- }
266
- },
267
- [rows, show]
268
- );
269
-
270
216
  if (loading) {
271
217
  return (
272
218
  <div className="overflow-clip h-[700px] min-h-[700px]">
@@ -289,14 +235,22 @@ export const Table = React.memo(function Table({
289
235
  transition={{duration: 0.5}}
290
236
  >
291
237
  <div className="relative">
292
- <TableContextMenuWrapper ref={contextMenuRef} menuId={MENU_ID} />
293
- <div className="font-robotoMono h-[80vh] w-full" onContextMenu={handleTableContextMenu}>
238
+ <TableContextMenuWrapper
239
+ ref={contextMenuRef}
240
+ menuId={MENU_ID}
241
+ unit={unit}
242
+ total={total}
243
+ totalUnfiltered={total}
244
+ columnVisibility={columnVisibility}
245
+ />
246
+ <div className="font-robotoMono h-[80vh] w-full">
294
247
  <TableComponent
295
248
  data={rows}
296
249
  columns={columns}
297
250
  initialSorting={initialSorting}
298
251
  columnVisibility={columnVisibility}
299
252
  onRowClick={onRowClick}
253
+ onRowContextMenu={onRowContextMenu}
300
254
  usePointerCursor={dashboardItems.length > 1}
301
255
  estimatedRowHeight={ROW_HEIGHT}
302
256
  />