@parca/profile 0.19.26 → 0.19.28

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.d.ts.map +1 -1
  3. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.js +4 -1
  4. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenuWrapper.d.ts.map +1 -1
  5. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenuWrapper.js +10 -5
  6. package/dist/ProfileFlameGraph/FlameGraphArrow/index.d.ts.map +1 -1
  7. package/dist/ProfileFlameGraph/FlameGraphArrow/index.js +7 -9
  8. package/dist/ProfileView/components/ProfileFilters/filterPresets.d.ts +1 -1
  9. package/dist/ProfileView/components/ProfileFilters/filterPresets.d.ts.map +1 -1
  10. package/dist/ProfileView/components/ProfileFilters/filterPresets.js +2 -1
  11. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts +7 -4
  12. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts.map +1 -1
  13. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.js +39 -50
  14. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts +1 -1
  15. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -1
  16. package/dist/ProfileView/components/Toolbars/index.js +1 -1
  17. package/dist/ProfileView/components/ViewSelector/index.js +1 -1
  18. package/package.json +5 -5
  19. package/src/ProfileFlameGraph/FlameGraphArrow/ContextMenu.tsx +5 -0
  20. package/src/ProfileFlameGraph/FlameGraphArrow/ContextMenuWrapper.tsx +11 -5
  21. package/src/ProfileFlameGraph/FlameGraphArrow/index.tsx +22 -21
  22. package/src/ProfileView/components/ProfileFilters/filterPresets.ts +4 -2
  23. package/src/ProfileView/components/ProfileFilters/useProfileFilters.ts +57 -76
  24. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +1 -1
  25. package/src/ProfileView/components/Toolbars/index.tsx +1 -1
  26. package/src/ProfileView/components/ViewSelector/index.tsx +1 -1
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.28](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.27...@parca/profile@0.19.28) (2025-07-22)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
10
+ ## [0.19.27](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.26...@parca/profile@0.19.27) (2025-07-21)
11
+
12
+ **Note:** Version bump only for package @parca/profile
13
+
6
14
  ## [0.19.26](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.25...@parca/profile@0.19.26) (2025-07-21)
7
15
 
8
16
  **Note:** Version bump only for package @parca/profile
@@ -1 +1 @@
1
- {"version":3,"file":"ContextMenu.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/ContextMenu.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAC,KAAK,EAAC,MAAM,cAAc,CAAC;AAOnC,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAO1C,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,QAAA,MAAM,WAAW,GAAI,kIAalB,gBAAgB,KAAG,GAAG,CAAC,OAiOzB,CAAC;AAEF,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"ContextMenu.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/ContextMenu.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAC,KAAK,EAAC,MAAM,cAAc,CAAC;AAOnC,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAO1C,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,QAAA,MAAM,WAAW,GAAI,kIAalB,gBAAgB,KAAG,GAAG,CAAC,OAsOzB,CAAC;AAEF,eAAe,WAAW,CAAC"}
@@ -93,13 +93,16 @@ const ContextMenu = ({ menuId, table, total, totalUnfiltered, row, compareAbsolu
93
93
  setDashboardItems([...dashboardItems, 'table']);
94
94
  }
95
95
  }, children: _jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx(Icon, { icon: "ph:table" }), _jsx("div", { children: "Show in table" })] }) }), enableSandwichView === true && (_jsx(Item, { id: "show-in-sandwich", onClick: () => {
96
+ if (functionName === '' || functionName == null) {
97
+ return;
98
+ }
96
99
  if (dashboardItems.includes('sandwich')) {
97
100
  setSandwichFunctionName(functionName);
98
101
  return;
99
102
  }
100
103
  setSandwichFunctionName(functionName);
101
104
  setDashboardItems([...dashboardItems, 'sandwich']);
102
- }, 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')
105
+ }, disabled: functionName === '' || functionName == null, 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')
103
106
  ? 'Focus sandwich on this frame.'
104
107
  : 'Show in sandwich', _jsx("span", { className: "absolute top-[-2px] text-xs lowercase text-red-500", children: "\u00A0alpha" })] })] }) })), _jsx(Item, { id: "reset-view", onClick: handleResetView, children: _jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx(Icon, { icon: "system-uicons:reset" }), _jsx("div", { children: "Reset graph" })] }) }), _jsxs(Item, { id: "hide-binary", onClick: () => hideBinary(getLastItem(mappingFile)), disabled: mappingFile === null || mappingFile === '', children: [_jsx("div", { "data-tooltip-id": "hide-binary-help", "data-tooltip-content": "Hide all frames for this binary", children: _jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx(Icon, { icon: "bx:bxs-hide" }), _jsxs("div", { children: ["Hide binary ", mappingFile !== null && `(${getLastItem(mappingFile)})`] })] }) }), _jsx(Tooltip, { place: "left", id: "hide-binary-help" })] }), _jsx(Submenu, { label: _jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx(Icon, { icon: "ph:copy" }), _jsx("div", { children: "Copy" })] }), 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))) }) }), checkDebuginfoStatusHandler !== undefined ? (_jsx(Item, { id: "check-debuginfo-status", onClick: () => checkDebuginfoStatusHandler(mappingBuildID), disabled: !isMappingBuildIDAvailable, children: _jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx(Icon, { icon: "bx:bx-info-circle" }), _jsx("div", { className: "relative pr-4", children: "Check debuginfo status" })] }) })) : null, _jsx(Separator, {}), _jsx(Item, { id: "dock-tooltip", onClick: handleDockTooltip, children: _jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx(Icon, { icon: "bx:dock-bottom" }), isGraphTooltipDocked ? 'Undock tooltip' : 'Dock tooltip'] }) })] }));
105
108
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ContextMenuWrapper.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/ContextMenuWrapper.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,KAAK,EAAC,MAAM,cAAc,CAAC;AAEnC,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAI1C,UAAU,uBAAuB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CACtD;AAED,QAAA,MAAM,kBAAkB,2HAkBvB,CAAC;AAIF,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"ContextMenuWrapper.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/ContextMenuWrapper.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,KAAK,EAAC,MAAM,cAAc,CAAC;AAEnC,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAI1C,UAAU,uBAAuB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CACtD;AAED,QAAA,MAAM,kBAAkB,2HAwBvB,CAAC;AAIF,eAAe,kBAAkB,CAAC"}
@@ -14,19 +14,24 @@ import { jsx as _jsx } from "react/jsx-runtime";
14
14
  import { forwardRef, useImperativeHandle, useState } from 'react';
15
15
  import ContextMenu from './ContextMenu';
16
16
  const ContextMenuWrapper = forwardRef((props, ref) => {
17
- // Fix for race condition: Always render ContextMenu to maintain component tree stability
18
- // but use callback timing to ensure correct data is available when menu shows
19
- const [row, setRow] = useState(0);
17
+ // Initialize with null to prevent rendering with invalid data
18
+ const [row, setRow] = useState(null);
20
19
  useImperativeHandle(ref, () => ({
21
20
  setRow: (newRow, callback) => {
22
21
  setRow(newRow);
23
22
  // Execute callback after state update using requestAnimationFrame
24
23
  if (callback != null) {
25
- requestAnimationFrame(callback);
24
+ requestAnimationFrame(() => {
25
+ requestAnimationFrame(callback);
26
+ });
26
27
  }
27
28
  },
28
29
  }));
29
- return _jsx(ContextMenu, { ...props, row: row });
30
+ // Only render ContextMenu when we have a valid row
31
+ if (row === null) {
32
+ return null;
33
+ }
34
+ return _jsx(ContextMenu, { ...props, row: row, isSandwich: props.isInSandwichView });
30
35
  });
31
36
  ContextMenuWrapper.displayName = 'ContextMenuWrapper';
32
37
  export default ContextMenuWrapper;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAQN,MAAM,OAAO,CAAC;AAKf,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAG9C,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAE1C,OAAO,EAAc,KAAK,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAE/D,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAIlD,OAAO,EAAuB,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAMtE,OAAO,EACL,gBAAgB,EAOjB,MAAM,SAAS,CAAC;AAEjB,eAAO,MAAM,iBAAiB,gBAAgB,CAAC;AAC/C,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,cAAc,aAAa,CAAC;AACzC,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,yBAAyB,uBAAuB,CAAC;AAC9D,eAAO,MAAM,cAAc,aAAa,CAAC;AACzC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAC7C,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,WAAW,UAAU,CAAC;AACnC,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AAEjD,UAAU,oBAAoB;IAC5B,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,UAAU,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IAC/C,YAAY,EAAE,OAAO,CAAC;IACtB,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,eAAe,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,gBAAgB,GAC3B,cAAc,MAAM,EAAE,EACtB,YAAY,OAAO,EACnB,qBAAqB,WAAW,KAC/B,aAQF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,eAAe,MAAM,EAAE,EACvB,YAAY,OAAO,EACnB,qBAAqB,WAAW,KAC/B,aAQF,CAAC;AAIF,eAAO,MAAM,eAAe,kDAsR1B,CAAC;AAEH,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAQN,MAAM,OAAO,CAAC;AAKf,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAG9C,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAE1C,OAAO,EAAc,KAAK,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAE/D,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAIlD,OAAO,EAAuB,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAMtE,OAAO,EACL,gBAAgB,EAOjB,MAAM,SAAS,CAAC;AAEjB,eAAO,MAAM,iBAAiB,gBAAgB,CAAC;AAC/C,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,cAAc,aAAa,CAAC;AACzC,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,yBAAyB,uBAAuB,CAAC;AAC9D,eAAO,MAAM,cAAc,aAAa,CAAC;AACzC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAC7C,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,WAAW,UAAU,CAAC;AACnC,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AAEjD,UAAU,oBAAoB;IAC5B,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,UAAU,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IAC/C,YAAY,EAAE,OAAO,CAAC;IACtB,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,eAAe,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,gBAAgB,GAC3B,cAAc,MAAM,EAAE,EACtB,YAAY,OAAO,EACnB,qBAAqB,WAAW,KAC/B,aAQF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,eAAe,MAAM,EAAE,EACvB,YAAY,OAAO,EACnB,qBAAqB,WAAW,KAC/B,aAQF,CAAC;AAIF,eAAO,MAAM,eAAe,kDAuR1B,CAAC;AAEH,eAAe,eAAe,CAAC"}
@@ -148,7 +148,11 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({ arrow, total, fil
148
148
  // Add a new frame filter to hide this binary using the new ProfileFilters system
149
149
  excludeBinary(binaryToRemove);
150
150
  };
151
- const handleRowClick = (row) => {
151
+ const handleRowClick = useCallback((row) => {
152
+ if (isFlameChart) {
153
+ // In flame charts, we don't want to expand the node, so we return early.
154
+ return;
155
+ }
152
156
  // Walk down the stack starting at row until we reach the root (row 0).
153
157
  const path = [];
154
158
  let currentRow = row;
@@ -160,7 +164,7 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({ arrow, total, fil
160
164
  // Reverse the path so that the root is first.
161
165
  path.reverse();
162
166
  setCurPath(path);
163
- };
167
+ }, [table, setCurPath, isFlameChart]);
164
168
  const depthColumn = table.getChild(FIELD_DEPTH);
165
169
  const maxDepth = getMaxDepth(depthColumn);
166
170
  // Apply frame limit if maxFrameCount is provided and not expanded
@@ -215,12 +219,6 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({ arrow, total, fil
215
219
  return (_jsx(TooltipProvider, { table: table, total: total, totalUnfiltered: total + filtered, profileType: profileType, unit: arrow.unit, compareAbsolute: compareAbsolute, tooltipId: tooltipId, children: _jsxs("div", { className: "relative", children: [_jsx(ContextMenuWrapper, { ref: contextMenuRef, menuId: MENU_ID, table: table, total: total, totalUnfiltered: total + filtered, compareAbsolute: compareAbsolute, resetPath: () => setCurPath([]), hideMenu: hideAll, hideBinary: hideBinary, unit: arrow.unit, profileType: profileType, isInSandwichView: isInSandwichView }), _jsx(MemoizedTooltip, { contextElement: svgElement, dockedMetainfo: dockedMetainfo }), _jsx("div", { ref: containerRef, className: "overflow-auto scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-100 dark:scrollbar-thumb-gray-600 dark:scrollbar-track-gray-800 will-change-transform scroll-smooth webkit-overflow-scrolling-touch contain", style: {
216
220
  width: width ?? '100%',
217
221
  contain: 'layout style paint',
218
- }, children: _jsx("svg", { className: "font-robotoMono", width: width ?? 0, height: totalHeight, preserveAspectRatio: "xMinYMid", ref: svg, children: visibleNodes.map(row => (_jsx(FlameNode, { table: table, row: row, colors: colorByColors, colorBy: colorByValue, totalWidth: width ?? 1, height: RowHeight, darkMode: isDarkMode, compareMode: compareMode, colorForSimilarNodes: colorForSimilarNodes, selectedRow: selectedRow, onClick: () => {
219
- if (isFlameChart) {
220
- // We don't want to expand in flame charts.
221
- return;
222
- }
223
- handleRowClick(row);
224
- }, onContextMenu: displayMenu, hoveringRow: highlightSimilarStacksPreference ? hoveringRow : undefined, setHoveringRow: highlightSimilarStacksPreference ? setHoveringRow : noop, isFlameChart: isFlameChart, profileSource: profileSource, isRenderedAsFlamegraph: isRenderedAsFlamegraph, isInSandwichView: isInSandwichView, maxDepth: maxDepth, effectiveDepth: deferredEffectiveDepth, tooltipId: tooltipId }, row))) }) })] }) }));
222
+ }, children: _jsx("svg", { className: "font-robotoMono", width: width ?? 0, height: totalHeight, preserveAspectRatio: "xMinYMid", ref: svg, children: visibleNodes.map(row => (_jsx(FlameNode, { table: table, row: row, colors: colorByColors, colorBy: colorByValue, totalWidth: width ?? 1, height: RowHeight, darkMode: isDarkMode, compareMode: compareMode, colorForSimilarNodes: colorForSimilarNodes, selectedRow: selectedRow, onClick: () => handleRowClick(row), onContextMenu: displayMenu, hoveringRow: highlightSimilarStacksPreference ? hoveringRow : undefined, setHoveringRow: highlightSimilarStacksPreference ? setHoveringRow : noop, isFlameChart: isFlameChart, profileSource: profileSource, isRenderedAsFlamegraph: isRenderedAsFlamegraph, isInSandwichView: isInSandwichView, maxDepth: maxDepth, effectiveDepth: deferredEffectiveDepth, tooltipId: tooltipId }, row))) }) })] }) }));
225
223
  });
226
224
  export default FlameGraphArrow;
@@ -1,4 +1,4 @@
1
- import type { ProfileFilter } from '@parca/store';
1
+ import type { ProfileFilter } from './useProfileFilters';
2
2
  export interface FilterPreset {
3
3
  key: string;
4
4
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"filterPresets.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/ProfileFilters/filterPresets.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAEhD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC;CAC3C;AAED,eAAO,MAAM,aAAa,EAAE,YAAY,EA6CvC,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,OAEzC,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,YAAY,GAAG,SAE3D,CAAC"}
1
+ {"version":3,"file":"filterPresets.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/ProfileFilters/filterPresets.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAEvD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC;CAC3C;AAED,eAAO,MAAM,aAAa,EAAE,YAAY,EA6CvC,CAAC;AAIF,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,OAEzC,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,YAAY,GAAG,SAE3D,CAAC"}
@@ -56,8 +56,9 @@ export const filterPresets = [
56
56
  ],
57
57
  },
58
58
  ];
59
+ const presetKeys = new Set(filterPresets.map(preset => preset.key));
59
60
  export const isPresetKey = (key) => {
60
- return filterPresets.some(preset => preset.key === key);
61
+ return presetKeys.has(key);
61
62
  };
62
63
  export const getPresetByKey = (key) => {
63
64
  return filterPresets.find(preset => preset.key === key);
@@ -1,7 +1,11 @@
1
1
  import { type Filter } from '@parca/client';
2
- import { type ProfileFilter } from '@parca/store';
3
- import { type FilterPreset } from './filterPresets';
4
- export type { ProfileFilter };
2
+ export interface ProfileFilter {
3
+ id: string;
4
+ type?: 'stack' | 'frame' | string;
5
+ field?: 'function_name' | 'binary' | 'system_name' | 'filename' | 'address' | 'line_number';
6
+ matchType?: 'equal' | 'not_equal' | 'contains' | 'not_contains';
7
+ value: string;
8
+ }
5
9
  export declare const convertToProtoFilters: (profileFilters: ProfileFilter[]) => Filter[];
6
10
  export declare const useProfileFilters: () => {
7
11
  localFilters: ProfileFilter[];
@@ -15,6 +19,5 @@ export declare const useProfileFilters: () => {
15
19
  removeFilter: (id: string) => void;
16
20
  updateFilter: (id: string, updates: Partial<ProfileFilter>) => void;
17
21
  resetFilters: () => void;
18
- applyPreset: (preset: FilterPreset) => void;
19
22
  };
20
23
  //# sourceMappingURL=useProfileFilters.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useProfileFilters.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/ProfileFilters/useProfileFilters.ts"],"names":[],"mappings":"AAeA,OAAO,EAAC,KAAK,MAAM,EAAC,MAAM,eAAe,CAAC;AAC1C,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAA8B,KAAK,YAAY,EAAC,MAAM,iBAAiB,CAAC;AAG/E,YAAY,EAAC,aAAa,EAAC,CAAC;AAG5B,eAAO,MAAM,qBAAqB,GAAI,gBAAgB,aAAa,EAAE,KAAG,MAAM,EAoG7E,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAO;IACnC,YAAY,EAAE,aAAa,EAAE,CAAC;IAC9B,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,aAAa,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,mBAAmB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,KAAK,IAAI,CAAC;IACpE,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,WAAW,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;CAwM7C,CAAC"}
1
+ {"version":3,"file":"useProfileFilters.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/ProfileFilters/useProfileFilters.ts"],"names":[],"mappings":"AAeA,OAAO,EAAC,KAAK,MAAM,EAAC,MAAM,eAAe,CAAC;AAK1C,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;IAClC,KAAK,CAAC,EAAE,eAAe,GAAG,QAAQ,GAAG,aAAa,GAAG,UAAU,GAAG,SAAS,GAAG,aAAa,CAAC;IAC5F,SAAS,CAAC,EAAE,OAAO,GAAG,WAAW,GAAG,UAAU,GAAG,cAAc,CAAC;IAChE,KAAK,EAAE,MAAM,CAAC;CACf;AAGD,eAAO,MAAM,qBAAqB,GAAI,gBAAgB,aAAa,EAAE,KAAG,MAAM,EAqG7E,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAO;IACnC,YAAY,EAAE,aAAa,EAAE,CAAC;IAC9B,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,aAAa,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,mBAAmB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,KAAK,IAAI,CAAC;IACpE,YAAY,EAAE,MAAM,IAAI,CAAC;CAsL1B,CAAC"}
@@ -10,8 +10,7 @@
10
10
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
- import { useCallback, useEffect, useMemo } from 'react';
14
- import { selectLocalFilters, setLocalFilters, useAppDispatch, useAppSelector, } from '@parca/store';
13
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
15
14
  import { getPresetByKey, isPresetKey } from './filterPresets';
16
15
  import { useProfileFiltersUrlState } from './useProfileFiltersUrlState';
17
16
  // Convert ProfileFilter[] to protobuf Filter[] matching the expected structure
@@ -27,6 +26,7 @@ export const convertToProtoFilters = (profileFilters) => {
27
26
  expandedFilters.push({
28
27
  ...presetFilter,
29
28
  id: `${filter.id}-expanded-${index}`,
29
+ value: presetFilter.value,
30
30
  });
31
31
  });
32
32
  }
@@ -114,29 +114,29 @@ export const convertToProtoFilters = (profileFilters) => {
114
114
  };
115
115
  export const useProfileFilters = () => {
116
116
  const { appliedFilters, setAppliedFilters } = useProfileFiltersUrlState();
117
- const dispatch = useAppDispatch();
118
- const localFilters = useAppSelector(selectLocalFilters);
117
+ const [localFilters, setLocalFilters] = useState([]);
118
+ const lastAppliedFiltersRef = useRef([]);
119
+ const localFiltersRef = useRef(localFilters);
120
+ localFiltersRef.current = localFilters;
119
121
  useEffect(() => {
120
- if (appliedFilters != null && appliedFilters.length > 0) {
121
- // Check if they're different to avoid unnecessary updates
122
- const areFiltersEqual = appliedFilters.length === localFilters.length &&
123
- appliedFilters.every((applied, index) => {
124
- const local = localFilters[index];
125
- return (local != null &&
126
- applied.type === local.type &&
127
- applied.field === local.field &&
128
- applied.matchType === local.matchType &&
129
- applied.value === local.value);
130
- });
131
- if (!areFiltersEqual) {
132
- dispatch(setLocalFilters(appliedFilters));
133
- }
134
- }
135
- else if (appliedFilters != null && appliedFilters.length === 0 && localFilters.length > 0) {
136
- dispatch(setLocalFilters([]));
122
+ const currentApplied = appliedFilters ?? [];
123
+ const lastApplied = lastAppliedFiltersRef.current;
124
+ // Check if appliedFilters actually changed (avoid circular updates)
125
+ const appliedChanged = currentApplied.length !== lastApplied.length ||
126
+ currentApplied.some((applied, index) => {
127
+ const last = lastApplied[index];
128
+ return (last == null ||
129
+ applied.type !== last.type ||
130
+ applied.field !== last.field ||
131
+ applied.matchType !== last.matchType ||
132
+ applied.value !== last.value);
133
+ });
134
+ if (!appliedChanged) {
135
+ return;
137
136
  }
138
- // eslint-disable-next-line react-hooks/exhaustive-deps
139
- }, []);
137
+ lastAppliedFiltersRef.current = currentApplied;
138
+ setLocalFilters(currentApplied);
139
+ }, [appliedFilters]);
140
140
  const hasUnsavedChanges = useMemo(() => {
141
141
  const localWithValues = localFilters.filter(f => {
142
142
  // For preset filters, only need type and value
@@ -169,8 +169,8 @@ export const useProfileFilters = () => {
169
169
  id: `filter-${Date.now()}-${Math.random()}`,
170
170
  value: '',
171
171
  };
172
- dispatch(setLocalFilters([...localFilters, newFilter]));
173
- }, [dispatch, localFilters]);
172
+ setLocalFilters([...localFiltersRef.current, newFilter]);
173
+ }, []);
174
174
  const excludeBinary = useCallback((binaryName) => {
175
175
  // Check if this binary is already being filtered with not_contains
176
176
  const existingFilter = (appliedFilters ?? []).find(f => f.type === 'frame' &&
@@ -187,11 +187,13 @@ export const useProfileFilters = () => {
187
187
  matchType: 'not_contains',
188
188
  value: binaryName,
189
189
  };
190
- dispatch(setLocalFilters([...localFilters, newFilter]));
190
+ setLocalFilters([...localFiltersRef.current, newFilter]);
191
191
  // Auto-apply the filter since it has a value
192
192
  const filtersToApply = [...(appliedFilters ?? []), newFilter];
193
193
  setAppliedFilters(filtersToApply);
194
- }, [appliedFilters, setAppliedFilters, dispatch, localFilters]);
194
+ },
195
+ // eslint-disable-next-line react-hooks/exhaustive-deps
196
+ [setAppliedFilters]);
195
197
  const removeExcludeBinary = useCallback((binaryName) => {
196
198
  // Search for the exclude filter (not_contains) for this binary
197
199
  const filterToRemove = (appliedFilters ?? []).find(f => f.type === 'frame' &&
@@ -203,22 +205,21 @@ export const useProfileFilters = () => {
203
205
  const updatedAppliedFilters = (appliedFilters ?? []).filter(f => f.id !== filterToRemove.id);
204
206
  setAppliedFilters(updatedAppliedFilters);
205
207
  // Also remove from local filters
206
- const updatedLocalFilters = localFilters.filter(f => f.id !== filterToRemove.id);
207
- dispatch(setLocalFilters(updatedLocalFilters));
208
+ setLocalFilters(localFiltersRef.current.filter(f => f.id !== filterToRemove.id));
208
209
  }
209
- }, [appliedFilters, setAppliedFilters, dispatch, localFilters]);
210
+ }, [appliedFilters, setAppliedFilters]);
210
211
  const removeFilter = useCallback((id) => {
211
- dispatch(setLocalFilters(localFilters.filter(f => f.id !== id)));
212
- }, [dispatch, localFilters]);
212
+ setLocalFilters(localFiltersRef.current.filter(f => f.id !== id));
213
+ }, []);
213
214
  const updateFilter = useCallback((id, updates) => {
214
- dispatch(setLocalFilters(localFilters.map(f => (f.id === id ? { ...f, ...updates } : f))));
215
- }, [dispatch, localFilters]);
215
+ setLocalFilters(localFiltersRef.current.map(f => (f.id === id ? { ...f, ...updates } : f)));
216
+ }, []);
216
217
  const resetFilters = useCallback(() => {
217
- dispatch(setLocalFilters([]));
218
+ setLocalFilters([]);
218
219
  setAppliedFilters([]);
219
- }, [dispatch, setAppliedFilters]);
220
+ }, [setAppliedFilters]);
220
221
  const onApplyFilters = useCallback(() => {
221
- const validFilters = localFilters.filter(f => {
222
+ const validFilters = localFiltersRef.current.filter(f => {
222
223
  // For preset filters, only need type and value
223
224
  if (f.type != null && isPresetKey(f.type)) {
224
225
  return f.value !== '' && f.type != null;
@@ -231,21 +232,10 @@ export const useProfileFilters = () => {
231
232
  id: `filter-${Date.now()}-${index}`,
232
233
  }));
233
234
  setAppliedFilters(filtersToApply);
234
- }, [localFilters, setAppliedFilters]);
235
+ }, [setAppliedFilters]);
235
236
  const protoFilters = useMemo(() => {
236
237
  return convertToProtoFilters(appliedFilters ?? []);
237
238
  }, [appliedFilters]);
238
- const applyPreset = useCallback((preset) => {
239
- const presetFilter = {
240
- id: `filter-preset-${Date.now()}`,
241
- type: preset.key,
242
- value: preset.name,
243
- };
244
- // Add preset filter to existing filters
245
- const updatedFilters = [...localFilters, presetFilter];
246
- dispatch(setLocalFilters(updatedFilters));
247
- setAppliedFilters(updatedFilters);
248
- }, [dispatch, setAppliedFilters, localFilters]);
249
239
  return {
250
240
  localFilters,
251
241
  appliedFilters,
@@ -258,6 +248,5 @@ export const useProfileFilters = () => {
258
248
  removeFilter,
259
249
  updateFilter,
260
250
  resetFilters,
261
- applyPreset,
262
251
  };
263
252
  };
@@ -1,5 +1,5 @@
1
1
  import { type ParamValueSetterCustom } from '@parca/components';
2
- import { type ProfileFilter } from '@parca/store';
2
+ import { type ProfileFilter } from './useProfileFilters';
3
3
  export declare const decodeProfileFilters: (encoded: string) => ProfileFilter[];
4
4
  export declare const useProfileFiltersUrlState: () => {
5
5
  appliedFilters: ProfileFilter[];
@@ -1 +1 @@
1
- {"version":3,"file":"useProfileFiltersUrlState.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts"],"names":[],"mappings":"AAaA,OAAO,EAAoB,KAAK,sBAAsB,EAAC,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAC,KAAK,aAAa,EAAC,MAAM,cAAc,CAAC;AAwDhD,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,KAAG,aAAa,EAkCnE,CAAC;AAEF,eAAO,MAAM,yBAAyB,QAAO;IAC3C,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,iBAAiB,EAAE,sBAAsB,CAAC,aAAa,EAAE,CAAC,CAAC;CAmB5D,CAAC"}
1
+ {"version":3,"file":"useProfileFiltersUrlState.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts"],"names":[],"mappings":"AAaA,OAAO,EAAoB,KAAK,sBAAsB,EAAC,MAAM,mBAAmB,CAAC;AAGjF,OAAO,EAAC,KAAK,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAsDvD,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,KAAG,aAAa,EAkCnE,CAAC;AAEF,eAAO,MAAM,yBAAyB,QAAO;IAC3C,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,iBAAiB,EAAE,sBAAsB,CAAC,aAAa,EAAE,CAAC,CAAC;CAmB5D,CAAC"}
@@ -31,5 +31,5 @@ export const VisualisationToolbar = ({ groupBy, toggleGroupBy, groupByLabels, se
31
31
  fields: groupBy ?? [],
32
32
  };
33
33
  }
34
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex w-full justify-between items-end", children: [_jsxs("div", { className: "flex gap-2 items-end", children: [isGraphViz && (_jsxs(_Fragment, { children: [_jsx(GroupByDropdown, { groupBy: groupBy, labels: groupByLabels, setGroupByLabels: setGroupByLabels }), _jsx(InvertCallStack, {})] })), _jsx(ProfileFilters, {}), profileViewExternalSubActions != null ? profileViewExternalSubActions : null] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(MultiLevelDropdown, { groupBy: groupBy, toggleGroupBy: toggleGroupBy, profileType: profileType, onSelect: () => { }, isTableVizOnly: isTableVizOnly }), _jsx(ShareButton, { profileSource: profileSource, queryClient: queryClient, queryRequest: req, onDownloadPProf: onDownloadPProf, pprofdownloading: pprofdownloading ?? false, profileViewExternalSubActions: profileViewExternalSubActions }), showVisualizationSelector ? _jsx(ViewSelector, { profileSource: profileSource }) : null] })] }), isGraphVizOnly && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsx(FlameGraphToolbar, { curPath: curPath, setNewCurPath: setNewCurPath })] })), isTableVizOnly && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsx(TableToolbar, { profileType: profileType, total: total, filtered: filtered })] }))] }));
34
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex w-full justify-between items-end gap-2", children: [_jsxs("div", { className: "flex gap-2 items-end", children: [isGraphViz && (_jsxs(_Fragment, { children: [_jsx(GroupByDropdown, { groupBy: groupBy, labels: groupByLabels, setGroupByLabels: setGroupByLabels }), _jsx(InvertCallStack, {})] })), _jsx(ProfileFilters, {}), profileViewExternalSubActions != null ? profileViewExternalSubActions : null] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(MultiLevelDropdown, { groupBy: groupBy, toggleGroupBy: toggleGroupBy, profileType: profileType, onSelect: () => { }, isTableVizOnly: isTableVizOnly }), _jsx(ShareButton, { profileSource: profileSource, queryClient: queryClient, queryRequest: req, onDownloadPProf: onDownloadPProf, pprofdownloading: pprofdownloading ?? false, profileViewExternalSubActions: profileViewExternalSubActions }), showVisualizationSelector ? _jsx(ViewSelector, { profileSource: profileSource }) : null] })] }), isGraphVizOnly && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsx(FlameGraphToolbar, { curPath: curPath, setNewCurPath: setNewCurPath })] })), isTableVizOnly && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsx(TableToolbar, { profileType: profileType, total: total, filtered: filtered })] }))] }));
35
35
  };
@@ -34,7 +34,7 @@ const ViewSelector = ({ profileSource }) => {
34
34
  allItems.push({ key: 'source', label: 'Source', canBeSelected: false });
35
35
  }
36
36
  const getOption = ({ label, supportingText, }) => {
37
- const title = (_jsx("span", { className: "capitalize", children: typeof label === 'string' ? label.replaceAll('-', ' ') : label }));
37
+ const title = (_jsx("span", { className: "capitalize whitespace-nowrap", children: typeof label === 'string' ? label.replaceAll('-', ' ') : label }));
38
38
  return {
39
39
  active: title,
40
40
  expanded: (_jsxs(_Fragment, { children: [title, supportingText !== null && _jsx("span", { className: "text-xs", children: supportingText })] })),
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.19.26",
3
+ "version": "0.19.28",
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.352",
10
+ "@parca/components": "0.16.353",
11
11
  "@parca/dynamicsize": "0.16.65",
12
- "@parca/hooks": "0.0.97",
12
+ "@parca/hooks": "0.0.98",
13
13
  "@parca/icons": "0.16.72",
14
14
  "@parca/parser": "0.16.79",
15
- "@parca/store": "0.16.181",
15
+ "@parca/store": "0.16.182",
16
16
  "@parca/utilities": "0.0.105",
17
17
  "@popperjs/core": "^2.11.8",
18
18
  "@protobuf-ts/runtime-rpc": "^2.5.0",
@@ -78,5 +78,5 @@
78
78
  "access": "public",
79
79
  "registry": "https://registry.npmjs.org/"
80
80
  },
81
- "gitHead": "efe1ffd0a42671c5c6bf561ebd6c132015f616f1"
81
+ "gitHead": "fcd8ab30d83c0bc8f23e4c244da5e2b06aacf74a"
82
82
  }
@@ -188,6 +188,10 @@ const ContextMenu = ({
188
188
  <Item
189
189
  id="show-in-sandwich"
190
190
  onClick={() => {
191
+ if (functionName === '' || functionName == null) {
192
+ return;
193
+ }
194
+
191
195
  if (dashboardItems.includes('sandwich')) {
192
196
  setSandwichFunctionName(functionName);
193
197
  return;
@@ -196,6 +200,7 @@ const ContextMenu = ({
196
200
  setSandwichFunctionName(functionName);
197
201
  setDashboardItems([...dashboardItems, 'sandwich']);
198
202
  }}
203
+ disabled={functionName === '' || functionName == null}
199
204
  >
200
205
  <div className="flex w-full items-center gap-2">
201
206
  <Icon icon="tdesign:sandwich-filled" />
@@ -39,21 +39,27 @@ export interface ContextMenuWrapperRef {
39
39
 
40
40
  const ContextMenuWrapper = forwardRef<ContextMenuWrapperRef, ContextMenuWrapperProps>(
41
41
  (props, ref) => {
42
- // Fix for race condition: Always render ContextMenu to maintain component tree stability
43
- // but use callback timing to ensure correct data is available when menu shows
44
- const [row, setRow] = useState(0);
42
+ // Initialize with null to prevent rendering with invalid data
43
+ const [row, setRow] = useState<number | null>(null);
45
44
 
46
45
  useImperativeHandle(ref, () => ({
47
46
  setRow: (newRow: number, callback?: () => void) => {
48
47
  setRow(newRow);
49
48
  // Execute callback after state update using requestAnimationFrame
50
49
  if (callback != null) {
51
- requestAnimationFrame(callback);
50
+ requestAnimationFrame(() => {
51
+ requestAnimationFrame(callback);
52
+ });
52
53
  }
53
54
  },
54
55
  }));
55
56
 
56
- return <ContextMenu {...props} row={row} />;
57
+ // Only render ContextMenu when we have a valid row
58
+ if (row === null) {
59
+ return null;
60
+ }
61
+
62
+ return <ContextMenu {...props} row={row} isSandwich={props.isInSandwichView} />;
57
63
  }
58
64
  );
59
65
 
@@ -247,20 +247,27 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
247
247
  excludeBinary(binaryToRemove);
248
248
  };
249
249
 
250
- const handleRowClick = (row: number): void => {
251
- // Walk down the stack starting at row until we reach the root (row 0).
252
- const path: CurrentPathFrame[] = [];
253
- let currentRow = row;
254
- while (currentRow > 0) {
255
- const frame = getCurrentPathFrameData(table, currentRow);
256
- path.push(frame);
257
- currentRow = table.getChild(FIELD_PARENT)?.get(currentRow) ?? 0;
258
- }
259
-
260
- // Reverse the path so that the root is first.
261
- path.reverse();
262
- setCurPath(path);
263
- };
250
+ const handleRowClick = useCallback(
251
+ (row: number): void => {
252
+ if (isFlameChart) {
253
+ // In flame charts, we don't want to expand the node, so we return early.
254
+ return;
255
+ }
256
+ // Walk down the stack starting at row until we reach the root (row 0).
257
+ const path: CurrentPathFrame[] = [];
258
+ let currentRow = row;
259
+ while (currentRow > 0) {
260
+ const frame = getCurrentPathFrameData(table, currentRow);
261
+ path.push(frame);
262
+ currentRow = table.getChild(FIELD_PARENT)?.get(currentRow) ?? 0;
263
+ }
264
+
265
+ // Reverse the path so that the root is first.
266
+ path.reverse();
267
+ setCurPath(path);
268
+ },
269
+ [table, setCurPath, isFlameChart]
270
+ );
264
271
 
265
272
  const depthColumn = table.getChild(FIELD_DEPTH);
266
273
  const maxDepth = getMaxDepth(depthColumn);
@@ -377,13 +384,7 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
377
384
  compareMode={compareMode}
378
385
  colorForSimilarNodes={colorForSimilarNodes}
379
386
  selectedRow={selectedRow}
380
- onClick={() => {
381
- if (isFlameChart) {
382
- // We don't want to expand in flame charts.
383
- return;
384
- }
385
- handleRowClick(row);
386
- }}
387
+ onClick={() => handleRowClick(row)}
387
388
  onContextMenu={displayMenu}
388
389
  hoveringRow={highlightSimilarStacksPreference ? hoveringRow : undefined}
389
390
  setHoveringRow={highlightSimilarStacksPreference ? setHoveringRow : noop}
@@ -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 type {ProfileFilter} from '@parca/store';
14
+ import type {ProfileFilter} from './useProfileFilters';
15
15
 
16
16
  export interface FilterPreset {
17
17
  key: string;
@@ -67,8 +67,10 @@ export const filterPresets: FilterPreset[] = [
67
67
  },
68
68
  ];
69
69
 
70
+ const presetKeys = new Set(filterPresets.map(preset => preset.key));
71
+
70
72
  export const isPresetKey = (key: string): boolean => {
71
- return filterPresets.some(preset => preset.key === key);
73
+ return presetKeys.has(key);
72
74
  };
73
75
 
74
76
  export const getPresetByKey = (key: string): FilterPreset | undefined => {
@@ -11,21 +11,20 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {useCallback, useEffect, useMemo} from 'react';
14
+ import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
15
15
 
16
16
  import {type Filter} from '@parca/client';
17
- import {
18
- selectLocalFilters,
19
- setLocalFilters,
20
- useAppDispatch,
21
- useAppSelector,
22
- type ProfileFilter,
23
- } from '@parca/store';
24
-
25
- import {getPresetByKey, isPresetKey, type FilterPreset} from './filterPresets';
17
+
18
+ import {getPresetByKey, isPresetKey} from './filterPresets';
26
19
  import {useProfileFiltersUrlState} from './useProfileFiltersUrlState';
27
20
 
28
- export type {ProfileFilter};
21
+ export interface ProfileFilter {
22
+ id: string;
23
+ type?: 'stack' | 'frame' | string; // string allows preset keys
24
+ field?: 'function_name' | 'binary' | 'system_name' | 'filename' | 'address' | 'line_number';
25
+ matchType?: 'equal' | 'not_equal' | 'contains' | 'not_contains';
26
+ value: string;
27
+ }
29
28
 
30
29
  // Convert ProfileFilter[] to protobuf Filter[] matching the expected structure
31
30
  export const convertToProtoFilters = (profileFilters: ProfileFilter[]): Filter[] => {
@@ -41,6 +40,7 @@ export const convertToProtoFilters = (profileFilters: ProfileFilter[]): Filter[]
41
40
  expandedFilters.push({
42
41
  ...presetFilter,
43
42
  id: `${filter.id}-expanded-${index}`,
43
+ value: presetFilter.value,
44
44
  });
45
45
  });
46
46
  }
@@ -142,36 +142,41 @@ export const useProfileFilters = (): {
142
142
  removeFilter: (id: string) => void;
143
143
  updateFilter: (id: string, updates: Partial<ProfileFilter>) => void;
144
144
  resetFilters: () => void;
145
- applyPreset: (preset: FilterPreset) => void;
146
145
  } => {
147
146
  const {appliedFilters, setAppliedFilters} = useProfileFiltersUrlState();
148
- const dispatch = useAppDispatch();
149
- const localFilters = useAppSelector(selectLocalFilters);
147
+
148
+ const [localFilters, setLocalFilters] = useState<ProfileFilter[]>([]);
149
+
150
+ const lastAppliedFiltersRef = useRef<ProfileFilter[]>([]);
151
+
152
+ const localFiltersRef = useRef<ProfileFilter[]>(localFilters);
153
+ localFiltersRef.current = localFilters;
150
154
 
151
155
  useEffect(() => {
152
- if (appliedFilters != null && appliedFilters.length > 0) {
153
- // Check if they're different to avoid unnecessary updates
154
- const areFiltersEqual =
155
- appliedFilters.length === localFilters.length &&
156
- appliedFilters.every((applied, index) => {
157
- const local = localFilters[index];
158
- return (
159
- local != null &&
160
- applied.type === local.type &&
161
- applied.field === local.field &&
162
- applied.matchType === local.matchType &&
163
- applied.value === local.value
164
- );
165
- });
156
+ const currentApplied = appliedFilters ?? [];
157
+ const lastApplied = lastAppliedFiltersRef.current;
158
+
159
+ // Check if appliedFilters actually changed (avoid circular updates)
160
+ const appliedChanged =
161
+ currentApplied.length !== lastApplied.length ||
162
+ currentApplied.some((applied, index) => {
163
+ const last = lastApplied[index];
164
+ return (
165
+ last == null ||
166
+ applied.type !== last.type ||
167
+ applied.field !== last.field ||
168
+ applied.matchType !== last.matchType ||
169
+ applied.value !== last.value
170
+ );
171
+ });
166
172
 
167
- if (!areFiltersEqual) {
168
- dispatch(setLocalFilters(appliedFilters));
169
- }
170
- } else if (appliedFilters != null && appliedFilters.length === 0 && localFilters.length > 0) {
171
- dispatch(setLocalFilters([]));
173
+ if (!appliedChanged) {
174
+ return;
172
175
  }
173
- // eslint-disable-next-line react-hooks/exhaustive-deps
174
- }, []);
176
+
177
+ lastAppliedFiltersRef.current = currentApplied;
178
+ setLocalFilters(currentApplied);
179
+ }, [appliedFilters]);
175
180
 
176
181
  const hasUnsavedChanges = useMemo(() => {
177
182
  const localWithValues = localFilters.filter(f => {
@@ -210,8 +215,8 @@ export const useProfileFilters = (): {
210
215
  id: `filter-${Date.now()}-${Math.random()}`,
211
216
  value: '',
212
217
  };
213
- dispatch(setLocalFilters([...localFilters, newFilter]));
214
- }, [dispatch, localFilters]);
218
+ setLocalFilters([...localFiltersRef.current, newFilter]);
219
+ }, []);
215
220
 
216
221
  const excludeBinary = useCallback(
217
222
  (binaryName: string) => {
@@ -235,13 +240,14 @@ export const useProfileFilters = (): {
235
240
  matchType: 'not_contains',
236
241
  value: binaryName,
237
242
  };
238
- dispatch(setLocalFilters([...localFilters, newFilter]));
243
+ setLocalFilters([...localFiltersRef.current, newFilter]);
239
244
 
240
245
  // Auto-apply the filter since it has a value
241
246
  const filtersToApply = [...(appliedFilters ?? []), newFilter];
242
247
  setAppliedFilters(filtersToApply);
243
248
  },
244
- [appliedFilters, setAppliedFilters, dispatch, localFilters]
249
+ // eslint-disable-next-line react-hooks/exhaustive-deps
250
+ [setAppliedFilters]
245
251
  );
246
252
 
247
253
  const removeExcludeBinary = useCallback(
@@ -263,34 +269,27 @@ export const useProfileFilters = (): {
263
269
  setAppliedFilters(updatedAppliedFilters);
264
270
 
265
271
  // Also remove from local filters
266
- const updatedLocalFilters = localFilters.filter(f => f.id !== filterToRemove.id);
267
- dispatch(setLocalFilters(updatedLocalFilters));
272
+ setLocalFilters(localFiltersRef.current.filter(f => f.id !== filterToRemove.id));
268
273
  }
269
274
  },
270
- [appliedFilters, setAppliedFilters, dispatch, localFilters]
275
+ [appliedFilters, setAppliedFilters]
271
276
  );
272
277
 
273
- const removeFilter = useCallback(
274
- (id: string) => {
275
- dispatch(setLocalFilters(localFilters.filter(f => f.id !== id)));
276
- },
277
- [dispatch, localFilters]
278
- );
278
+ const removeFilter = useCallback((id: string) => {
279
+ setLocalFilters(localFiltersRef.current.filter(f => f.id !== id));
280
+ }, []);
279
281
 
280
- const updateFilter = useCallback(
281
- (id: string, updates: Partial<ProfileFilter>) => {
282
- dispatch(setLocalFilters(localFilters.map(f => (f.id === id ? {...f, ...updates} : f))));
283
- },
284
- [dispatch, localFilters]
285
- );
282
+ const updateFilter = useCallback((id: string, updates: Partial<ProfileFilter>) => {
283
+ setLocalFilters(localFiltersRef.current.map(f => (f.id === id ? {...f, ...updates} : f)));
284
+ }, []);
286
285
 
287
286
  const resetFilters = useCallback(() => {
288
- dispatch(setLocalFilters([]));
287
+ setLocalFilters([]);
289
288
  setAppliedFilters([]);
290
- }, [dispatch, setAppliedFilters]);
289
+ }, [setAppliedFilters]);
291
290
 
292
291
  const onApplyFilters = useCallback((): void => {
293
- const validFilters = localFilters.filter(f => {
292
+ const validFilters = localFiltersRef.current.filter(f => {
294
293
  // For preset filters, only need type and value
295
294
  if (f.type != null && isPresetKey(f.type)) {
296
295
  return f.value !== '' && f.type != null;
@@ -305,29 +304,12 @@ export const useProfileFilters = (): {
305
304
  }));
306
305
 
307
306
  setAppliedFilters(filtersToApply);
308
- }, [localFilters, setAppliedFilters]);
307
+ }, [setAppliedFilters]);
309
308
 
310
309
  const protoFilters = useMemo(() => {
311
310
  return convertToProtoFilters(appliedFilters ?? []);
312
311
  }, [appliedFilters]);
313
312
 
314
- const applyPreset = useCallback(
315
- (preset: FilterPreset) => {
316
- const presetFilter: ProfileFilter = {
317
- id: `filter-preset-${Date.now()}`,
318
- type: preset.key,
319
- value: preset.name,
320
- };
321
-
322
- // Add preset filter to existing filters
323
- const updatedFilters = [...localFilters, presetFilter];
324
- dispatch(setLocalFilters(updatedFilters));
325
-
326
- setAppliedFilters(updatedFilters);
327
- },
328
- [dispatch, setAppliedFilters, localFilters]
329
- );
330
-
331
313
  return {
332
314
  localFilters,
333
315
  appliedFilters,
@@ -340,6 +322,5 @@ export const useProfileFilters = (): {
340
322
  removeFilter,
341
323
  updateFilter,
342
324
  resetFilters,
343
- applyPreset,
344
325
  };
345
326
  };
@@ -12,9 +12,9 @@
12
12
  // limitations under the License.
13
13
 
14
14
  import {useURLStateCustom, type ParamValueSetterCustom} from '@parca/components';
15
- import {type ProfileFilter} from '@parca/store';
16
15
 
17
16
  import {isPresetKey} from './filterPresets';
17
+ import {type ProfileFilter} from './useProfileFilters';
18
18
 
19
19
  // Compact encoding mappings
20
20
  const TYPE_MAP: Record<string, string> = {
@@ -154,7 +154,7 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
154
154
 
155
155
  return (
156
156
  <>
157
- <div className="flex w-full justify-between items-end">
157
+ <div className="flex w-full justify-between items-end gap-2">
158
158
  <div className="flex gap-2 items-end">
159
159
  {isGraphViz && (
160
160
  <>
@@ -88,7 +88,7 @@ const ViewSelector = ({profileSource}: Props): JSX.Element => {
88
88
  supportingText?: string;
89
89
  }): DropdownElement => {
90
90
  const title = (
91
- <span className="capitalize">
91
+ <span className="capitalize whitespace-nowrap">
92
92
  {typeof label === 'string' ? label.replaceAll('-', ' ') : label}
93
93
  </span>
94
94
  );