@parca/profile 0.16.374 → 0.16.376

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 (41) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/ProfileIcicleGraph/IcicleGraph/useColoredGraph.js +1 -1
  3. package/dist/ProfileIcicleGraph/IcicleGraph/utils.d.ts +1 -1
  4. package/dist/ProfileIcicleGraph/IcicleGraph/utils.js +1 -5
  5. package/dist/ProfileIcicleGraph/IcicleGraphArrow/ColorStackLegend.d.ts +4 -4
  6. package/dist/ProfileIcicleGraph/IcicleGraphArrow/ColorStackLegend.js +50 -13
  7. package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.d.ts +2 -1
  8. package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.js +3 -2
  9. package/dist/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.js +0 -1
  10. package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.d.ts +5 -1
  11. package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.js +48 -50
  12. package/dist/ProfileIcicleGraph/IcicleGraphArrow/useNodeColor.d.ts +1 -2
  13. package/dist/ProfileIcicleGraph/IcicleGraphArrow/useNodeColor.js +2 -7
  14. package/dist/ProfileIcicleGraph/IcicleGraphArrow/utils.js +0 -3
  15. package/dist/ProfileIcicleGraph/index.d.ts +3 -1
  16. package/dist/ProfileIcicleGraph/index.js +57 -36
  17. package/dist/ProfileSource.js +2 -0
  18. package/dist/ProfileView/index.d.ts +2 -0
  19. package/dist/ProfileView/index.js +1 -1
  20. package/dist/ProfileViewWithData.js +17 -10
  21. package/dist/Table/index.js +2 -9
  22. package/dist/useQuery.d.ts +1 -3
  23. package/dist/useQuery.js +21 -8
  24. package/package.json +7 -7
  25. package/src/ProfileIcicleGraph/IcicleGraph/useColoredGraph.ts +1 -1
  26. package/src/ProfileIcicleGraph/IcicleGraph/utils.ts +1 -7
  27. package/src/ProfileIcicleGraph/IcicleGraphArrow/ColorStackLegend.tsx +62 -16
  28. package/src/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.tsx +21 -1
  29. package/src/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.tsx +0 -1
  30. package/src/ProfileIcicleGraph/IcicleGraphArrow/index.tsx +66 -61
  31. package/src/ProfileIcicleGraph/IcicleGraphArrow/useNodeColor.ts +1 -8
  32. package/src/ProfileIcicleGraph/IcicleGraphArrow/utils.ts +0 -5
  33. package/src/ProfileIcicleGraph/index.tsx +142 -96
  34. package/src/ProfileSource.tsx +2 -0
  35. package/src/ProfileView/index.tsx +4 -0
  36. package/src/ProfileViewWithData.tsx +26 -10
  37. package/src/Table/index.tsx +1 -11
  38. package/src/useQuery.tsx +22 -11
  39. package/dist/ProfileIcicleGraph/ActionButtons/RuntimeFilterDropdown.d.ts +0 -9
  40. package/dist/ProfileIcicleGraph/ActionButtons/RuntimeFilterDropdown.js +0 -23
  41. package/src/ProfileIcicleGraph/ActionButtons/RuntimeFilterDropdown.tsx +0 -123
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Copyright 2022 The Parca Authors
3
3
  // Licensed under the Apache License, Version 2.0 (the "License");
4
4
  // you may not use this file except in compliance with the License.
@@ -11,19 +11,19 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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 { useCallback, useEffect, useMemo } from 'react';
14
+ import { useCallback, useEffect, useMemo, useState } from 'react';
15
15
  import { Icon } from '@iconify/react';
16
16
  import { AnimatePresence, motion } from 'framer-motion';
17
- import { Button, IcicleActionButtonPlaceholder, IcicleGraphSkeleton, IconButton, useParcaContext, useURLState, } from '@parca/components';
17
+ import { Button, IcicleGraphSkeleton, IconButton, useParcaContext, useURLState, } from '@parca/components';
18
18
  import { USER_PREFERENCES, useUserPreference } from '@parca/hooks';
19
- import { capitalizeOnlyFirstLetter, divide } from '@parca/utilities';
19
+ import { capitalizeOnlyFirstLetter, divide, selectQueryParam, } from '@parca/utilities';
20
20
  import { useProfileViewContext } from '../ProfileView/ProfileViewContext';
21
21
  import DiffLegend from '../components/DiffLegend';
22
22
  import GroupByDropdown from './ActionButtons/GroupByDropdown';
23
- import RuntimeFilterDropdown from './ActionButtons/RuntimeFilterDropdown';
24
23
  import SortBySelect from './ActionButtons/SortBySelect';
25
24
  import IcicleGraph from './IcicleGraph';
26
25
  import IcicleGraphArrow, { FIELD_FUNCTION_NAME } from './IcicleGraphArrow';
26
+ import ColorStackLegend from './IcicleGraphArrow/ColorStackLegend';
27
27
  const numberFormatter = new Intl.NumberFormat('en-US');
28
28
  const ErrorContent = ({ errorMessage }) => {
29
29
  return _jsx("div", { className: "flex justify-center p-10", children: errorMessage });
@@ -33,13 +33,20 @@ const ShowHideLegendButton = ({ navigateTo, isHalfScreen, }) => {
33
33
  param: 'color_stack_legend',
34
34
  navigateTo,
35
35
  });
36
+ const [binaryFrameFilter, setBinaryFrameFilter] = useURLState({
37
+ param: 'binary_frame_filter',
38
+ navigateTo,
39
+ });
36
40
  const { compareMode } = useProfileViewContext();
37
41
  const isColorStackLegendEnabled = colorStackLegend === 'true';
38
42
  const [colorProfileName] = useUserPreference(USER_PREFERENCES.FLAMEGRAPH_COLOR_PROFILE.key);
39
43
  const setColorStackLegend = useCallback((value) => {
40
44
  setStoreColorStackLegend(value);
41
45
  }, [setStoreColorStackLegend]);
42
- return (_jsx(_Fragment, { children: colorProfileName === 'default' || compareMode ? null : (_jsx(_Fragment, { children: isHalfScreen ? (_jsx(IconButton, { className: "rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 items-center flex border border-gray-200 dark:border-gray-600 dark:text-white justify-center !py-2 !px-3 cursor-pointer min-h-[38px]", icon: isColorStackLegendEnabled ? 'ph:eye-closed' : 'ph:eye', toolTipText: isColorStackLegendEnabled ? 'Hide legend' : 'Show legend', onClick: () => setColorStackLegend(isColorStackLegendEnabled ? 'false' : 'true'), id: "h-show-legend-button" })) : (_jsxs(Button, { className: "gap-2 w-max", variant: "neutral", onClick: () => setColorStackLegend(isColorStackLegendEnabled ? 'false' : 'true'), id: "h-show-legend-button", children: [isColorStackLegendEnabled ? 'Hide legend' : 'Show legend', _jsx(Icon, { icon: isColorStackLegendEnabled ? 'ph:eye-closed' : 'ph:eye', width: 20 })] })) })) }));
46
+ const resetLegend = () => {
47
+ setBinaryFrameFilter([]);
48
+ };
49
+ return (_jsx(_Fragment, { children: colorProfileName === 'default' || compareMode ? null : (_jsx(_Fragment, { children: isHalfScreen ? (_jsxs(_Fragment, { children: [_jsx(IconButton, { className: "rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 items-center flex border border-gray-200 dark:border-gray-600 dark:text-white justify-center !py-2 !px-3 cursor-pointer min-h-[38px]", icon: isColorStackLegendEnabled ? 'ph:eye-closed' : 'ph:eye', toolTipText: isColorStackLegendEnabled ? 'Hide legend' : 'Show legend', onClick: () => setColorStackLegend(isColorStackLegendEnabled ? 'false' : 'true'), id: "h-show-legend-button" }), binaryFrameFilter !== undefined && binaryFrameFilter.length > 0 && (_jsx(IconButton, { className: "rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 items-center flex border border-gray-200 dark:border-gray-600 dark:text-white justify-center !py-2 !px-3 cursor-pointer min-h-[38px]", icon: "system-uicons:reset", toolTipText: "Reset the legend selection", onClick: () => resetLegend(), id: "h-reset-legend-button" }))] })) : (_jsxs(_Fragment, { children: [_jsxs(Button, { className: "gap-2 w-max", variant: "neutral", onClick: () => setColorStackLegend(isColorStackLegendEnabled ? 'false' : 'true'), id: "h-show-legend-button", children: [isColorStackLegendEnabled ? 'Hide legend' : 'Show legend', _jsx(Icon, { icon: isColorStackLegendEnabled ? 'ph:eye-closed' : 'ph:eye', width: 20 })] }), binaryFrameFilter !== undefined && binaryFrameFilter.length > 0 && (_jsxs(Button, { className: "gap-2 w-max", variant: "neutral", onClick: () => resetLegend(), id: "h-reset-legend-button", children: ["Reset Legend", _jsx(Icon, { icon: "system-uicons:reset", width: 20 })] }))] })) })) }));
43
50
  };
44
51
  const GroupAndSortActionButtons = ({ navigateTo }) => {
45
52
  const [storeSortBy = FIELD_FUNCTION_NAME, setStoreSortBy] = useURLState({
@@ -68,23 +75,13 @@ const GroupAndSortActionButtons = ({ navigateTo }) => {
68
75
  ? setGroupBy(groupBy.filter(v => v !== key)) // remove
69
76
  : setGroupBy([...groupBy, key]); // add
70
77
  }, [groupBy, setGroupBy]);
71
- const [showRuntimeRubyStr, setShowRuntimeRuby] = useURLState({
72
- param: 'show_runtime_ruby',
73
- navigateTo,
74
- });
75
- const [showRuntimePythonStr, setShowRuntimePython] = useURLState({
76
- param: 'show_runtime_python',
77
- navigateTo,
78
- });
79
- const [showInterpretedOnlyStr, setShowInterpretedOnly] = useURLState({
80
- param: 'show_interpreted_only',
81
- navigateTo,
82
- });
83
- return (_jsxs(_Fragment, { children: [_jsx(GroupByDropdown, { groupBy: groupBy, toggleGroupBy: toggleGroupBy }), _jsx(SortBySelect, { compareMode: compareMode, sortBy: storeSortBy, setSortBy: setStoreSortBy }), _jsx(RuntimeFilterDropdown, { showRuntimeRuby: showRuntimeRubyStr === 'true', toggleShowRuntimeRuby: () => setShowRuntimeRuby(showRuntimeRubyStr === 'true' ? 'false' : 'true'), showRuntimePython: showRuntimePythonStr === 'true', toggleShowRuntimePython: () => setShowRuntimePython(showRuntimePythonStr === 'true' ? 'false' : 'true'), showInterpretedOnly: showInterpretedOnlyStr === 'true', toggleShowInterpretedOnly: () => setShowInterpretedOnly(showInterpretedOnlyStr === 'true' ? 'false' : 'true') })] }));
78
+ return (_jsxs(_Fragment, { children: [_jsx(GroupByDropdown, { groupBy: groupBy, toggleGroupBy: toggleGroupBy }), _jsx(SortBySelect, { compareMode: compareMode, sortBy: storeSortBy, setSortBy: setStoreSortBy })] }));
84
79
  };
85
- const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, total, filtered, curPath, setNewCurPath, profileType, navigateTo, loading, setActionButtons, error, width, isHalfScreen, }) {
80
+ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, total, filtered, curPath, setNewCurPath, profileType, navigateTo, loading, setActionButtons, error, width, isHalfScreen, mappings, mappingsLoading, }) {
86
81
  const { onError, authenticationErrorMessage, isDarkMode } = useParcaContext();
87
82
  const { compareMode } = useProfileViewContext();
83
+ const [isLoading, setIsLoading] = useState(true);
84
+ const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
88
85
  const [storeSortBy = FIELD_FUNCTION_NAME] = useURLState({
89
86
  param: 'sort_by',
90
87
  navigateTo,
@@ -113,14 +110,7 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, to
113
110
  ];
114
111
  }, [graph, arrow, filtered, total]);
115
112
  useEffect(() => {
116
- if (loading && setActionButtons !== undefined) {
117
- setActionButtons(_jsx(IcicleActionButtonPlaceholder, { isHalfScreen: isHalfScreen }));
118
- return;
119
- }
120
- if (setActionButtons === undefined) {
121
- return;
122
- }
123
- setActionButtons(_jsx("div", { className: "flex w-full justify-end gap-2 pb-2", children: _jsxs("div", { className: "ml-2 flex w-full flex-col items-start justify-between gap-2 md:flex-row md:items-end", children: [arrow !== undefined && _jsx(GroupAndSortActionButtons, { navigateTo: navigateTo }), isHalfScreen ? (_jsx(IconButton, { icon: isInvert ? 'ph:sort-ascending' : 'ph:sort-descending', toolTipText: isInvert ? 'Original Call Stack' : 'Invert Call Stack', onClick: () => setInvertStack(isInvert ? '' : 'true'), className: "rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 items-center flex border border-gray-200 dark:border-gray-600 dark:text-white justify-center py-2 px-3 cursor-pointer min-h-[38px]" })) : (_jsxs(Button, { variant: "neutral", className: "gap-2 w-max", onClick: () => setInvertStack(isInvert ? '' : 'true'), children: [isInvert ? 'Original Call Stack' : 'Invert Call Stack', _jsx(Icon, { icon: isInvert ? 'ph:sort-ascending' : 'ph:sort-descending', width: 20 })] })), _jsx(ShowHideLegendButton, { isHalfScreen: isHalfScreen, navigateTo: navigateTo }), isHalfScreen ? (_jsx(IconButton, { icon: "system-uicons:reset", disabled: curPath.length === 0, toolTipText: "Reset View", onClick: () => setNewCurPath([]), className: "rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 items-center flex border border-gray-200 dark:border-gray-600 dark:text-white justify-center py-2 px-3 cursor-pointer min-h-[38px]" })) : (_jsxs(Button, { variant: "neutral", className: "gap-2 w-max", onClick: () => setNewCurPath([]), disabled: curPath.length === 0, children: ["Reset View", _jsx(Icon, { icon: "system-uicons:reset", width: 20 })] }))] }) }));
113
+ setActionButtons?.(_jsx("div", { className: "flex w-full justify-end gap-2 pb-2", children: _jsxs("div", { className: "ml-2 flex w-full flex-col items-start justify-between gap-2 md:flex-row md:items-end", children: [_jsx(GroupAndSortActionButtons, { navigateTo: navigateTo }), isHalfScreen ? (_jsx(IconButton, { icon: isInvert ? 'ph:sort-ascending' : 'ph:sort-descending', toolTipText: isInvert ? 'Original Call Stack' : 'Invert Call Stack', onClick: () => setInvertStack(isInvert ? '' : 'true'), className: "rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 items-center flex border border-gray-200 dark:border-gray-600 dark:text-white justify-center py-2 px-3 cursor-pointer min-h-[38px]" })) : (_jsxs(Button, { variant: "neutral", className: "gap-2 w-max", onClick: () => setInvertStack(isInvert ? '' : 'true'), children: [isInvert ? 'Original Call Stack' : 'Invert Call Stack', _jsx(Icon, { icon: isInvert ? 'ph:sort-ascending' : 'ph:sort-descending', width: 20 })] })), _jsx(ShowHideLegendButton, { isHalfScreen: isHalfScreen, navigateTo: navigateTo }), isHalfScreen ? (_jsx(IconButton, { icon: "system-uicons:reset", disabled: curPath.length === 0, toolTipText: "Reset View", onClick: () => setNewCurPath([]), className: "rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 items-center flex border border-gray-200 dark:border-gray-600 dark:text-white justify-center py-2 px-3 cursor-pointer min-h-[38px]" })) : (_jsxs(Button, { variant: "neutral", className: "gap-2 w-max", onClick: () => setNewCurPath([]), disabled: curPath.length === 0, children: ["Reset View", _jsx(Icon, { icon: "system-uicons:reset", width: 20 })] }))] }) }));
124
114
  }, [
125
115
  navigateTo,
126
116
  isInvert,
@@ -131,10 +121,16 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, to
131
121
  setActionButtons,
132
122
  loading,
133
123
  isHalfScreen,
124
+ isLoading,
134
125
  ]);
135
- if (loading) {
136
- return (_jsx("div", { className: "h-auto overflow-clip", children: _jsx(IcicleGraphSkeleton, { isHalfScreen: isHalfScreen, isDarkMode: isDarkMode }) }));
137
- }
126
+ useEffect(() => {
127
+ if (!loading && (arrow !== undefined || graph !== undefined)) {
128
+ setIsLoading(false);
129
+ }
130
+ else {
131
+ setIsLoading(true);
132
+ }
133
+ }, [loading, arrow, graph]);
138
134
  if (error != null) {
139
135
  onError?.(error);
140
136
  if (authenticationErrorMessage !== undefined && error.code === 'UNAUTHENTICATED') {
@@ -142,13 +138,38 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, to
142
138
  }
143
139
  return _jsx(ErrorContent, { errorMessage: capitalizeOnlyFirstLetter(error.message) });
144
140
  }
145
- if (graph === undefined && arrow === undefined)
146
- return _jsx("div", { className: "mx-auto text-center", children: "No data..." });
147
- if (total === 0n && !loading)
148
- return _jsx("div", { className: "mx-auto text-center", children: "Profile has no samples" });
141
+ // eslint-disable-next-line react-hooks/rules-of-hooks
142
+ const icicleGraph = useMemo(() => {
143
+ if (isLoading) {
144
+ return (_jsx("div", { className: "h-auto overflow-clip", children: _jsx(IcicleGraphSkeleton, { isHalfScreen: isHalfScreen, isDarkMode: isDarkMode }) }));
145
+ }
146
+ if (graph === undefined && arrow === undefined)
147
+ return _jsx("div", { className: "mx-auto text-center", children: "No data..." });
148
+ if (total === 0n && !loading)
149
+ return _jsx("div", { className: "mx-auto text-center", children: "Profile has no samples" });
150
+ if (graph !== undefined)
151
+ return (_jsx(IcicleGraph, { width: width, graph: graph, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, profileType: profileType, navigateTo: navigateTo }));
152
+ if (arrow !== undefined)
153
+ return (_jsx(IcicleGraphArrow, { width: width, arrow: arrow, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, profileType: profileType, navigateTo: navigateTo, sortBy: storeSortBy, flamegraphLoading: isLoading, isHalfScreen: isHalfScreen }));
154
+ }, [
155
+ isLoading,
156
+ graph,
157
+ arrow,
158
+ total,
159
+ filtered,
160
+ curPath,
161
+ setNewCurPath,
162
+ profileType,
163
+ navigateTo,
164
+ width,
165
+ storeSortBy,
166
+ isHalfScreen,
167
+ isDarkMode,
168
+ loading,
169
+ ]);
149
170
  if (isTrimmed) {
150
171
  console.info(`Trimmed ${trimmedFormatted} (${trimmedPercentage}%) too small values.`);
151
172
  }
152
- return (_jsx(AnimatePresence, { children: _jsxs(motion.div, { className: "relative h-full w-full", initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.5 }, children: [compareMode ? _jsx(DiffLegend, {}) : null, _jsxs("div", { className: "min-h-48", id: "h-icicle-graph", children: [graph !== undefined && (_jsx(IcicleGraph, { width: width, graph: graph, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, profileType: profileType, navigateTo: navigateTo })), arrow !== undefined && (_jsx(IcicleGraphArrow, { width: width, arrow: arrow, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, profileType: profileType, navigateTo: navigateTo, sortBy: storeSortBy }))] }), _jsxs("p", { className: "my-2 text-xs", children: ["Showing ", totalFormatted, ' ', isFiltered ? (_jsxs("span", { children: ["(", filteredPercentage, "%) filtered of ", totalUnfilteredFormatted, ' '] })) : (_jsx(_Fragment, {})), "values.", ' '] })] }, "icicle-graph-loaded") }));
173
+ return (_jsx(AnimatePresence, { children: _jsxs(motion.div, { className: "relative h-full w-full", initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.5 }, children: [compareMode ? _jsx(DiffLegend, {}) : null, isColorStackLegendEnabled && (_jsx(ColorStackLegend, { navigateTo: navigateTo, compareMode: compareMode, mappings: mappings, mappingsLoading: mappingsLoading })), _jsx("div", { className: "min-h-48", id: "h-icicle-graph", children: _jsx(_Fragment, { children: icicleGraph }) }), _jsxs("p", { className: "my-2 text-xs", children: ["Showing ", totalFormatted, ' ', isFiltered ? (_jsxs("span", { children: ["(", filteredPercentage, "%) filtered of ", totalUnfilteredFormatted, ' '] })) : (_jsx(_Fragment, {})), "values.", ' '] })] }, "icicle-graph-loaded") }));
153
174
  };
154
175
  export default ProfileIcicleGraph;
@@ -108,6 +108,7 @@ export class ProfileDiffSource {
108
108
  reportType: QueryRequest_ReportType.FLAMEGRAPH_UNSPECIFIED,
109
109
  mode: QueryRequest_Mode.DIFF,
110
110
  filterQuery: this.filterQuery,
111
+ filter: [],
111
112
  };
112
113
  }
113
114
  ProfileType() {
@@ -158,6 +159,7 @@ export class MergedProfileSource {
158
159
  reportType: QueryRequest_ReportType.FLAMEGRAPH_UNSPECIFIED,
159
160
  mode: QueryRequest_Mode.MERGE,
160
161
  filterQuery: this.filterQuery,
162
+ filter: [],
161
163
  };
162
164
  }
163
165
  ProfileType() {
@@ -11,6 +11,8 @@ export interface FlamegraphData {
11
11
  total?: bigint;
12
12
  filtered?: bigint;
13
13
  error?: any;
14
+ mappings?: string[];
15
+ mappingsLoading: boolean;
14
16
  }
15
17
  export interface TopTableData {
16
18
  loading: boolean;
@@ -111,7 +111,7 @@ export const ProfileView = ({ total, filtered, flamegraphData, topTableData, cal
111
111
  ? isHalfScreen
112
112
  ? (dimensions.width - 40) / 2
113
113
  : dimensions.width - 16
114
- : 0 }) }));
114
+ : 0, mappings: flamegraphData.mappings, mappingsLoading: flamegraphData.mappingsLoading }) }));
115
115
  }
116
116
  case 'callgraph': {
117
117
  return callgraphData?.data !== undefined &&
@@ -25,14 +25,12 @@ export const ProfileViewWithData = ({ queryClient, profileSource, navigateTo, })
25
25
  const [sourceBuildID] = useURLState({ param: 'source_buildid', navigateTo });
26
26
  const [sourceFilename] = useURLState({ param: 'source_filename', navigateTo });
27
27
  const [groupBy = [FIELD_FUNCTION_NAME]] = useURLState({ param: 'group_by', navigateTo });
28
- const [showRuntimeRubyStr] = useURLState({ param: 'show_runtime_ruby', navigateTo });
29
- const showRuntimeRuby = showRuntimeRubyStr === 'true';
30
- const [showRuntimePythonStr] = useURLState({ param: 'show_runtime_python', navigateTo });
31
- const showRuntimePython = showRuntimePythonStr === 'true';
32
- const [showInterpretedOnlyStr] = useURLState({ param: 'show_interpreted_only', navigateTo });
33
- const showInterpretedOnly = showInterpretedOnlyStr === 'true';
34
28
  const [invertStack] = useURLState({ param: 'invert_call_stack', navigateTo });
35
29
  const invertCallStack = invertStack === 'true';
30
+ const [binaryFrameFilterStr] = useURLState({ param: 'binary_frame_filter', navigateTo });
31
+ const binaryFrameFilter = typeof binaryFrameFilterStr === 'string'
32
+ ? binaryFrameFilterStr.split(',')
33
+ : binaryFrameFilterStr;
36
34
  const [pprofDownloading, setPprofDownloading] = useState(false);
37
35
  const nodeTrimThreshold = useMemo(() => {
38
36
  let width =
@@ -48,10 +46,15 @@ export const ProfileViewWithData = ({ queryClient, profileSource, navigateTo, })
48
46
  skip: !dashboardItems.includes('icicle'),
49
47
  nodeTrimThreshold,
50
48
  groupBy: groupByParam,
51
- showRuntimeRuby,
52
- showRuntimePython,
53
- showInterpretedOnly,
54
49
  invertCallStack,
50
+ binaryFrameFilter,
51
+ });
52
+ const { isLoading: profilemetadataLoading, response: profilemetadataResponse } = useQuery(queryClient, profileSource, QueryRequest_ReportType.PROFILE_METADATA, {
53
+ skip: !dashboardItems.includes('icicle'),
54
+ nodeTrimThreshold,
55
+ groupBy: groupByParam,
56
+ invertCallStack,
57
+ binaryFrameFilter: undefined,
55
58
  });
56
59
  const { perf } = useParcaContext();
57
60
  const { isLoading: tableLoading, response: tableResponse, error: tableError, } = useQuery(queryClient, profileSource, QueryRequest_ReportType.TABLE_ARROW, {
@@ -126,7 +129,7 @@ export const ProfileViewWithData = ({ queryClient, profileSource, navigateTo, })
126
129
  filtered = BigInt(sourceResponse.filtered);
127
130
  }
128
131
  return (_jsx(ProfileView, { total: total, filtered: filtered, flamegraphData: {
129
- loading: flamegraphLoading,
132
+ loading: flamegraphLoading && profilemetadataLoading,
130
133
  data: flamegraphResponse?.report.oneofKind === 'flamegraph'
131
134
  ? flamegraphResponse?.report?.flamegraph
132
135
  : undefined,
@@ -136,6 +139,10 @@ export const ProfileViewWithData = ({ queryClient, profileSource, navigateTo, })
136
139
  total: BigInt(flamegraphResponse?.total ?? '0'),
137
140
  filtered: BigInt(flamegraphResponse?.filtered ?? '0'),
138
141
  error: flamegraphError,
142
+ mappings: profilemetadataResponse?.report.oneofKind === 'profileMetadata'
143
+ ? profilemetadataResponse?.report?.profileMetadata?.mappingFiles
144
+ : undefined,
145
+ mappingsLoading: profilemetadataLoading,
139
146
  }, topTableData: {
140
147
  loading: tableLoading,
141
148
  arrow: tableResponse?.report.oneofKind === 'tableArrow'
@@ -14,7 +14,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
14
14
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
15
15
  import { tableFromIPC } from 'apache-arrow';
16
16
  import { AnimatePresence, motion } from 'framer-motion';
17
- import { Button, TableActionButtonPlaceholder, Table as TableComponent, TableSkeleton, useParcaContext, useURLState, } from '@parca/components';
17
+ import { Button, Table as TableComponent, TableSkeleton, useParcaContext, useURLState, } from '@parca/components';
18
18
  import { getLastItem, isSearchMatch, parseParams, valueFormatter, } from '@parca/utilities';
19
19
  import { useProfileViewContext } from '../ProfileView/ProfileViewContext';
20
20
  import { hexifyAddress } from '../utils';
@@ -214,14 +214,7 @@ export const Table = React.memo(function Table({ data, total, filtered, profileT
214
214
  }
215
215
  }, [navigateTo, router, filterByFunctionInput]);
216
216
  useEffect(() => {
217
- if (loading && setActionButtons !== undefined) {
218
- setActionButtons(_jsx(TableActionButtonPlaceholder, {}));
219
- return;
220
- }
221
- if (setActionButtons === undefined) {
222
- return;
223
- }
224
- setActionButtons(_jsxs(_Fragment, { children: [_jsx(ColumnsVisibility, { columns: columns, visibility: columnVisibility, setVisibility: (id, visible) => {
217
+ setActionButtons?.(_jsxs(_Fragment, { children: [_jsx(ColumnsVisibility, { columns: columns, visibility: columnVisibility, setVisibility: (id, visible) => {
225
218
  setColumnVisibility({ ...columnVisibility, [id]: visible });
226
219
  } }), dashboardItems.length > 1 && (_jsx(Button, { color: "neutral", onClick: clearSelection, className: "w-auto", variant: "neutral", disabled: currentSearchString === undefined || currentSearchString.length === 0, children: "Clear selection" }))] }));
227
220
  }, [
@@ -13,10 +13,8 @@ interface UseQueryOptions {
13
13
  sourceBuildID?: string;
14
14
  sourceFilename?: string;
15
15
  sourceOnly?: boolean;
16
- showRuntimeRuby?: boolean;
17
- showRuntimePython?: boolean;
18
- showInterpretedOnly?: boolean;
19
16
  invertCallStack?: boolean;
17
+ binaryFrameFilter?: string[];
20
18
  }
21
19
  export declare const useQuery: (client: QueryServiceClient, profileSource: ProfileSource, reportType: QueryRequest_ReportType, options?: UseQueryOptions) => IQueryResult;
22
20
  export {};
package/dist/useQuery.js CHANGED
@@ -25,10 +25,8 @@ export const useQuery = (client, profileSource, reportType, options) => {
25
25
  options?.sourceBuildID,
26
26
  options?.sourceOnly,
27
27
  options?.sourceOnly === true ? '' : options?.sourceFilename,
28
- options?.showRuntimeRuby ?? false,
29
- options?.showRuntimePython ?? false,
30
- options?.showInterpretedOnly ?? false,
31
28
  options?.invertCallStack ?? false,
29
+ options?.binaryFrameFilter ?? '',
32
30
  ],
33
31
  queryFn: async () => {
34
32
  const req = profileSource.QueryRequest();
@@ -44,12 +42,27 @@ export const useQuery = (client, profileSource, reportType, options) => {
44
42
  sourceOnly: options?.sourceOnly ?? false,
45
43
  };
46
44
  }
47
- req.runtimeFilter = {
48
- showRuby: options?.showRuntimeRuby ?? false,
49
- showPython: options?.showRuntimePython ?? false,
50
- showInterpretedOnly: options?.showInterpretedOnly ?? false,
51
- };
52
45
  req.invertCallStack = options?.invertCallStack ?? false;
46
+ if (options?.binaryFrameFilter !== undefined && options?.binaryFrameFilter.length > 0) {
47
+ req.filter = [
48
+ {
49
+ filter: {
50
+ oneofKind: 'frameFilter',
51
+ frameFilter: {
52
+ filter: {
53
+ oneofKind: 'binaryFrameFilter',
54
+ binaryFrameFilter: {
55
+ includeBinaries: options?.binaryFrameFilter ?? [],
56
+ },
57
+ },
58
+ },
59
+ },
60
+ },
61
+ ];
62
+ }
63
+ else {
64
+ req.filter = [];
65
+ }
53
66
  try {
54
67
  const { response } = await client.query(req, { meta: metadata });
55
68
  return response;
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.16.374",
3
+ "version": "0.16.376",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@headlessui/react": "^1.7.19",
7
7
  "@iconify/react": "^4.0.0",
8
- "@parca/client": "^0.16.114",
9
- "@parca/components": "^0.16.277",
8
+ "@parca/client": "^0.16.115",
9
+ "@parca/components": "^0.16.278",
10
10
  "@parca/dynamicsize": "^0.16.65",
11
- "@parca/hooks": "^0.0.56",
11
+ "@parca/hooks": "^0.0.57",
12
12
  "@parca/icons": "^0.16.69",
13
13
  "@parca/parser": "^0.16.74",
14
- "@parca/store": "^0.16.145",
15
- "@parca/utilities": "^0.0.73",
14
+ "@parca/store": "^0.16.146",
15
+ "@parca/utilities": "^0.0.74",
16
16
  "@popperjs/core": "^2.11.8",
17
17
  "@protobuf-ts/runtime-rpc": "^2.5.0",
18
18
  "@tanstack/react-query": "^4.0.5",
@@ -71,5 +71,5 @@
71
71
  "access": "public",
72
72
  "registry": "https://registry.npmjs.org/"
73
73
  },
74
- "gitHead": "acf7298e22788f6f2c91cbdc36e0c90007526cf4"
74
+ "gitHead": "6f30d0d2d958ea4a32f58671bfc97bf72eb232d7"
75
75
  }
@@ -71,7 +71,7 @@ const colorNodes = (
71
71
  features
72
72
  );
73
73
  }
74
- const feature = extractFeature(node, mappings, locations, strings, functions);
74
+ const feature = extractFeature(node, mappings, locations, strings);
75
75
  coloredNode.feature = feature.name;
76
76
  features[feature.name] = feature.type;
77
77
  return coloredNode;
@@ -91,14 +91,8 @@ export const extractFeature = (
91
91
  data: FlamegraphNode,
92
92
  mappings: Mapping[],
93
93
  locations: Location[],
94
- strings: string[],
95
- functions: ParcaFunction[]
94
+ strings: string[]
96
95
  ): Feature => {
97
- const name = nodeLabel(data, strings, mappings, locations, functions, false).trim();
98
- if (name.startsWith('runtime') || name === 'root') {
99
- return {name: 'runtime', type: FEATURE_TYPES.Runtime};
100
- }
101
-
102
96
  const binaryName = getBinaryName(data, mappings, locations, strings);
103
97
  if (binaryName != null) {
104
98
  return {name: binaryName, type: FEATURE_TYPES.Binary};
@@ -17,27 +17,59 @@ import {Icon} from '@iconify/react';
17
17
  import cx from 'classnames';
18
18
 
19
19
  import {useURLState} from '@parca/components';
20
- import {USER_PREFERENCES, useUserPreference} from '@parca/hooks';
21
- import {EVERYTHING_ELSE} from '@parca/store';
22
- import type {NavigateFunction} from '@parca/utilities';
20
+ import {USER_PREFERENCES, useCurrentColorProfile, useUserPreference} from '@parca/hooks';
21
+ import {EVERYTHING_ELSE, selectDarkMode, useAppSelector} from '@parca/store';
22
+ import {getLastItem, type NavigateFunction} from '@parca/utilities';
23
23
 
24
- import {mappingColors} from './IcicleGraphNodes';
24
+ import {getMappingColors} from '.';
25
25
 
26
26
  interface Props {
27
- mappingColors: mappingColors;
27
+ mappings?: string[];
28
+ mappingsLoading?: boolean;
28
29
  navigateTo?: NavigateFunction;
29
30
  compareMode?: boolean;
30
31
  }
31
32
 
32
33
  const ColorStackLegend = ({
33
- mappingColors,
34
+ mappings,
34
35
  navigateTo,
35
36
  compareMode = false,
37
+ mappingsLoading,
36
38
  }: Props): React.JSX.Element => {
39
+ const isDarkMode = useAppSelector(selectDarkMode);
40
+ const currentColorProfile = useCurrentColorProfile();
37
41
  const [colorProfileName] = useUserPreference<string>(
38
42
  USER_PREFERENCES.FLAMEGRAPH_COLOR_PROFILE.key
39
43
  );
40
- const [currentSearchString, setSearchString] = useURLState({param: 'search_string', navigateTo});
44
+ const [currentSearchString, setSearchString] = useURLState({
45
+ param: 'binary_frame_filter',
46
+ navigateTo,
47
+ });
48
+
49
+ const mappingsList = useMemo(() => {
50
+ if (mappings === undefined) {
51
+ return [];
52
+ }
53
+ const list =
54
+ mappings
55
+ ?.map(mapping => {
56
+ return getLastItem(mapping) as string;
57
+ })
58
+ .flat() ?? [];
59
+
60
+ // We add a EVERYTHING ELSE mapping to the list.
61
+ list.push('');
62
+
63
+ // We sort the mappings alphabetically to make sure that the order is always the same.
64
+ list.sort((a, b) => a.localeCompare(b));
65
+
66
+ return list;
67
+ }, [mappings]);
68
+
69
+ const mappingColors = useMemo(() => {
70
+ const colors = getMappingColors(mappingsList, isDarkMode, currentColorProfile);
71
+ return colors;
72
+ }, [isDarkMode, mappingsList, currentColorProfile]);
41
73
 
42
74
  const stackColorArray = useMemo(() => {
43
75
  return Object.entries(mappingColors).sort(([featureA], [featureB]) => {
@@ -51,7 +83,7 @@ const ColorStackLegend = ({
51
83
  });
52
84
  }, [mappingColors]);
53
85
 
54
- if (mappingColors === undefined) {
86
+ if (mappingColors === undefined && mappingsLoading === false) {
55
87
  return <></>;
56
88
  }
57
89
 
@@ -67,7 +99,8 @@ const ColorStackLegend = ({
67
99
  <div className="my-4 flex w-full flex-wrap justify-start">
68
100
  {stackColorArray.map(([feature, color]) => {
69
101
  const filteringAllowed = feature !== EVERYTHING_ELSE;
70
- const isHighlighted = currentSearchString === feature;
102
+ const isHighlighted =
103
+ currentSearchString !== undefined ? currentSearchString.includes(feature) : false;
71
104
  return (
72
105
  <div
73
106
  key={feature}
@@ -79,14 +112,19 @@ const ColorStackLegend = ({
79
112
  }
80
113
  )}
81
114
  onClick={() => {
82
- if (!filteringAllowed) {
115
+ if (!filteringAllowed || isHighlighted) {
83
116
  return;
84
117
  }
85
- if (isHighlighted) {
86
- setSearchString('');
87
- return;
88
- }
89
- setSearchString(feature);
118
+
119
+ // Check if the current search string is defined and an array
120
+ const updatedSearchString = Array.isArray(currentSearchString)
121
+ ? [...currentSearchString, feature] // If array, append the feature
122
+ : // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
123
+ currentSearchString // If not array, preserve current value
124
+ ? currentSearchString.split(',') // If string, split by commas
125
+ : [feature]; // If undefined, initialize array with feature
126
+
127
+ setSearchString(updatedSearchString);
90
128
  }}
91
129
  >
92
130
  <div className="flex w-11/12 items-center justify-start">
@@ -102,7 +140,15 @@ const ColorStackLegend = ({
102
140
  <Icon
103
141
  icon="radix-icons:cross-circled"
104
142
  onClick={e => {
105
- setSearchString('');
143
+ let searchString: string[] = [];
144
+ if (typeof currentSearchString === 'string') {
145
+ searchString.push(currentSearchString);
146
+ } else {
147
+ searchString = currentSearchString;
148
+ }
149
+
150
+ // remove the current feature from the search string array of strings
151
+ setSearchString(searchString.filter((f: string) => f !== feature));
106
152
  e.stopPropagation();
107
153
  }}
108
154
  />
@@ -19,7 +19,7 @@ import {Tooltip} from 'react-tooltip';
19
19
  import {useParcaContext} from '@parca/components';
20
20
  import {USER_PREFERENCES, useUserPreference} from '@parca/hooks';
21
21
  import {ProfileType} from '@parca/parser';
22
- import {type NavigateFunction} from '@parca/utilities';
22
+ import {getLastItem, type NavigateFunction} from '@parca/utilities';
23
23
 
24
24
  import {useGraphTooltip} from '../../GraphTooltipArrow/useGraphTooltip';
25
25
  import {useGraphTooltipMetaInfo} from '../../GraphTooltipArrow/useGraphTooltipMetaInfo';
@@ -38,6 +38,7 @@ interface ContextMenuProps {
38
38
  curPath: string[];
39
39
  setCurPath: (path: string[]) => void;
40
40
  hideMenu: () => void;
41
+ hideBinary: (binaryToRemove: string) => void;
41
42
  }
42
43
 
43
44
  const ContextMenu = ({
@@ -53,6 +54,7 @@ const ContextMenu = ({
53
54
  setCurPath,
54
55
  hideMenu,
55
56
  profileType,
57
+ hideBinary,
56
58
  }: ContextMenuProps): JSX.Element => {
57
59
  const {isDarkMode} = useParcaContext();
58
60
  const {enableSourcesView} = useParcaContext();
@@ -157,6 +159,24 @@ const ContextMenu = ({
157
159
  <div>Reset view</div>
158
160
  </div>
159
161
  </Item>
162
+ <Item
163
+ id="hide-binary"
164
+ onClick={() => hideBinary(getLastItem(mappingFile) as string)}
165
+ disabled={mappingFile === null || mappingFile === ''}
166
+ >
167
+ <div
168
+ data-tooltip-id="hide-binary-help"
169
+ data-tooltip-content="Hide all frames for this binary"
170
+ >
171
+ <div className="flex w-full items-center gap-2">
172
+ <Icon icon="bx:bxs-hide" />
173
+ <div>
174
+ Hide Binary {mappingFile !== null && `(${getLastItem(mappingFile) as string})`}
175
+ </div>
176
+ </div>
177
+ </div>
178
+ <Tooltip place="left" id="hide-binary-help" />
179
+ </Item>
160
180
  <Submenu
161
181
  label={
162
182
  <div className="flex w-full items-center gap-2">
@@ -362,7 +362,6 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
362
362
  diffPerSecond,
363
363
  mappingColors,
364
364
  mappingFile,
365
- functionName,
366
365
  });
367
366
  const name = useMemo(() => {
368
367
  return isRoot ? 'root' : nodeLabel(table, row, level, binaries.length > 1);