@parca/profile 0.16.375 → 0.16.377

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.16.377](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.376...@parca/profile@0.16.377) (2024-05-30)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
10
+ ## 0.16.376 (2024-05-30)
11
+
12
+ **Note:** Version bump only for package @parca/profile
13
+
6
14
  ## [0.16.375](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.374...@parca/profile@0.16.375) (2024-05-23)
7
15
 
8
16
  **Note:** Version bump only for package @parca/profile
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
- import type { NavigateFunction } from '@parca/utilities';
3
- import { mappingColors } from './IcicleGraphNodes';
2
+ import { type NavigateFunction } from '@parca/utilities';
4
3
  interface Props {
5
- mappingColors: mappingColors;
4
+ mappings?: string[];
5
+ loading?: boolean;
6
6
  navigateTo?: NavigateFunction;
7
7
  compareMode?: boolean;
8
8
  }
9
- declare const ColorStackLegend: ({ mappingColors, navigateTo, compareMode, }: Props) => React.JSX.Element;
9
+ declare const ColorStackLegend: ({ mappings, navigateTo, compareMode, loading, }: Props) => React.JSX.Element;
10
10
  export default ColorStackLegend;
@@ -15,14 +15,23 @@ import { useMemo } from 'react';
15
15
  import { Icon } from '@iconify/react';
16
16
  import cx from 'classnames';
17
17
  import { useURLState } from '@parca/components';
18
- import { USER_PREFERENCES, useUserPreference } from '@parca/hooks';
19
- import { EVERYTHING_ELSE } from '@parca/store';
20
- const ColorStackLegend = ({ mappingColors, navigateTo, compareMode = false, }) => {
18
+ import { USER_PREFERENCES, useCurrentColorProfile, useUserPreference } from '@parca/hooks';
19
+ import { EVERYTHING_ELSE, selectDarkMode, useAppSelector } from '@parca/store';
20
+ import { getMappingColors } from '.';
21
+ import useMappingList from './useMappingList';
22
+ const ColorStackLegend = ({ mappings, navigateTo, compareMode = false, loading, }) => {
23
+ const isDarkMode = useAppSelector(selectDarkMode);
24
+ const currentColorProfile = useCurrentColorProfile();
21
25
  const [colorProfileName] = useUserPreference(USER_PREFERENCES.FLAMEGRAPH_COLOR_PROFILE.key);
22
26
  const [currentSearchString, setSearchString] = useURLState({
23
27
  param: 'binary_frame_filter',
24
28
  navigateTo,
25
29
  });
30
+ const mappingsList = useMappingList(mappings);
31
+ const mappingColors = useMemo(() => {
32
+ const colors = getMappingColors(mappingsList, isDarkMode, currentColorProfile);
33
+ return colors;
34
+ }, [isDarkMode, mappingsList, currentColorProfile]);
26
35
  const stackColorArray = useMemo(() => {
27
36
  return Object.entries(mappingColors).sort(([featureA], [featureB]) => {
28
37
  if (featureA === EVERYTHING_ELSE) {
@@ -34,7 +43,7 @@ const ColorStackLegend = ({ mappingColors, navigateTo, compareMode = false, }) =
34
43
  return featureA?.localeCompare(featureB ?? '') ?? 0;
35
44
  });
36
45
  }, [mappingColors]);
37
- if (mappingColors === undefined) {
46
+ if (stackColorArray.length === 0 && loading === false) {
38
47
  return _jsx(_Fragment, {});
39
48
  }
40
49
  if (Object.entries(mappingColors).length === 0) {
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
2
  import { FlamegraphArrow } from '@parca/client';
3
3
  import { ProfileType } from '@parca/parser';
4
- import { type NavigateFunction } from '@parca/utilities';
4
+ import { type ColorConfig, type NavigateFunction } from '@parca/utilities';
5
+ import { mappingColors } from './IcicleGraphNodes';
5
6
  export declare const FIELD_LABELS_ONLY = "labels_only";
6
7
  export declare const FIELD_MAPPING_FILE = "mapping_file";
7
8
  export declare const FIELD_MAPPING_BUILD_ID = "mapping_build_id";
@@ -28,7 +29,10 @@ interface IcicleGraphArrowProps {
28
29
  setCurPath: (path: string[]) => void;
29
30
  navigateTo?: NavigateFunction;
30
31
  sortBy: string;
31
- mappings?: string[];
32
+ flamegraphLoading: boolean;
33
+ isHalfScreen: boolean;
34
+ mappingsListFromMetadata: string[];
32
35
  }
36
+ export declare const getMappingColors: (mappingsList: string[], isDarkMode: boolean, currentColorProfile: ColorConfig) => mappingColors;
33
37
  export declare const IcicleGraphArrow: React.NamedExoticComponent<IcicleGraphArrowProps>;
34
38
  export default IcicleGraphArrow;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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.
@@ -17,15 +17,14 @@ import { useContextMenu } from 'react-contexify';
17
17
  import { useURLState } from '@parca/components';
18
18
  import { USER_PREFERENCES, useCurrentColorProfile, useUserPreference } from '@parca/hooks';
19
19
  import { getColorForFeature, selectDarkMode, setHoveringNode, useAppDispatch, useAppSelector, } from '@parca/store';
20
- import { getLastItem, scaleLinear, selectQueryParam } from '@parca/utilities';
20
+ import { getLastItem, scaleLinear, selectQueryParam, } from '@parca/utilities';
21
21
  import GraphTooltipArrow from '../../GraphTooltipArrow';
22
22
  import GraphTooltipArrowContent from '../../GraphTooltipArrow/Content';
23
23
  import { DockedGraphTooltip } from '../../GraphTooltipArrow/DockedGraphTooltip';
24
24
  import { useProfileViewContext } from '../../ProfileView/ProfileViewContext';
25
- import ColorStackLegend from './ColorStackLegend';
26
25
  import ContextMenu from './ContextMenu';
27
26
  import { IcicleNode, RowHeight } from './IcicleGraphNodes';
28
- import { extractFeature } from './utils';
27
+ import { arrowToString, extractFeature } from './utils';
29
28
  export const FIELD_LABELS_ONLY = 'labels_only';
30
29
  export const FIELD_MAPPING_FILE = 'mapping_file';
31
30
  export const FIELD_MAPPING_BUILD_ID = 'mapping_build_id';
@@ -42,7 +41,15 @@ export const FIELD_CUMULATIVE = 'cumulative';
42
41
  export const FIELD_CUMULATIVE_PER_SECOND = 'cumulative_per_second';
43
42
  export const FIELD_DIFF = 'diff';
44
43
  export const FIELD_DIFF_PER_SECOND = 'diff_per_second';
45
- export const IcicleGraphArrow = memo(function IcicleGraphArrow({ arrow, total, filtered, width, setCurPath, curPath, profileType, navigateTo, sortBy, mappings, }) {
44
+ export const getMappingColors = (mappingsList, isDarkMode, currentColorProfile) => {
45
+ const mappingFeatures = mappingsList.map(mapping => extractFeature(mapping));
46
+ const colors = {};
47
+ Object.entries(mappingFeatures).forEach(([_, feature]) => {
48
+ colors[feature.name] = getColorForFeature(feature.name, isDarkMode, currentColorProfile.colors);
49
+ });
50
+ return colors;
51
+ };
52
+ export const IcicleGraphArrow = memo(function IcicleGraphArrow({ arrow, total, filtered, width, setCurPath, curPath, profileType, navigateTo, sortBy, flamegraphLoading, mappingsListFromMetadata, }) {
46
53
  const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
47
54
  const dispatch = useAppDispatch();
48
55
  const [highlightSimilarStacksPreference] = useUserPreference(USER_PREFERENCES.HIGHLIGHT_SIMILAR_STACKS.key);
@@ -63,35 +70,46 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({ arrow, total, f
63
70
  });
64
71
  const currentSearchString = selectQueryParam('search_string') ?? '';
65
72
  const { compareMode } = useProfileViewContext();
66
- const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
67
73
  const currentColorProfile = useCurrentColorProfile();
68
74
  const colorForSimilarNodes = currentColorProfile.colorForSimilarNodes;
69
75
  const mappingsList = useMemo(() => {
70
- const list = mappings
71
- ?.map(mapping => {
72
- return getLastItem(mapping);
76
+ // Read the mappings from the dictionary that contains all mapping strings.
77
+ // This is great, as might only have a dozen or so mappings,
78
+ // and don't need to read through all the rows (potentially thousands).
79
+ const mappingsDict = table.getChild(FIELD_MAPPING_FILE);
80
+ const mappings = mappingsDict?.data
81
+ .map(mapping => {
82
+ if (mapping.dictionary == null) {
83
+ return [];
84
+ }
85
+ const len = mapping.dictionary.length;
86
+ const entries = [];
87
+ for (let i = 0; i < len; i++) {
88
+ const fn = arrowToString(mapping.dictionary.get(i));
89
+ entries.push(getLastItem(fn) ?? '');
90
+ }
91
+ return entries;
73
92
  })
74
93
  .flat() ?? [];
75
94
  // We add a EVERYTHING ELSE mapping to the list.
76
- list.push('');
95
+ mappings.push('');
77
96
  // We sort the mappings alphabetically to make sure that the order is always the same.
78
- list.sort((a, b) => a.localeCompare(b));
79
- return list;
80
- }, [mappings]);
97
+ mappings.sort((a, b) => a.localeCompare(b));
98
+ return mappings;
99
+ }, [table]);
81
100
  const mappingColors = useMemo(() => {
82
- const mappingFeatures = mappingsList.map(mapping => extractFeature(mapping));
83
- const colors = {};
84
- Object.entries(mappingFeatures).forEach(([_, feature]) => {
85
- colors[feature.name] = getColorForFeature(feature.name, isDarkMode, currentColorProfile.colors);
86
- });
101
+ const colors = getMappingColors(mappingsList, isDarkMode, currentColorProfile);
87
102
  return colors;
88
- }, [mappingsList, isDarkMode, currentColorProfile]);
103
+ }, [isDarkMode, mappingsList, currentColorProfile]);
89
104
  useEffect(() => {
90
105
  if (ref.current != null) {
91
106
  setHeight(ref?.current.getBoundingClientRect().height);
92
107
  }
93
- }, [width]);
108
+ }, [width, flamegraphLoading]);
94
109
  const xScale = useMemo(() => {
110
+ if (total === 0n) {
111
+ return () => 0;
112
+ }
95
113
  if (width === undefined) {
96
114
  return () => 0;
97
115
  }
@@ -118,36 +136,33 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({ arrow, total, f
118
136
  return;
119
137
  }
120
138
  // first time hiding a binary
121
- const newMappingsList = mappingsList.filter(mapping => mapping !== binaryToRemove);
139
+ const newMappingsList = mappingsListFromMetadata.filter(mapping => mapping !== binaryToRemove);
122
140
  setBinaryFrameFilter(newMappingsList);
123
141
  };
124
142
  // useMemo for the root graph as it otherwise renders the whole graph if the hoveringRow changes.
125
143
  const root = useMemo(() => {
126
144
  return (_jsx("svg", { className: "font-robotoMono", width: width, height: height, preserveAspectRatio: "xMinYMid", ref: svg, onContextMenu: displayMenu, children: _jsx("g", { ref: ref, children: _jsx("g", { transform: 'translate(0, 0)', children: _jsx(IcicleNode, { table: table, row: 0, mappingColors: mappingColors, x: 0, y: 0, totalWidth: width ?? 1, height: RowHeight, setCurPath: setCurPath, curPath: curPath, total: total, xScale: xScale, path: [], level: 0, isRoot: true, searchString: currentSearchString, setHoveringRow: setHoveringRow, setHoveringLevel: setHoveringLevel, sortBy: sortBy, darkMode: isDarkMode, compareMode: compareMode, profileType: profileType, isContextMenuOpen: isContextMenuOpen, hoveringName: hoveringName, setHoveringName: setHoveringName, hoveringRow: hoveringRow, colorForSimilarNodes: colorForSimilarNodes, highlightSimilarStacksPreference: highlightSimilarStacksPreference }) }) }) }));
127
145
  }, [
128
- compareMode,
129
- curPath,
130
- currentSearchString,
146
+ width,
131
147
  height,
132
- isDarkMode,
133
- profileType,
148
+ displayMenu,
149
+ table,
134
150
  mappingColors,
135
151
  setCurPath,
136
- sortBy,
137
- table,
152
+ curPath,
138
153
  total,
139
- width,
140
154
  xScale,
155
+ currentSearchString,
156
+ sortBy,
157
+ isDarkMode,
158
+ compareMode,
159
+ profileType,
141
160
  isContextMenuOpen,
142
- displayMenu,
143
- colorForSimilarNodes,
144
- highlightSimilarStacksPreference,
145
161
  hoveringName,
146
162
  hoveringRow,
163
+ colorForSimilarNodes,
164
+ highlightSimilarStacksPreference,
147
165
  ]);
148
- if (table.numRows === 0 || width === undefined) {
149
- return _jsx(_Fragment, {});
150
- }
151
- return (_jsx(_Fragment, { children: _jsxs("div", { onMouseLeave: () => dispatch(setHoveringNode(undefined)), children: [_jsx(ContextMenu, { menuId: MENU_ID, table: table, row: hoveringRow ?? 0, level: hoveringLevel ?? 0, total: total, totalUnfiltered: total + filtered, profileType: profileType, navigateTo: navigateTo, trackVisibility: trackVisibility, curPath: curPath, setCurPath: setCurPath, hideMenu: hideAll, hideBinary: hideBinary }), isColorStackLegendEnabled && (_jsx(ColorStackLegend, { mappingColors: mappingColors, navigateTo: navigateTo, compareMode: compareMode })), dockedMetainfo ? (_jsx(DockedGraphTooltip, { table: table, row: hoveringRow, level: hoveringLevel ?? 0, total: total, totalUnfiltered: total + filtered, profileType: profileType })) : (!isContextMenuOpen && (_jsx(GraphTooltipArrow, { contextElement: svg.current, isContextMenuOpen: isContextMenuOpen, children: _jsx(GraphTooltipArrowContent, { table: table, row: hoveringRow, level: hoveringLevel ?? 0, isFixed: false, total: total, totalUnfiltered: total + filtered, profileType: profileType, navigateTo: navigateTo }) }))), root] }) }));
166
+ return (_jsx(_Fragment, { children: _jsxs("div", { onMouseLeave: () => dispatch(setHoveringNode(undefined)), children: [_jsx(ContextMenu, { menuId: MENU_ID, table: table, row: hoveringRow ?? 0, level: hoveringLevel ?? 0, total: total, totalUnfiltered: total + filtered, profileType: profileType, navigateTo: navigateTo, trackVisibility: trackVisibility, curPath: curPath, setCurPath: setCurPath, hideMenu: hideAll, hideBinary: hideBinary }), dockedMetainfo ? (_jsx(DockedGraphTooltip, { table: table, row: hoveringRow, level: hoveringLevel ?? 0, total: total, totalUnfiltered: total + filtered, profileType: profileType })) : (!isContextMenuOpen && (_jsx(GraphTooltipArrow, { contextElement: svg.current, isContextMenuOpen: isContextMenuOpen, children: _jsx(GraphTooltipArrowContent, { table: table, row: hoveringRow, level: hoveringLevel ?? 0, isFixed: false, total: total, totalUnfiltered: total + filtered, profileType: profileType, navigateTo: navigateTo }) }))), root] }) }));
152
167
  });
153
168
  export default IcicleGraphArrow;
@@ -0,0 +1,2 @@
1
+ declare const useMappingList: (mappings: string[] | undefined) => string[];
2
+ export default useMappingList;
@@ -0,0 +1,33 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+ import { useMemo } from 'react';
14
+ import { getLastItem } from '@parca/utilities';
15
+ const useMappingList = (mappings) => {
16
+ const mappingsList = useMemo(() => {
17
+ if (mappings === undefined) {
18
+ return [];
19
+ }
20
+ const list = mappings
21
+ ?.map(mapping => {
22
+ return getLastItem(mapping);
23
+ })
24
+ .flat() ?? [];
25
+ // We add a EVERYTHING ELSE mapping to the list.
26
+ list.push('');
27
+ // We sort the mappings alphabetically to make sure that the order is always the same.
28
+ list.sort((a, b) => a.localeCompare(b));
29
+ return list;
30
+ }, [mappings]);
31
+ return mappingsList;
32
+ };
33
+ export default useMappingList;
@@ -18,6 +18,7 @@ interface ProfileIcicleGraphProps {
18
18
  error?: any;
19
19
  isHalfScreen: boolean;
20
20
  mappings?: string[];
21
+ mappingsLoading?: boolean;
21
22
  }
22
23
  declare const ProfileIcicleGraph: ({ graph, arrow, total, filtered, curPath, setNewCurPath, profileType, navigateTo, loading, setActionButtons, error, width, isHalfScreen, mappings, }: ProfileIcicleGraphProps) => JSX.Element;
23
24
  export default ProfileIcicleGraph;
@@ -14,15 +14,17 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
14
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
23
  import SortBySelect from './ActionButtons/SortBySelect';
24
24
  import IcicleGraph from './IcicleGraph';
25
25
  import IcicleGraphArrow, { FIELD_FUNCTION_NAME } from './IcicleGraphArrow';
26
+ import ColorStackLegend from './IcicleGraphArrow/ColorStackLegend';
27
+ import useMappingList from './IcicleGraphArrow/useMappingList';
26
28
  const numberFormatter = new Intl.NumberFormat('en-US');
27
29
  const ErrorContent = ({ errorMessage }) => {
28
30
  return _jsx("div", { className: "flex justify-center p-10", children: errorMessage });
@@ -80,6 +82,8 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, to
80
82
  const { onError, authenticationErrorMessage, isDarkMode } = useParcaContext();
81
83
  const { compareMode } = useProfileViewContext();
82
84
  const [isLoading, setIsLoading] = useState(true);
85
+ const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
86
+ const mappingsList = useMappingList(mappings);
83
87
  const [storeSortBy = FIELD_FUNCTION_NAME] = useURLState({
84
88
  param: 'sort_by',
85
89
  navigateTo,
@@ -108,14 +112,7 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, to
108
112
  ];
109
113
  }, [graph, arrow, filtered, total]);
110
114
  useEffect(() => {
111
- if (isLoading && setActionButtons !== undefined) {
112
- setActionButtons(_jsx(IcicleActionButtonPlaceholder, { isHalfScreen: isHalfScreen }));
113
- return;
114
- }
115
- if (setActionButtons === undefined) {
116
- return;
117
- }
118
- 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 }), arrow !== undefined && 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 })] }))] }) }));
115
+ 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 })] }))] }) }));
119
116
  }, [
120
117
  navigateTo,
121
118
  isInvert,
@@ -128,17 +125,15 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, to
128
125
  isHalfScreen,
129
126
  isLoading,
130
127
  ]);
128
+ const loadingState = !loading && (arrow !== undefined || graph !== undefined) && mappings !== undefined;
131
129
  useEffect(() => {
132
- if (!loading && (arrow !== undefined || graph !== undefined)) {
130
+ if (loadingState) {
133
131
  setIsLoading(false);
134
132
  }
135
133
  else {
136
134
  setIsLoading(true);
137
135
  }
138
- }, [loading, arrow, graph]);
139
- if (isLoading) {
140
- return (_jsx("div", { className: "h-auto overflow-clip", children: _jsx(IcicleGraphSkeleton, { isHalfScreen: isHalfScreen, isDarkMode: isDarkMode }) }));
141
- }
136
+ }, [loadingState]);
142
137
  if (error != null) {
143
138
  onError?.(error);
144
139
  if (authenticationErrorMessage !== undefined && error.code === 'UNAUTHENTICATED') {
@@ -146,13 +141,39 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, to
146
141
  }
147
142
  return _jsx(ErrorContent, { errorMessage: capitalizeOnlyFirstLetter(error.message) });
148
143
  }
149
- if (graph === undefined && arrow === undefined)
150
- return _jsx("div", { className: "mx-auto text-center", children: "No data..." });
151
- if (total === 0n && !loading)
152
- return _jsx("div", { className: "mx-auto text-center", children: "Profile has no samples" });
144
+ // eslint-disable-next-line react-hooks/rules-of-hooks
145
+ const icicleGraph = useMemo(() => {
146
+ if (isLoading) {
147
+ return (_jsx("div", { className: "h-auto overflow-clip", children: _jsx(IcicleGraphSkeleton, { isHalfScreen: isHalfScreen, isDarkMode: isDarkMode }) }));
148
+ }
149
+ if (graph === undefined && arrow === undefined)
150
+ return _jsx("div", { className: "mx-auto text-center", children: "No data..." });
151
+ if (total === 0n && !loading)
152
+ return _jsx("div", { className: "mx-auto text-center", children: "Profile has no samples" });
153
+ if (graph !== undefined)
154
+ return (_jsx(IcicleGraph, { width: width, graph: graph, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, profileType: profileType, navigateTo: navigateTo }));
155
+ if (arrow !== undefined)
156
+ 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, mappingsListFromMetadata: mappingsList }));
157
+ }, [
158
+ isLoading,
159
+ graph,
160
+ arrow,
161
+ total,
162
+ loading,
163
+ width,
164
+ filtered,
165
+ curPath,
166
+ setNewCurPath,
167
+ profileType,
168
+ navigateTo,
169
+ storeSortBy,
170
+ isHalfScreen,
171
+ isDarkMode,
172
+ mappingsList,
173
+ ]);
153
174
  if (isTrimmed) {
154
175
  console.info(`Trimmed ${trimmedFormatted} (${trimmedPercentage}%) too small values.`);
155
176
  }
156
- 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, mappings: mappings }))] }), _jsxs("p", { className: "my-2 text-xs", children: ["Showing ", totalFormatted, ' ', isFiltered ? (_jsxs("span", { children: ["(", filteredPercentage, "%) filtered of ", totalUnfilteredFormatted, ' '] })) : (_jsx(_Fragment, {})), "values.", ' '] })] }, "icicle-graph-loaded") }));
177
+ 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, loading: isLoading })), _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") }));
157
178
  };
158
179
  export default ProfileIcicleGraph;
@@ -107,11 +107,11 @@ export const ProfileView = ({ total, filtered, flamegraphData, topTableData, cal
107
107
  return (_jsx(ConditionalWrapper, { condition: perf?.onRender != null, WrapperComponent: Profiler, wrapperProps: {
108
108
  id: 'icicleGraph',
109
109
  onRender: perf?.onRender,
110
- }, children: _jsx(ProfileIcicleGraph, { curPath: curPath, setNewCurPath: setNewCurPath, arrow: flamegraphData?.arrow, graph: flamegraphData?.data, total: total, filtered: filtered, profileType: profileSource?.ProfileType(), navigateTo: navigateTo, loading: flamegraphData.loading && flamegraphData.mappingsLoading, setActionButtons: setActionButtons, error: flamegraphData.error, isHalfScreen: isHalfScreen, width: dimensions?.width !== undefined
110
+ }, children: _jsx(ProfileIcicleGraph, { curPath: curPath, setNewCurPath: setNewCurPath, arrow: flamegraphData?.arrow, graph: flamegraphData?.data, total: total, filtered: filtered, profileType: profileSource?.ProfileType(), navigateTo: navigateTo, loading: flamegraphData.loading, setActionButtons: setActionButtons, error: flamegraphData.error, isHalfScreen: isHalfScreen, width: dimensions?.width !== undefined
111
111
  ? isHalfScreen
112
112
  ? (dimensions.width - 40) / 2
113
113
  : dimensions.width - 16
114
- : 0, mappings: flamegraphData.mappings }) }));
114
+ : 0, mappings: flamegraphData.mappings, mappingsLoading: flamegraphData.mappingsLoading }) }));
115
115
  }
116
116
  case 'callgraph': {
117
117
  return callgraphData?.data !== undefined &&
@@ -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
  }, [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.16.375",
3
+ "version": "0.16.377",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@headlessui/react": "^1.7.19",
@@ -71,5 +71,5 @@
71
71
  "access": "public",
72
72
  "registry": "https://registry.npmjs.org/"
73
73
  },
74
- "gitHead": "1c9338c7294c1df88345032cee964653ce4e349b"
74
+ "gitHead": "7f48ce8d53a8f749508d0b002c04bf1257c4bbde"
75
75
  }
@@ -17,23 +17,28 @@ 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 {type NavigateFunction} from '@parca/utilities';
23
23
 
24
- import {mappingColors} from './IcicleGraphNodes';
24
+ import {getMappingColors} from '.';
25
+ import useMappingList from './useMappingList';
25
26
 
26
27
  interface Props {
27
- mappingColors: mappingColors;
28
+ mappings?: string[];
29
+ loading?: boolean;
28
30
  navigateTo?: NavigateFunction;
29
31
  compareMode?: boolean;
30
32
  }
31
33
 
32
34
  const ColorStackLegend = ({
33
- mappingColors,
35
+ mappings,
34
36
  navigateTo,
35
37
  compareMode = false,
38
+ loading,
36
39
  }: Props): React.JSX.Element => {
40
+ const isDarkMode = useAppSelector(selectDarkMode);
41
+ const currentColorProfile = useCurrentColorProfile();
37
42
  const [colorProfileName] = useUserPreference<string>(
38
43
  USER_PREFERENCES.FLAMEGRAPH_COLOR_PROFILE.key
39
44
  );
@@ -42,6 +47,13 @@ const ColorStackLegend = ({
42
47
  navigateTo,
43
48
  });
44
49
 
50
+ const mappingsList = useMappingList(mappings);
51
+
52
+ const mappingColors = useMemo(() => {
53
+ const colors = getMappingColors(mappingsList, isDarkMode, currentColorProfile);
54
+ return colors;
55
+ }, [isDarkMode, mappingsList, currentColorProfile]);
56
+
45
57
  const stackColorArray = useMemo(() => {
46
58
  return Object.entries(mappingColors).sort(([featureA], [featureB]) => {
47
59
  if (featureA === EVERYTHING_ELSE) {
@@ -54,7 +66,7 @@ const ColorStackLegend = ({
54
66
  });
55
67
  }, [mappingColors]);
56
68
 
57
- if (mappingColors === undefined) {
69
+ if (stackColorArray.length === 0 && loading === false) {
58
70
  return <></>;
59
71
  }
60
72
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react';
15
15
 
16
- import {Table, tableFromIPC} from 'apache-arrow';
16
+ import {Dictionary, Table, Vector, tableFromIPC} from 'apache-arrow';
17
17
  import {useContextMenu} from 'react-contexify';
18
18
 
19
19
  import {FlamegraphArrow} from '@parca/client';
@@ -27,16 +27,21 @@ import {
27
27
  useAppDispatch,
28
28
  useAppSelector,
29
29
  } from '@parca/store';
30
- import {getLastItem, scaleLinear, selectQueryParam, type NavigateFunction} from '@parca/utilities';
30
+ import {
31
+ getLastItem,
32
+ scaleLinear,
33
+ selectQueryParam,
34
+ type ColorConfig,
35
+ type NavigateFunction,
36
+ } from '@parca/utilities';
31
37
 
32
38
  import GraphTooltipArrow from '../../GraphTooltipArrow';
33
39
  import GraphTooltipArrowContent from '../../GraphTooltipArrow/Content';
34
40
  import {DockedGraphTooltip} from '../../GraphTooltipArrow/DockedGraphTooltip';
35
41
  import {useProfileViewContext} from '../../ProfileView/ProfileViewContext';
36
- import ColorStackLegend from './ColorStackLegend';
37
42
  import ContextMenu from './ContextMenu';
38
43
  import {IcicleNode, RowHeight, mappingColors} from './IcicleGraphNodes';
39
- import {extractFeature} from './utils';
44
+ import {arrowToString, extractFeature} from './utils';
40
45
 
41
46
  export const FIELD_LABELS_ONLY = 'labels_only';
42
47
  export const FIELD_MAPPING_FILE = 'mapping_file';
@@ -65,9 +70,25 @@ interface IcicleGraphArrowProps {
65
70
  setCurPath: (path: string[]) => void;
66
71
  navigateTo?: NavigateFunction;
67
72
  sortBy: string;
68
- mappings?: string[];
73
+ flamegraphLoading: boolean;
74
+ isHalfScreen: boolean;
75
+ mappingsListFromMetadata: string[];
69
76
  }
70
77
 
78
+ export const getMappingColors = (
79
+ mappingsList: string[],
80
+ isDarkMode: boolean,
81
+ currentColorProfile: ColorConfig
82
+ ): mappingColors => {
83
+ const mappingFeatures = mappingsList.map(mapping => extractFeature(mapping));
84
+
85
+ const colors: mappingColors = {};
86
+ Object.entries(mappingFeatures).forEach(([_, feature]) => {
87
+ colors[feature.name] = getColorForFeature(feature.name, isDarkMode, currentColorProfile.colors);
88
+ });
89
+ return colors;
90
+ };
91
+
71
92
  export const IcicleGraphArrow = memo(function IcicleGraphArrow({
72
93
  arrow,
73
94
  total,
@@ -78,7 +99,8 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
78
99
  profileType,
79
100
  navigateTo,
80
101
  sortBy,
81
- mappings,
102
+ flamegraphLoading,
103
+ mappingsListFromMetadata,
82
104
  }: IcicleGraphArrowProps): React.JSX.Element {
83
105
  const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean>(false);
84
106
  const dispatch = useAppDispatch();
@@ -106,50 +128,54 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
106
128
 
107
129
  const currentSearchString = (selectQueryParam('search_string') as string) ?? '';
108
130
  const {compareMode} = useProfileViewContext();
109
- const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
110
131
  const currentColorProfile = useCurrentColorProfile();
111
132
  const colorForSimilarNodes = currentColorProfile.colorForSimilarNodes;
112
133
 
113
134
  const mappingsList = useMemo(() => {
114
- const list =
115
- mappings
116
- ?.map(mapping => {
117
- return getLastItem(mapping) as string;
135
+ // Read the mappings from the dictionary that contains all mapping strings.
136
+ // This is great, as might only have a dozen or so mappings,
137
+ // and don't need to read through all the rows (potentially thousands).
138
+ const mappingsDict: Vector<Dictionary> | null = table.getChild(FIELD_MAPPING_FILE);
139
+ const mappings =
140
+ mappingsDict?.data
141
+ .map(mapping => {
142
+ if (mapping.dictionary == null) {
143
+ return [];
144
+ }
145
+ const len = mapping.dictionary.length;
146
+ const entries: string[] = [];
147
+ for (let i = 0; i < len; i++) {
148
+ const fn = arrowToString(mapping.dictionary.get(i));
149
+ entries.push(getLastItem(fn) ?? '');
150
+ }
151
+ return entries;
118
152
  })
119
153
  .flat() ?? [];
120
154
 
121
155
  // We add a EVERYTHING ELSE mapping to the list.
122
- list.push('');
156
+ mappings.push('');
123
157
 
124
158
  // We sort the mappings alphabetically to make sure that the order is always the same.
125
- list.sort((a, b) => a.localeCompare(b));
126
-
127
- return list;
128
- }, [mappings]);
159
+ mappings.sort((a, b) => a.localeCompare(b));
160
+ return mappings;
161
+ }, [table]);
129
162
 
130
163
  const mappingColors = useMemo(() => {
131
- const mappingFeatures = mappingsList.map(mapping => extractFeature(mapping));
132
-
133
- const colors: mappingColors = {};
134
-
135
- Object.entries(mappingFeatures).forEach(([_, feature]) => {
136
- colors[feature.name] = getColorForFeature(
137
- feature.name,
138
- isDarkMode,
139
- currentColorProfile.colors
140
- );
141
- });
142
-
164
+ const colors = getMappingColors(mappingsList, isDarkMode, currentColorProfile);
143
165
  return colors;
144
- }, [mappingsList, isDarkMode, currentColorProfile]);
166
+ }, [isDarkMode, mappingsList, currentColorProfile]);
145
167
 
146
168
  useEffect(() => {
147
169
  if (ref.current != null) {
148
170
  setHeight(ref?.current.getBoundingClientRect().height);
149
171
  }
150
- }, [width]);
172
+ }, [width, flamegraphLoading]);
151
173
 
152
174
  const xScale = useMemo(() => {
175
+ if (total === 0n) {
176
+ return () => 0;
177
+ }
178
+
153
179
  if (width === undefined) {
154
180
  return () => 0;
155
181
  }
@@ -184,7 +210,7 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
184
210
  }
185
211
 
186
212
  // first time hiding a binary
187
- const newMappingsList = mappingsList.filter(mapping => mapping !== binaryToRemove);
213
+ const newMappingsList = mappingsListFromMetadata.filter(mapping => mapping !== binaryToRemove);
188
214
  setBinaryFrameFilter(newMappingsList);
189
215
  };
190
216
 
@@ -235,31 +261,27 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
235
261
  </svg>
236
262
  );
237
263
  }, [
238
- compareMode,
239
- curPath,
240
- currentSearchString,
264
+ width,
241
265
  height,
242
- isDarkMode,
243
- profileType,
266
+ displayMenu,
267
+ table,
244
268
  mappingColors,
245
269
  setCurPath,
246
- sortBy,
247
- table,
270
+ curPath,
248
271
  total,
249
- width,
250
272
  xScale,
273
+ currentSearchString,
274
+ sortBy,
275
+ isDarkMode,
276
+ compareMode,
277
+ profileType,
251
278
  isContextMenuOpen,
252
- displayMenu,
253
- colorForSimilarNodes,
254
- highlightSimilarStacksPreference,
255
279
  hoveringName,
256
280
  hoveringRow,
281
+ colorForSimilarNodes,
282
+ highlightSimilarStacksPreference,
257
283
  ]);
258
284
 
259
- if (table.numRows === 0 || width === undefined) {
260
- return <></>;
261
- }
262
-
263
285
  return (
264
286
  <>
265
287
  <div onMouseLeave={() => dispatch(setHoveringNode(undefined))}>
@@ -278,13 +300,6 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
278
300
  hideMenu={hideAll}
279
301
  hideBinary={hideBinary}
280
302
  />
281
- {isColorStackLegendEnabled && (
282
- <ColorStackLegend
283
- mappingColors={mappingColors}
284
- navigateTo={navigateTo}
285
- compareMode={compareMode}
286
- />
287
- )}
288
303
  {dockedMetainfo ? (
289
304
  <DockedGraphTooltip
290
305
  table={table}
@@ -0,0 +1,42 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ import {useMemo} from 'react';
15
+
16
+ import {getLastItem} from '@parca/utilities';
17
+
18
+ const useMappingList = (mappings: string[] | undefined): string[] => {
19
+ const mappingsList = useMemo(() => {
20
+ if (mappings === undefined) {
21
+ return [];
22
+ }
23
+ const list =
24
+ mappings
25
+ ?.map(mapping => {
26
+ return getLastItem(mapping) as string;
27
+ })
28
+ .flat() ?? [];
29
+
30
+ // We add a EVERYTHING ELSE mapping to the list.
31
+ list.push('');
32
+
33
+ // We sort the mappings alphabetically to make sure that the order is always the same.
34
+ list.sort((a, b) => a.localeCompare(b));
35
+
36
+ return list;
37
+ }, [mappings]);
38
+
39
+ return mappingsList;
40
+ };
41
+
42
+ export default useMappingList;
@@ -19,7 +19,6 @@ import {AnimatePresence, motion} from 'framer-motion';
19
19
  import {Flamegraph, FlamegraphArrow} from '@parca/client';
20
20
  import {
21
21
  Button,
22
- IcicleActionButtonPlaceholder,
23
22
  IcicleGraphSkeleton,
24
23
  IconButton,
25
24
  useParcaContext,
@@ -27,7 +26,12 @@ import {
27
26
  } from '@parca/components';
28
27
  import {USER_PREFERENCES, useUserPreference} from '@parca/hooks';
29
28
  import {ProfileType} from '@parca/parser';
30
- import {capitalizeOnlyFirstLetter, divide, type NavigateFunction} from '@parca/utilities';
29
+ import {
30
+ capitalizeOnlyFirstLetter,
31
+ divide,
32
+ selectQueryParam,
33
+ type NavigateFunction,
34
+ } from '@parca/utilities';
31
35
 
32
36
  import {useProfileViewContext} from '../ProfileView/ProfileViewContext';
33
37
  import DiffLegend from '../components/DiffLegend';
@@ -35,6 +39,8 @@ import GroupByDropdown from './ActionButtons/GroupByDropdown';
35
39
  import SortBySelect from './ActionButtons/SortBySelect';
36
40
  import IcicleGraph from './IcicleGraph';
37
41
  import IcicleGraphArrow, {FIELD_FUNCTION_NAME} from './IcicleGraphArrow';
42
+ import ColorStackLegend from './IcicleGraphArrow/ColorStackLegend';
43
+ import useMappingList from './IcicleGraphArrow/useMappingList';
38
44
 
39
45
  const numberFormatter = new Intl.NumberFormat('en-US');
40
46
 
@@ -55,6 +61,7 @@ interface ProfileIcicleGraphProps {
55
61
  error?: any;
56
62
  isHalfScreen: boolean;
57
63
  mappings?: string[];
64
+ mappingsLoading?: boolean;
58
65
  }
59
66
 
60
67
  const ErrorContent = ({errorMessage}: {errorMessage: string}): JSX.Element => {
@@ -218,6 +225,9 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
218
225
  const {onError, authenticationErrorMessage, isDarkMode} = useParcaContext();
219
226
  const {compareMode} = useProfileViewContext();
220
227
  const [isLoading, setIsLoading] = useState<boolean>(true);
228
+ const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
229
+
230
+ const mappingsList = useMappingList(mappings);
221
231
 
222
232
  const [storeSortBy = FIELD_FUNCTION_NAME] = useURLState({
223
233
  param: 'sort_by',
@@ -262,20 +272,11 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
262
272
  }, [graph, arrow, filtered, total]);
263
273
 
264
274
  useEffect(() => {
265
- if (isLoading && setActionButtons !== undefined) {
266
- setActionButtons(<IcicleActionButtonPlaceholder isHalfScreen={isHalfScreen} />);
267
- return;
268
- }
269
-
270
- if (setActionButtons === undefined) {
271
- return;
272
- }
273
-
274
- setActionButtons(
275
+ setActionButtons?.(
275
276
  <div className="flex w-full justify-end gap-2 pb-2">
276
277
  <div className="ml-2 flex w-full flex-col items-start justify-between gap-2 md:flex-row md:items-end">
277
- {arrow !== undefined && <GroupAndSortActionButtons navigateTo={navigateTo} />}
278
- {arrow !== undefined && isHalfScreen ? (
278
+ {<GroupAndSortActionButtons navigateTo={navigateTo} />}
279
+ {isHalfScreen ? (
279
280
  <IconButton
280
281
  icon={isInvert ? 'ph:sort-ascending' : 'ph:sort-descending'}
281
282
  toolTipText={isInvert ? 'Original Call Stack' : 'Invert Call Stack'}
@@ -328,21 +329,16 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
328
329
  isLoading,
329
330
  ]);
330
331
 
332
+ const loadingState =
333
+ !loading && (arrow !== undefined || graph !== undefined) && mappings !== undefined;
334
+
331
335
  useEffect(() => {
332
- if (!loading && (arrow !== undefined || graph !== undefined)) {
336
+ if (loadingState) {
333
337
  setIsLoading(false);
334
338
  } else {
335
339
  setIsLoading(true);
336
340
  }
337
- }, [loading, arrow, graph]);
338
-
339
- if (isLoading) {
340
- return (
341
- <div className="h-auto overflow-clip">
342
- <IcicleGraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
343
- </div>
344
- );
345
- }
341
+ }, [loadingState]);
346
342
 
347
343
  if (error != null) {
348
344
  onError?.(error);
@@ -354,11 +350,70 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
354
350
  return <ErrorContent errorMessage={capitalizeOnlyFirstLetter(error.message)} />;
355
351
  }
356
352
 
357
- if (graph === undefined && arrow === undefined)
358
- return <div className="mx-auto text-center">No data...</div>;
353
+ // eslint-disable-next-line react-hooks/rules-of-hooks
354
+ const icicleGraph = useMemo(() => {
355
+ if (isLoading) {
356
+ return (
357
+ <div className="h-auto overflow-clip">
358
+ <IcicleGraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
359
+ </div>
360
+ );
361
+ }
359
362
 
360
- if (total === 0n && !loading)
361
- return <div className="mx-auto text-center">Profile has no samples</div>;
363
+ if (graph === undefined && arrow === undefined)
364
+ return <div className="mx-auto text-center">No data...</div>;
365
+
366
+ if (total === 0n && !loading)
367
+ return <div className="mx-auto text-center">Profile has no samples</div>;
368
+
369
+ if (graph !== undefined)
370
+ return (
371
+ <IcicleGraph
372
+ width={width}
373
+ graph={graph}
374
+ total={total}
375
+ filtered={filtered}
376
+ curPath={curPath}
377
+ setCurPath={setNewCurPath}
378
+ profileType={profileType}
379
+ navigateTo={navigateTo}
380
+ />
381
+ );
382
+
383
+ if (arrow !== undefined)
384
+ return (
385
+ <IcicleGraphArrow
386
+ width={width}
387
+ arrow={arrow}
388
+ total={total}
389
+ filtered={filtered}
390
+ curPath={curPath}
391
+ setCurPath={setNewCurPath}
392
+ profileType={profileType}
393
+ navigateTo={navigateTo}
394
+ sortBy={storeSortBy as string}
395
+ flamegraphLoading={isLoading}
396
+ isHalfScreen={isHalfScreen}
397
+ mappingsListFromMetadata={mappingsList}
398
+ />
399
+ );
400
+ }, [
401
+ isLoading,
402
+ graph,
403
+ arrow,
404
+ total,
405
+ loading,
406
+ width,
407
+ filtered,
408
+ curPath,
409
+ setNewCurPath,
410
+ profileType,
411
+ navigateTo,
412
+ storeSortBy,
413
+ isHalfScreen,
414
+ isDarkMode,
415
+ mappingsList,
416
+ ]);
362
417
 
363
418
  if (isTrimmed) {
364
419
  console.info(`Trimmed ${trimmedFormatted} (${trimmedPercentage}%) too small values.`);
@@ -374,33 +429,16 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
374
429
  transition={{duration: 0.5}}
375
430
  >
376
431
  {compareMode ? <DiffLegend /> : null}
432
+ {isColorStackLegendEnabled && (
433
+ <ColorStackLegend
434
+ navigateTo={navigateTo}
435
+ compareMode={compareMode}
436
+ mappings={mappings}
437
+ loading={isLoading}
438
+ />
439
+ )}
377
440
  <div className="min-h-48" id="h-icicle-graph">
378
- {graph !== undefined && (
379
- <IcicleGraph
380
- width={width}
381
- graph={graph}
382
- total={total}
383
- filtered={filtered}
384
- curPath={curPath}
385
- setCurPath={setNewCurPath}
386
- profileType={profileType}
387
- navigateTo={navigateTo}
388
- />
389
- )}
390
- {arrow !== undefined && (
391
- <IcicleGraphArrow
392
- width={width}
393
- arrow={arrow}
394
- total={total}
395
- filtered={filtered}
396
- curPath={curPath}
397
- setCurPath={setNewCurPath}
398
- profileType={profileType}
399
- navigateTo={navigateTo}
400
- sortBy={storeSortBy as string}
401
- mappings={mappings}
402
- />
403
- )}
441
+ <>{icicleGraph}</>
404
442
  </div>
405
443
  <p className="my-2 text-xs">
406
444
  Showing {totalFormatted}{' '}
@@ -240,7 +240,7 @@ export const ProfileView = ({
240
240
  filtered={filtered}
241
241
  profileType={profileSource?.ProfileType()}
242
242
  navigateTo={navigateTo}
243
- loading={flamegraphData.loading && flamegraphData.mappingsLoading}
243
+ loading={flamegraphData.loading}
244
244
  setActionButtons={setActionButtons}
245
245
  error={flamegraphData.error}
246
246
  isHalfScreen={isHalfScreen}
@@ -252,6 +252,7 @@ export const ProfileView = ({
252
252
  : 0
253
253
  }
254
254
  mappings={flamegraphData.mappings}
255
+ mappingsLoading={flamegraphData.mappingsLoading}
255
256
  />
256
257
  </ConditionalWrapper>
257
258
  );
@@ -18,7 +18,6 @@ import {AnimatePresence, motion} from 'framer-motion';
18
18
 
19
19
  import {
20
20
  Button,
21
- TableActionButtonPlaceholder,
22
21
  Table as TableComponent,
23
22
  TableSkeleton,
24
23
  useParcaContext,
@@ -310,16 +309,7 @@ export const Table = React.memo(function Table({
310
309
  }, [navigateTo, router, filterByFunctionInput]);
311
310
 
312
311
  useEffect(() => {
313
- if (loading && setActionButtons !== undefined) {
314
- setActionButtons(<TableActionButtonPlaceholder />);
315
- return;
316
- }
317
-
318
- if (setActionButtons === undefined) {
319
- return;
320
- }
321
-
322
- setActionButtons(
312
+ setActionButtons?.(
323
313
  <>
324
314
  <ColumnsVisibility
325
315
  columns={columns}