@parca/profile 0.16.375 → 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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,10 @@
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.376 (2024-05-30)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
6
10
  ## [0.16.375](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.374...@parca/profile@0.16.375) (2024-05-23)
7
11
 
8
12
  **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
+ mappingsLoading?: 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, mappingsLoading, }: Props) => React.JSX.Element;
10
10
  export default ColorStackLegend;
@@ -15,14 +15,37 @@ 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 { getLastItem } from '@parca/utilities';
21
+ import { getMappingColors } from '.';
22
+ const ColorStackLegend = ({ mappings, navigateTo, compareMode = false, mappingsLoading, }) => {
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 = useMemo(() => {
31
+ if (mappings === undefined) {
32
+ return [];
33
+ }
34
+ const list = mappings
35
+ ?.map(mapping => {
36
+ return getLastItem(mapping);
37
+ })
38
+ .flat() ?? [];
39
+ // We add a EVERYTHING ELSE mapping to the list.
40
+ list.push('');
41
+ // We sort the mappings alphabetically to make sure that the order is always the same.
42
+ list.sort((a, b) => a.localeCompare(b));
43
+ return list;
44
+ }, [mappings]);
45
+ const mappingColors = useMemo(() => {
46
+ const colors = getMappingColors(mappingsList, isDarkMode, currentColorProfile);
47
+ return colors;
48
+ }, [isDarkMode, mappingsList, currentColorProfile]);
26
49
  const stackColorArray = useMemo(() => {
27
50
  return Object.entries(mappingColors).sort(([featureA], [featureB]) => {
28
51
  if (featureA === EVERYTHING_ELSE) {
@@ -34,7 +57,7 @@ const ColorStackLegend = ({ mappingColors, navigateTo, compareMode = false, }) =
34
57
  return featureA?.localeCompare(featureB ?? '') ?? 0;
35
58
  });
36
59
  }, [mappingColors]);
37
- if (mappingColors === undefined) {
60
+ if (mappingColors === undefined && mappingsLoading === false) {
38
61
  return _jsx(_Fragment, {});
39
62
  }
40
63
  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,9 @@ 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;
32
34
  }
35
+ export declare const getMappingColors: (mappingsList: string[], isDarkMode: boolean, currentColorProfile: ColorConfig) => mappingColors;
33
36
  export declare const IcicleGraphArrow: React.NamedExoticComponent<IcicleGraphArrowProps>;
34
37
  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, }) {
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,47 @@ 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';
73
+ // const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
67
74
  const currentColorProfile = useCurrentColorProfile();
68
75
  const colorForSimilarNodes = currentColorProfile.colorForSimilarNodes;
69
76
  const mappingsList = useMemo(() => {
70
- const list = mappings
71
- ?.map(mapping => {
72
- return getLastItem(mapping);
77
+ // Read the mappings from the dictionary that contains all mapping strings.
78
+ // This is great, as might only have a dozen or so mappings,
79
+ // and don't need to read through all the rows (potentially thousands).
80
+ const mappingsDict = table.getChild(FIELD_MAPPING_FILE);
81
+ const mappings = mappingsDict?.data
82
+ .map(mapping => {
83
+ if (mapping.dictionary == null) {
84
+ return [];
85
+ }
86
+ const len = mapping.dictionary.length;
87
+ const entries = [];
88
+ for (let i = 0; i < len; i++) {
89
+ const fn = arrowToString(mapping.dictionary.get(i));
90
+ entries.push(getLastItem(fn) ?? '');
91
+ }
92
+ return entries;
73
93
  })
74
94
  .flat() ?? [];
75
95
  // We add a EVERYTHING ELSE mapping to the list.
76
- list.push('');
96
+ mappings.push('');
77
97
  // 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]);
98
+ mappings.sort((a, b) => a.localeCompare(b));
99
+ return mappings;
100
+ }, [table]);
81
101
  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
- });
102
+ const colors = getMappingColors(mappingsList, isDarkMode, currentColorProfile);
87
103
  return colors;
88
- }, [mappingsList, isDarkMode, currentColorProfile]);
104
+ }, [isDarkMode, mappingsList, currentColorProfile]);
89
105
  useEffect(() => {
90
106
  if (ref.current != null) {
91
107
  setHeight(ref?.current.getBoundingClientRect().height);
92
108
  }
93
- }, [width]);
109
+ }, [width, flamegraphLoading]);
94
110
  const xScale = useMemo(() => {
111
+ if (total === 0n) {
112
+ return () => 0;
113
+ }
95
114
  if (width === undefined) {
96
115
  return () => 0;
97
116
  }
@@ -125,29 +144,26 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({ arrow, total, f
125
144
  const root = useMemo(() => {
126
145
  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
146
  }, [
128
- compareMode,
129
- curPath,
130
- currentSearchString,
147
+ width,
131
148
  height,
132
- isDarkMode,
133
- profileType,
149
+ displayMenu,
150
+ table,
134
151
  mappingColors,
135
152
  setCurPath,
136
- sortBy,
137
- table,
153
+ curPath,
138
154
  total,
139
- width,
140
155
  xScale,
156
+ currentSearchString,
157
+ sortBy,
158
+ isDarkMode,
159
+ compareMode,
160
+ profileType,
141
161
  isContextMenuOpen,
142
- displayMenu,
143
- colorForSimilarNodes,
144
- highlightSimilarStacksPreference,
145
162
  hoveringName,
146
163
  hoveringRow,
164
+ colorForSimilarNodes,
165
+ highlightSimilarStacksPreference,
147
166
  ]);
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] }) }));
167
+ 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
168
  });
153
169
  export default IcicleGraphArrow;
@@ -18,6 +18,7 @@ interface ProfileIcicleGraphProps {
18
18
  error?: any;
19
19
  isHalfScreen: boolean;
20
20
  mappings?: string[];
21
+ mappingsLoading?: boolean;
21
22
  }
22
- declare const ProfileIcicleGraph: ({ graph, arrow, total, filtered, curPath, setNewCurPath, profileType, navigateTo, loading, setActionButtons, error, width, isHalfScreen, mappings, }: ProfileIcicleGraphProps) => JSX.Element;
23
+ declare const ProfileIcicleGraph: ({ graph, arrow, total, filtered, curPath, setNewCurPath, profileType, navigateTo, loading, setActionButtons, error, width, isHalfScreen, mappings, mappingsLoading, }: ProfileIcicleGraphProps) => JSX.Element;
23
24
  export default ProfileIcicleGraph;
@@ -14,15 +14,16 @@ 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';
26
27
  const numberFormatter = new Intl.NumberFormat('en-US');
27
28
  const ErrorContent = ({ errorMessage }) => {
28
29
  return _jsx("div", { className: "flex justify-center p-10", children: errorMessage });
@@ -76,10 +77,11 @@ const GroupAndSortActionButtons = ({ navigateTo }) => {
76
77
  }, [groupBy, setGroupBy]);
77
78
  return (_jsxs(_Fragment, { children: [_jsx(GroupByDropdown, { groupBy: groupBy, toggleGroupBy: toggleGroupBy }), _jsx(SortBySelect, { compareMode: compareMode, sortBy: storeSortBy, setSortBy: setStoreSortBy })] }));
78
79
  };
79
- const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, total, filtered, curPath, setNewCurPath, profileType, navigateTo, loading, setActionButtons, error, width, isHalfScreen, mappings, }) {
80
+ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, total, filtered, curPath, setNewCurPath, profileType, navigateTo, loading, setActionButtons, error, width, isHalfScreen, mappings, mappingsLoading, }) {
80
81
  const { onError, authenticationErrorMessage, isDarkMode } = useParcaContext();
81
82
  const { compareMode } = useProfileViewContext();
82
83
  const [isLoading, setIsLoading] = useState(true);
84
+ const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
83
85
  const [storeSortBy = FIELD_FUNCTION_NAME] = useURLState({
84
86
  param: 'sort_by',
85
87
  navigateTo,
@@ -108,14 +110,7 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, to
108
110
  ];
109
111
  }, [graph, arrow, filtered, total]);
110
112
  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 })] }))] }) }));
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 })] }))] }) }));
119
114
  }, [
120
115
  navigateTo,
121
116
  isInvert,
@@ -136,9 +131,6 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, to
136
131
  setIsLoading(true);
137
132
  }
138
133
  }, [loading, arrow, graph]);
139
- if (isLoading) {
140
- return (_jsx("div", { className: "h-auto overflow-clip", children: _jsx(IcicleGraphSkeleton, { isHalfScreen: isHalfScreen, isDarkMode: isDarkMode }) }));
141
- }
142
134
  if (error != null) {
143
135
  onError?.(error);
144
136
  if (authenticationErrorMessage !== undefined && error.code === 'UNAUTHENTICATED') {
@@ -146,13 +138,38 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, arrow, to
146
138
  }
147
139
  return _jsx(ErrorContent, { errorMessage: capitalizeOnlyFirstLetter(error.message) });
148
140
  }
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" });
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
+ ]);
153
170
  if (isTrimmed) {
154
171
  console.info(`Trimmed ${trimmedFormatted} (${trimmedPercentage}%) too small values.`);
155
172
  }
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") }));
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") }));
157
174
  };
158
175
  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.376",
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": "6f30d0d2d958ea4a32f58671bfc97bf72eb232d7"
75
75
  }
@@ -17,23 +17,27 @@ 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
  );
@@ -42,6 +46,31 @@ const ColorStackLegend = ({
42
46
  navigateTo,
43
47
  });
44
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]);
73
+
45
74
  const stackColorArray = useMemo(() => {
46
75
  return Object.entries(mappingColors).sort(([featureA], [featureB]) => {
47
76
  if (featureA === EVERYTHING_ELSE) {
@@ -54,7 +83,7 @@ const ColorStackLegend = ({
54
83
  });
55
84
  }, [mappingColors]);
56
85
 
57
- if (mappingColors === undefined) {
86
+ if (mappingColors === undefined && mappingsLoading === false) {
58
87
  return <></>;
59
88
  }
60
89
 
@@ -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,24 @@ 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;
69
75
  }
70
76
 
77
+ export const getMappingColors = (
78
+ mappingsList: string[],
79
+ isDarkMode: boolean,
80
+ currentColorProfile: ColorConfig
81
+ ): mappingColors => {
82
+ const mappingFeatures = mappingsList.map(mapping => extractFeature(mapping));
83
+
84
+ const colors: mappingColors = {};
85
+ Object.entries(mappingFeatures).forEach(([_, feature]) => {
86
+ colors[feature.name] = getColorForFeature(feature.name, isDarkMode, currentColorProfile.colors);
87
+ });
88
+ return colors;
89
+ };
90
+
71
91
  export const IcicleGraphArrow = memo(function IcicleGraphArrow({
72
92
  arrow,
73
93
  total,
@@ -78,7 +98,7 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
78
98
  profileType,
79
99
  navigateTo,
80
100
  sortBy,
81
- mappings,
101
+ flamegraphLoading,
82
102
  }: IcicleGraphArrowProps): React.JSX.Element {
83
103
  const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean>(false);
84
104
  const dispatch = useAppDispatch();
@@ -106,50 +126,55 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
106
126
 
107
127
  const currentSearchString = (selectQueryParam('search_string') as string) ?? '';
108
128
  const {compareMode} = useProfileViewContext();
109
- const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
129
+ // const isColorStackLegendEnabled = selectQueryParam('color_stack_legend') === 'true';
110
130
  const currentColorProfile = useCurrentColorProfile();
111
131
  const colorForSimilarNodes = currentColorProfile.colorForSimilarNodes;
112
132
 
113
133
  const mappingsList = useMemo(() => {
114
- const list =
115
- mappings
116
- ?.map(mapping => {
117
- return getLastItem(mapping) as string;
134
+ // Read the mappings from the dictionary that contains all mapping strings.
135
+ // This is great, as might only have a dozen or so mappings,
136
+ // and don't need to read through all the rows (potentially thousands).
137
+ const mappingsDict: Vector<Dictionary> | null = table.getChild(FIELD_MAPPING_FILE);
138
+ const mappings =
139
+ mappingsDict?.data
140
+ .map(mapping => {
141
+ if (mapping.dictionary == null) {
142
+ return [];
143
+ }
144
+ const len = mapping.dictionary.length;
145
+ const entries: string[] = [];
146
+ for (let i = 0; i < len; i++) {
147
+ const fn = arrowToString(mapping.dictionary.get(i));
148
+ entries.push(getLastItem(fn) ?? '');
149
+ }
150
+ return entries;
118
151
  })
119
152
  .flat() ?? [];
120
153
 
121
154
  // We add a EVERYTHING ELSE mapping to the list.
122
- list.push('');
155
+ mappings.push('');
123
156
 
124
157
  // 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]);
158
+ mappings.sort((a, b) => a.localeCompare(b));
159
+ return mappings;
160
+ }, [table]);
129
161
 
130
162
  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
-
163
+ const colors = getMappingColors(mappingsList, isDarkMode, currentColorProfile);
143
164
  return colors;
144
- }, [mappingsList, isDarkMode, currentColorProfile]);
165
+ }, [isDarkMode, mappingsList, currentColorProfile]);
145
166
 
146
167
  useEffect(() => {
147
168
  if (ref.current != null) {
148
169
  setHeight(ref?.current.getBoundingClientRect().height);
149
170
  }
150
- }, [width]);
171
+ }, [width, flamegraphLoading]);
151
172
 
152
173
  const xScale = useMemo(() => {
174
+ if (total === 0n) {
175
+ return () => 0;
176
+ }
177
+
153
178
  if (width === undefined) {
154
179
  return () => 0;
155
180
  }
@@ -235,31 +260,27 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
235
260
  </svg>
236
261
  );
237
262
  }, [
238
- compareMode,
239
- curPath,
240
- currentSearchString,
263
+ width,
241
264
  height,
242
- isDarkMode,
243
- profileType,
265
+ displayMenu,
266
+ table,
244
267
  mappingColors,
245
268
  setCurPath,
246
- sortBy,
247
- table,
269
+ curPath,
248
270
  total,
249
- width,
250
271
  xScale,
272
+ currentSearchString,
273
+ sortBy,
274
+ isDarkMode,
275
+ compareMode,
276
+ profileType,
251
277
  isContextMenuOpen,
252
- displayMenu,
253
- colorForSimilarNodes,
254
- highlightSimilarStacksPreference,
255
278
  hoveringName,
256
279
  hoveringRow,
280
+ colorForSimilarNodes,
281
+ highlightSimilarStacksPreference,
257
282
  ]);
258
283
 
259
- if (table.numRows === 0 || width === undefined) {
260
- return <></>;
261
- }
262
-
263
284
  return (
264
285
  <>
265
286
  <div onMouseLeave={() => dispatch(setHoveringNode(undefined))}>
@@ -278,13 +299,6 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
278
299
  hideMenu={hideAll}
279
300
  hideBinary={hideBinary}
280
301
  />
281
- {isColorStackLegendEnabled && (
282
- <ColorStackLegend
283
- mappingColors={mappingColors}
284
- navigateTo={navigateTo}
285
- compareMode={compareMode}
286
- />
287
- )}
288
302
  {dockedMetainfo ? (
289
303
  <DockedGraphTooltip
290
304
  table={table}
@@ -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,7 @@ 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';
38
43
 
39
44
  const numberFormatter = new Intl.NumberFormat('en-US');
40
45
 
@@ -55,6 +60,7 @@ interface ProfileIcicleGraphProps {
55
60
  error?: any;
56
61
  isHalfScreen: boolean;
57
62
  mappings?: string[];
63
+ mappingsLoading?: boolean;
58
64
  }
59
65
 
60
66
  const ErrorContent = ({errorMessage}: {errorMessage: string}): JSX.Element => {
@@ -214,10 +220,12 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
214
220
  width,
215
221
  isHalfScreen,
216
222
  mappings,
223
+ mappingsLoading,
217
224
  }: ProfileIcicleGraphProps): JSX.Element {
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';
221
229
 
222
230
  const [storeSortBy = FIELD_FUNCTION_NAME] = useURLState({
223
231
  param: 'sort_by',
@@ -262,20 +270,11 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
262
270
  }, [graph, arrow, filtered, total]);
263
271
 
264
272
  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(
273
+ setActionButtons?.(
275
274
  <div className="flex w-full justify-end gap-2 pb-2">
276
275
  <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 ? (
276
+ {<GroupAndSortActionButtons navigateTo={navigateTo} />}
277
+ {isHalfScreen ? (
279
278
  <IconButton
280
279
  icon={isInvert ? 'ph:sort-ascending' : 'ph:sort-descending'}
281
280
  toolTipText={isInvert ? 'Original Call Stack' : 'Invert Call Stack'}
@@ -336,14 +335,6 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
336
335
  }
337
336
  }, [loading, arrow, graph]);
338
337
 
339
- if (isLoading) {
340
- return (
341
- <div className="h-auto overflow-clip">
342
- <IcicleGraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
343
- </div>
344
- );
345
- }
346
-
347
338
  if (error != null) {
348
339
  onError?.(error);
349
340
 
@@ -354,11 +345,68 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
354
345
  return <ErrorContent errorMessage={capitalizeOnlyFirstLetter(error.message)} />;
355
346
  }
356
347
 
357
- if (graph === undefined && arrow === undefined)
358
- return <div className="mx-auto text-center">No data...</div>;
348
+ // eslint-disable-next-line react-hooks/rules-of-hooks
349
+ const icicleGraph = useMemo(() => {
350
+ if (isLoading) {
351
+ return (
352
+ <div className="h-auto overflow-clip">
353
+ <IcicleGraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
354
+ </div>
355
+ );
356
+ }
359
357
 
360
- if (total === 0n && !loading)
361
- return <div className="mx-auto text-center">Profile has no samples</div>;
358
+ if (graph === undefined && arrow === undefined)
359
+ return <div className="mx-auto text-center">No data...</div>;
360
+
361
+ if (total === 0n && !loading)
362
+ return <div className="mx-auto text-center">Profile has no samples</div>;
363
+
364
+ if (graph !== undefined)
365
+ return (
366
+ <IcicleGraph
367
+ width={width}
368
+ graph={graph}
369
+ total={total}
370
+ filtered={filtered}
371
+ curPath={curPath}
372
+ setCurPath={setNewCurPath}
373
+ profileType={profileType}
374
+ navigateTo={navigateTo}
375
+ />
376
+ );
377
+
378
+ if (arrow !== undefined)
379
+ return (
380
+ <IcicleGraphArrow
381
+ width={width}
382
+ arrow={arrow}
383
+ total={total}
384
+ filtered={filtered}
385
+ curPath={curPath}
386
+ setCurPath={setNewCurPath}
387
+ profileType={profileType}
388
+ navigateTo={navigateTo}
389
+ sortBy={storeSortBy as string}
390
+ flamegraphLoading={isLoading}
391
+ isHalfScreen={isHalfScreen}
392
+ />
393
+ );
394
+ }, [
395
+ isLoading,
396
+ graph,
397
+ arrow,
398
+ total,
399
+ filtered,
400
+ curPath,
401
+ setNewCurPath,
402
+ profileType,
403
+ navigateTo,
404
+ width,
405
+ storeSortBy,
406
+ isHalfScreen,
407
+ isDarkMode,
408
+ loading,
409
+ ]);
362
410
 
363
411
  if (isTrimmed) {
364
412
  console.info(`Trimmed ${trimmedFormatted} (${trimmedPercentage}%) too small values.`);
@@ -374,33 +422,16 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
374
422
  transition={{duration: 0.5}}
375
423
  >
376
424
  {compareMode ? <DiffLegend /> : null}
425
+ {isColorStackLegendEnabled && (
426
+ <ColorStackLegend
427
+ navigateTo={navigateTo}
428
+ compareMode={compareMode}
429
+ mappings={mappings}
430
+ mappingsLoading={mappingsLoading}
431
+ />
432
+ )}
377
433
  <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
- )}
434
+ <>{icicleGraph}</>
404
435
  </div>
405
436
  <p className="my-2 text-xs">
406
437
  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}