@parca/profile 0.19.12 → 0.19.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/GraphTooltipArrow/Content.js +1 -1
  3. package/dist/GraphTooltipArrow/index.js +2 -2
  4. package/dist/MatchersInput/index.d.ts +3 -1
  5. package/dist/MatchersInput/index.d.ts.map +1 -1
  6. package/dist/MatchersInput/index.js +8 -4
  7. package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.d.ts.map +1 -1
  8. package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.js +12 -3
  9. package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenuWrapper.d.ts +1 -1
  10. package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenuWrapper.d.ts.map +1 -1
  11. package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenuWrapper.js +9 -1
  12. package/dist/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.d.ts +1 -0
  13. package/dist/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.d.ts.map +1 -1
  14. package/dist/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.js +7 -3
  15. package/dist/ProfileIcicleGraph/IcicleGraphArrow/MemoizedTooltip.d.ts.map +1 -1
  16. package/dist/ProfileIcicleGraph/IcicleGraphArrow/MemoizedTooltip.js +17 -2
  17. package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.d.ts +2 -0
  18. package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.d.ts.map +1 -1
  19. package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.js +23 -8
  20. package/dist/ProfileIcicleGraph/index.d.ts +3 -1
  21. package/dist/ProfileIcicleGraph/index.d.ts.map +1 -1
  22. package/dist/ProfileIcicleGraph/index.js +6 -4
  23. package/dist/ProfileSelector/QueryControls.d.ts.map +1 -1
  24. package/dist/ProfileSelector/QueryControls.js +1 -1
  25. package/dist/ProfileSelector/index.d.ts.map +1 -1
  26. package/dist/ProfileSelector/index.js +4 -2
  27. package/dist/ProfileView/components/DashboardItems/index.d.ts.map +1 -1
  28. package/dist/ProfileView/components/DashboardItems/index.js +1 -1
  29. package/dist/ProfileView/components/ShareButton/index.d.ts.map +1 -1
  30. package/dist/ProfileView/components/ShareButton/index.js +1 -1
  31. package/dist/ProfileView/components/Toolbars/index.d.ts +0 -2
  32. package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
  33. package/dist/ProfileView/components/Toolbars/index.js +4 -5
  34. package/dist/ProfileView/components/ViewSelector/index.d.ts.map +1 -1
  35. package/dist/ProfileView/components/ViewSelector/index.js +18 -11
  36. package/dist/ProfileView/context/DashboardContext.d.ts.map +1 -1
  37. package/dist/ProfileView/context/DashboardContext.js +5 -0
  38. package/dist/ProfileView/index.d.ts.map +1 -1
  39. package/dist/ProfileView/index.js +4 -3
  40. package/dist/Sandwich/components/CalleesSection.d.ts +1 -2
  41. package/dist/Sandwich/components/CalleesSection.d.ts.map +1 -1
  42. package/dist/Sandwich/components/CalleesSection.js +2 -6
  43. package/dist/Sandwich/components/CallersSection.d.ts +4 -2
  44. package/dist/Sandwich/components/CallersSection.d.ts.map +1 -1
  45. package/dist/Sandwich/components/CallersSection.js +45 -9
  46. package/dist/Sandwich/components/TableSection.js +1 -1
  47. package/dist/Sandwich/index.d.ts +0 -1
  48. package/dist/Sandwich/index.d.ts.map +1 -1
  49. package/dist/Sandwich/index.js +27 -79
  50. package/dist/SimpleMatchers/index.d.ts +2 -0
  51. package/dist/SimpleMatchers/index.d.ts.map +1 -1
  52. package/dist/SimpleMatchers/index.js +16 -6
  53. package/dist/Table/MoreDropdown.d.ts.map +1 -1
  54. package/dist/Table/MoreDropdown.js +1 -2
  55. package/dist/Table/TableContextMenu.d.ts +9 -0
  56. package/dist/Table/TableContextMenu.d.ts.map +1 -0
  57. package/dist/Table/TableContextMenu.js +38 -0
  58. package/dist/Table/TableContextMenuWrapper.d.ts +10 -0
  59. package/dist/Table/TableContextMenuWrapper.d.ts.map +1 -0
  60. package/dist/Table/TableContextMenuWrapper.js +30 -0
  61. package/dist/Table/hooks/useTableConfiguration.d.ts.map +1 -1
  62. package/dist/Table/hooks/useTableConfiguration.js +2 -20
  63. package/dist/Table/index.d.ts.map +1 -1
  64. package/dist/Table/index.js +65 -5
  65. package/dist/ViewMatchers/index.d.ts +2 -0
  66. package/dist/ViewMatchers/index.d.ts.map +1 -1
  67. package/dist/ViewMatchers/index.js +14 -4
  68. package/dist/contexts/MatchersInputLabelsContext.d.ts +3 -1
  69. package/dist/contexts/MatchersInputLabelsContext.d.ts.map +1 -1
  70. package/dist/contexts/MatchersInputLabelsContext.js +3 -3
  71. package/dist/contexts/SimpleMatchersLabelContext.d.ts +3 -1
  72. package/dist/contexts/SimpleMatchersLabelContext.d.ts.map +1 -1
  73. package/dist/contexts/SimpleMatchersLabelContext.js +2 -2
  74. package/dist/styles.css +1 -1
  75. package/package.json +3 -3
  76. package/src/GraphTooltipArrow/Content.tsx +3 -3
  77. package/src/GraphTooltipArrow/index.tsx +2 -2
  78. package/src/MatchersInput/index.tsx +17 -4
  79. package/src/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.tsx +19 -3
  80. package/src/ProfileIcicleGraph/IcicleGraphArrow/ContextMenuWrapper.tsx +10 -2
  81. package/src/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.tsx +19 -2
  82. package/src/ProfileIcicleGraph/IcicleGraphArrow/MemoizedTooltip.tsx +20 -2
  83. package/src/ProfileIcicleGraph/IcicleGraphArrow/index.tsx +40 -6
  84. package/src/ProfileIcicleGraph/index.tsx +20 -2
  85. package/src/ProfileSelector/QueryControls.tsx +6 -0
  86. package/src/ProfileSelector/index.tsx +12 -2
  87. package/src/ProfileView/components/DashboardItems/index.tsx +0 -1
  88. package/src/ProfileView/components/ShareButton/index.tsx +9 -3
  89. package/src/ProfileView/components/Toolbars/index.tsx +7 -23
  90. package/src/ProfileView/components/ViewSelector/index.tsx +20 -11
  91. package/src/ProfileView/context/DashboardContext.tsx +6 -0
  92. package/src/ProfileView/index.tsx +12 -4
  93. package/src/Sandwich/components/CalleesSection.tsx +1 -7
  94. package/src/Sandwich/components/CallersSection.tsx +92 -35
  95. package/src/Sandwich/components/TableSection.tsx +2 -2
  96. package/src/Sandwich/index.tsx +20 -107
  97. package/src/SimpleMatchers/index.tsx +20 -4
  98. package/src/Table/MoreDropdown.tsx +1 -2
  99. package/src/Table/TableContextMenu.tsx +70 -0
  100. package/src/Table/TableContextMenuWrapper.tsx +48 -0
  101. package/src/Table/hooks/useTableConfiguration.tsx +2 -25
  102. package/src/Table/index.tsx +84 -5
  103. package/src/ViewMatchers/index.tsx +17 -3
  104. package/src/contexts/MatchersInputLabelsContext.tsx +10 -2
  105. package/src/contexts/SimpleMatchersLabelContext.tsx +5 -1
@@ -33,6 +33,8 @@ interface MatchersInputProps {
33
33
  runQuery: () => void;
34
34
  currentQuery: Query;
35
35
  profileType: string;
36
+ start?: number;
37
+ end?: number;
36
38
  }
37
39
 
38
40
  export interface ILabelNamesResult {
@@ -55,7 +57,7 @@ export const useLabelNames = (
55
57
  const metadata = useGrpcMetadata();
56
58
 
57
59
  const {data, isLoading, error} = useGrpcQuery<LabelsResponse>({
58
- key: ['labelNames', profileType, match?.join(',')],
60
+ key: ['labelNames', profileType, match?.join(','), start, end],
59
61
  queryFn: async () => {
60
62
  const request: LabelsRequest = {match: match !== undefined ? match : []};
61
63
  if (start !== undefined && end !== undefined) {
@@ -89,14 +91,20 @@ interface UseLabelValues {
89
91
  export const useLabelValues = (
90
92
  client: QueryServiceClient,
91
93
  labelName: string,
92
- profileType: string
94
+ profileType: string,
95
+ start?: number,
96
+ end?: number
93
97
  ): UseLabelValues => {
94
98
  const metadata = useGrpcMetadata();
95
99
 
96
100
  const {data, isLoading, error} = useGrpcQuery<string[]>({
97
- key: ['labelValues', labelName, profileType],
101
+ key: ['labelValues', labelName, profileType, start, end],
98
102
  queryFn: async () => {
99
103
  const request: ValuesRequest = {labelName, match: [], profileType};
104
+ if (start !== undefined && end !== undefined) {
105
+ request.start = millisToProtoTimestamp(start);
106
+ request.end = millisToProtoTimestamp(end);
107
+ }
100
108
  const {response} = await client.values(request, {meta: metadata});
101
109
  return sanitizeLabelValue(response.labelValues);
102
110
  },
@@ -321,7 +329,12 @@ const MatchersInput = ({
321
329
 
322
330
  export default function MatchersInputWithProvider(props: MatchersInputProps): JSX.Element {
323
331
  return (
324
- <LabelsProvider queryClient={props.queryClient} profileType={props.profileType}>
332
+ <LabelsProvider
333
+ queryClient={props.queryClient}
334
+ profileType={props.profileType}
335
+ start={props.start}
336
+ end={props.end}
337
+ >
325
338
  <MatchersInput {...props} />
326
339
  </LabelsProvider>
327
340
  );
@@ -13,6 +13,7 @@
13
13
 
14
14
  import {Icon} from '@iconify/react';
15
15
  import {Table} from 'apache-arrow';
16
+ import cx from 'classnames';
16
17
  import {Item, Menu, Separator, Submenu} from 'react-contexify';
17
18
  import {Tooltip} from 'react-tooltip';
18
19
 
@@ -144,7 +145,15 @@ const ContextMenu = ({
144
145
  const nonEmptyValuesToCopy = valuesToCopy.filter(({value}) => value !== '');
145
146
 
146
147
  return (
147
- <Menu id={menuId} theme={isDarkMode ? 'dark' : ''} className="w-[250px]">
148
+ <Menu
149
+ id={menuId}
150
+ theme={isDarkMode ? 'dark' : ''}
151
+ className={cx(
152
+ dashboardItems.includes('sandwich')
153
+ ? 'min-w-[350px] w-[350px]'
154
+ : 'min-w-[260px] w-fit-content'
155
+ )}
156
+ >
148
157
  <Item
149
158
  id="view-source-file"
150
159
  onClick={handleViewSourceFile}
@@ -181,14 +190,21 @@ const ContextMenu = ({
181
190
  <Item
182
191
  id="show-in-sandwich"
183
192
  onClick={() => {
193
+ if (dashboardItems.includes('sandwich')) {
194
+ setSandwichFunctionName(functionName);
195
+ return;
196
+ }
197
+
184
198
  setSandwichFunctionName(functionName);
185
- setDashboardItems(['sandwich']);
199
+ setDashboardItems([...dashboardItems, 'sandwich']);
186
200
  }}
187
201
  >
188
202
  <div className="flex w-full items-center gap-2">
189
203
  <Icon icon="tdesign:sandwich-filled" />
190
204
  <div className="relative">
191
- Show in sandwich
205
+ {dashboardItems.includes('sandwich')
206
+ ? 'Focus sandwich on this frame.'
207
+ : 'Show in sandwich'}
192
208
  <span className="absolute top-[-2px] text-xs lowercase text-red-500">
193
209
  &nbsp;alpha
194
210
  </span>
@@ -34,15 +34,23 @@ interface ContextMenuWrapperProps {
34
34
  }
35
35
 
36
36
  export interface ContextMenuWrapperRef {
37
- setRow: (row: number) => void;
37
+ setRow: (row: number, callback?: () => void) => void;
38
38
  }
39
39
 
40
40
  const ContextMenuWrapper = forwardRef<ContextMenuWrapperRef, ContextMenuWrapperProps>(
41
41
  (props, ref) => {
42
+ // Fix for race condition: Always render ContextMenu to maintain component tree stability
43
+ // but use callback timing to ensure correct data is available when menu shows
42
44
  const [row, setRow] = useState(0);
43
45
 
44
46
  useImperativeHandle(ref, () => ({
45
- setRow,
47
+ setRow: (newRow: number, callback?: () => void) => {
48
+ setRow(newRow);
49
+ // Execute callback after state update using requestAnimationFrame
50
+ if (callback != null) {
51
+ requestAnimationFrame(callback);
52
+ }
53
+ },
46
54
  }));
47
55
 
48
56
  return <ContextMenu {...props} row={row} />;
@@ -64,6 +64,7 @@ export interface IcicleNodeProps {
64
64
  isFlamegraph?: boolean;
65
65
  isSandwich?: boolean;
66
66
  maxDepth?: number;
67
+ effectiveDepth?: number;
67
68
  tooltipId?: string;
68
69
 
69
70
  // Hovering row must only ever be used for highlighting similar nodes, otherwise it will cause performance issues as it causes the full iciclegraph to get rerendered every time the hovering row changes.
@@ -102,6 +103,7 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
102
103
  isFlamegraph = false,
103
104
  isSandwich = false,
104
105
  maxDepth = 0,
106
+ effectiveDepth,
105
107
  tooltipId = 'default',
106
108
  }: IcicleNodeProps): React.JSX.Element {
107
109
  // get the columns to read from
@@ -115,12 +117,15 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
115
117
  const tsColumn = table.getChild(FIELD_TIMESTAMP);
116
118
 
117
119
  // get the actual values from the columns
120
+ const binaries = useAppSelector(selectBinaries);
121
+
118
122
  const mappingFile: string | null = arrowToString(mappingColumn?.get(row));
119
123
  const functionName: string | null = arrowToString(functionNameColumn?.get(row));
120
124
  const cumulative = cumulativeColumn?.get(row) !== null ? BigInt(cumulativeColumn?.get(row)) : 0n;
121
125
  const diff: bigint | null = diffColumn?.get(row) !== null ? BigInt(diffColumn?.get(row)) : null;
122
126
  const filename: string | null = arrowToString(filenameColumn?.get(row));
123
127
  const depth: number = depthColumn?.get(row) ?? 0;
128
+
124
129
  const valueOffset: bigint =
125
130
  valueOffsetColumn?.get(row) !== null && valueOffsetColumn?.get(row) !== undefined
126
131
  ? BigInt(valueOffsetColumn?.get(row))
@@ -136,7 +141,6 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
136
141
  const shouldBeHighlighted =
137
142
  functionName != null && hoveringName != null && functionName === hoveringName;
138
143
 
139
- const binaries = useAppSelector(selectBinaries);
140
144
  const colorResult = useNodeColor({
141
145
  isDarkMode: darkMode,
142
146
  compareMode,
@@ -145,6 +149,7 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
145
149
  colorsMap,
146
150
  colorAttribute,
147
151
  });
152
+
148
153
  const name = useMemo(() => {
149
154
  return row === 0 ? 'root' : nodeLabel(table, row, binaries.length > 1);
150
155
  }, [table, row, binaries]);
@@ -156,6 +161,11 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
156
161
  return {isHighlightEnabled: true, isHighlighted: isSearchMatch(searchString, name)};
157
162
  }, [searchString, name]);
158
163
 
164
+ // Hide frames beyond effective depth limit
165
+ if (effectiveDepth !== undefined && depth > effectiveDepth) {
166
+ return <></>;
167
+ }
168
+
159
169
  const selectionOffset =
160
170
  valueOffsetColumn?.get(selectedRow) !== null &&
161
171
  valueOffsetColumn?.get(selectedRow) !== undefined
@@ -241,7 +251,14 @@ export const IcicleNode = React.memo(function IcicleNodeNoMemo({
241
251
  return depth * height;
242
252
  };
243
253
 
244
- const y = calculateY(isFlamegraph, isSandwich, isIcicleChart, maxDepth, depth, height);
254
+ const y = calculateY(
255
+ isFlamegraph,
256
+ isSandwich,
257
+ isIcicleChart,
258
+ effectiveDepth ?? maxDepth,
259
+ depth,
260
+ height
261
+ );
245
262
 
246
263
  return (
247
264
  <>
@@ -39,11 +39,24 @@ export const MemoizedTooltip = memo(function MemoizedTooltip({
39
39
  };
40
40
 
41
41
  const eventName = `icicle-tooltip-update-${tooltipId}`;
42
- window.addEventListener(eventName as any, handleTooltipUpdate as any);
42
+
43
+ // Delay to ensure all DOM updates and React renders are complete
44
+ // This fixes the race condition in sandwich view
45
+ const timeoutId = setTimeout(() => {
46
+ window.addEventListener(eventName as any, handleTooltipUpdate as any);
47
+ }, 200);
48
+
43
49
  return () => {
50
+ clearTimeout(timeoutId);
44
51
  window.removeEventListener(eventName as any, handleTooltipUpdate as any);
45
52
  };
46
- }, [tooltipId]);
53
+ }, [tooltipId, table]);
54
+
55
+ // Re-render when contextElement becomes available (fixes sandwich view timing issue)
56
+ useEffect(() => {
57
+ // Force re-render when contextElement transitions from null to valid element
58
+ // This ensures tooltips work immediately in sandwich view
59
+ }, [contextElement, tooltipId]);
47
60
 
48
61
  if (dockedMetainfo) {
49
62
  return (
@@ -63,6 +76,11 @@ export const MemoizedTooltip = memo(function MemoizedTooltip({
63
76
  return null;
64
77
  }
65
78
 
79
+ // Fix for sandwich view tooltip issue: Don't render tooltip if contextElement is null
80
+ // This happens when the SVG ref isn't ready yet during initial sandwich view render
81
+ if (contextElement === null) {
82
+ return null;
83
+ }
66
84
  return (
67
85
  <GraphTooltipArrow contextElement={contextElement}>
68
86
  <GraphTooltipArrowContent
@@ -11,7 +11,15 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import React, {memo, useCallback, useMemo, useRef, useState} from 'react';
14
+ import React, {
15
+ memo,
16
+ useCallback,
17
+ useDeferredValue,
18
+ useEffect,
19
+ useMemo,
20
+ useRef,
21
+ useState,
22
+ } from 'react';
15
23
 
16
24
  import {Dictionary, Table, Vector, tableFromIPC} from 'apache-arrow';
17
25
  import {useContextMenu} from 'react-contexify';
@@ -77,6 +85,8 @@ interface IcicleGraphArrowProps {
77
85
  isFlamegraph?: boolean;
78
86
  isSandwich?: boolean;
79
87
  tooltipId?: string;
88
+ maxFrameCount?: number;
89
+ isExpanded?: boolean;
80
90
  }
81
91
 
82
92
  export const getMappingColors = (
@@ -135,6 +145,8 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
135
145
  isFlamegraph = false,
136
146
  isSandwich = false,
137
147
  tooltipId = 'default',
148
+ maxFrameCount,
149
+ isExpanded = false,
138
150
  }: IcicleGraphArrowProps): React.JSX.Element {
139
151
  const [highlightSimilarStacksPreference] = useUserPreference<boolean>(
140
152
  USER_PREFERENCES.HIGHLIGHT_SIMILAR_STACKS.key
@@ -148,6 +160,12 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
148
160
  }, [arrow]);
149
161
  const svg = useRef(null);
150
162
 
163
+ const [svgElement, setSvgElement] = useState<SVGSVGElement | null>(null);
164
+
165
+ useEffect(() => {
166
+ setSvgElement(svg.current);
167
+ }, [tooltipId]);
168
+
151
169
  const [binaryFrameFilter, setBinaryFrameFilter] = useURLState('binary_frame_filter');
152
170
 
153
171
  const [currentSearchString] = useURLState('search_string');
@@ -215,9 +233,14 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
215
233
  });
216
234
  const displayMenu = useCallback(
217
235
  (e: React.MouseEvent, row: number): void => {
218
- contextMenuRef.current?.setRow(row);
219
- show({
220
- event: e,
236
+ e.preventDefault();
237
+ // Race condition fix: Use callback to ensure context menu shows only after
238
+ // row state has been updated and propagated through the hook chain.
239
+ // This prevents empty function names on first click.
240
+ contextMenuRef.current?.setRow(row, () => {
241
+ show({
242
+ event: e,
243
+ });
221
244
  });
222
245
  },
223
246
  [show]
@@ -255,7 +278,17 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
255
278
 
256
279
  const depthColumn = table.getChild(FIELD_DEPTH);
257
280
  const maxDepth = getMaxDepth(depthColumn);
258
- const height = isSandwich ? maxDepth * RowHeight : (maxDepth + 1) * RowHeight;
281
+
282
+ // Apply frame limit if maxFrameCount is provided and not expanded
283
+ const effectiveDepth =
284
+ maxFrameCount !== undefined && !isExpanded ? Math.min(maxDepth, maxFrameCount) : maxDepth;
285
+
286
+ // Use deferred value to prevent UI blocking when expanding frames
287
+ const deferredEffectiveDepth = useDeferredValue(effectiveDepth);
288
+
289
+ const height = isSandwich
290
+ ? deferredEffectiveDepth * RowHeight
291
+ : (deferredEffectiveDepth + 1) * RowHeight;
259
292
 
260
293
  // To find the selected row, we must walk the current path and look at which
261
294
  // children of the current frame matches the path element exactly. Until the
@@ -307,7 +340,7 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
307
340
  profileType={profileType}
308
341
  isSandwich={isSandwich}
309
342
  />
310
- <MemoizedTooltip contextElement={svg.current} dockedMetainfo={dockedMetainfo} />
343
+ <MemoizedTooltip contextElement={svgElement} dockedMetainfo={dockedMetainfo} />
311
344
  <svg
312
345
  className="font-robotoMono"
313
346
  width={width}
@@ -344,6 +377,7 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
344
377
  isFlamegraph={isFlamegraph}
345
378
  isSandwich={isSandwich}
346
379
  maxDepth={maxDepth}
380
+ effectiveDepth={deferredEffectiveDepth}
347
381
  tooltipId={tooltipId}
348
382
  />
349
383
  ))}
@@ -18,7 +18,12 @@ import {AnimatePresence, motion} from 'framer-motion';
18
18
  import {useMeasure} from 'react-use';
19
19
 
20
20
  import {FlamegraphArrow} from '@parca/client';
21
- import {IcicleGraphSkeleton, useParcaContext, useURLState} from '@parca/components';
21
+ import {
22
+ FlamegraphSkeleton,
23
+ IcicleGraphSkeleton,
24
+ useParcaContext,
25
+ useURLState,
26
+ } from '@parca/components';
22
27
  import {ProfileType} from '@parca/parser';
23
28
  import {capitalizeOnlyFirstLetter, divide} from '@parca/utilities';
24
29
 
@@ -53,6 +58,8 @@ interface ProfileIcicleGraphProps {
53
58
  isSandwichIcicleGraph?: boolean;
54
59
  isFlamegraph?: boolean;
55
60
  tooltipId?: string;
61
+ maxFrameCount?: number;
62
+ isExpanded?: boolean;
56
63
  }
57
64
 
58
65
  const ErrorContent = ({errorMessage}: {errorMessage: string | ReactNode}): JSX.Element => {
@@ -88,6 +95,8 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
88
95
  isSandwichIcicleGraph = false,
89
96
  isFlamegraph = false,
90
97
  tooltipId,
98
+ maxFrameCount,
99
+ isExpanded = false,
91
100
  }: ProfileIcicleGraphProps): JSX.Element {
92
101
  const {onError, authenticationErrorMessage, isDarkMode, iciclechartHelpText} = useParcaContext();
93
102
  const {compareMode} = useProfileViewContext();
@@ -184,10 +193,15 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
184
193
  ? validateIcicleChartQuery(profileSource as MergedProfileSource)
185
194
  : {isValid: true, isNonDelta: false, isDurationTooLong: false};
186
195
  const isInvalidIcicleChartQuery = isIcicleChart && !isIcicleChartValid;
196
+
187
197
  if (isLoading && !isInvalidIcicleChartQuery) {
188
198
  return (
189
199
  <div className="h-auto overflow-clip">
190
- <IcicleGraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
200
+ {isFlamegraph ? (
201
+ <FlamegraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
202
+ ) : (
203
+ <IcicleGraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
204
+ )}
191
205
  </div>
192
206
  );
193
207
  }
@@ -268,6 +282,8 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
268
282
  isFlamegraph={isFlamegraph}
269
283
  isSandwich={isSandwichIcicleGraph}
270
284
  tooltipId={tooltipId}
285
+ maxFrameCount={maxFrameCount}
286
+ isExpanded={isExpanded}
271
287
  />
272
288
  </div>
273
289
  </div>
@@ -295,6 +311,8 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
295
311
  effectiveCurPathArrow,
296
312
  setCurPathArrowWrapper,
297
313
  tooltipId,
314
+ maxFrameCount,
315
+ isExpanded,
298
316
  ]);
299
317
 
300
318
  useEffect(() => {
@@ -148,6 +148,8 @@ export function QueryControls({
148
148
  runQuery={setQueryExpression}
149
149
  currentQuery={query}
150
150
  queryClient={queryClient}
151
+ start={timeRangeSelection.getFromMs()}
152
+ end={timeRangeSelection.getToMs()}
151
153
  />
152
154
  ) : advancedModeForQueryBrowser ? (
153
155
  <MatchersInput
@@ -156,6 +158,8 @@ export function QueryControls({
156
158
  currentQuery={query}
157
159
  profileType={selectedProfileName}
158
160
  queryClient={queryClient}
161
+ start={timeRangeSelection.getFromMs()}
162
+ end={timeRangeSelection.getToMs()}
159
163
  />
160
164
  ) : (
161
165
  <SimpleMatchers
@@ -166,6 +170,8 @@ export function QueryControls({
166
170
  profileType={selectedProfileName}
167
171
  queryBrowserRef={queryBrowserRef}
168
172
  queryClient={queryClient}
173
+ start={timeRangeSelection.getFromMs()}
174
+ end={timeRangeSelection.getToMs()}
169
175
  />
170
176
  )}
171
177
  </div>
@@ -170,10 +170,20 @@ const ProfileSelector = ({
170
170
  return Query.parse(querySelection.expression).profileType();
171
171
  }, [querySelection.expression]);
172
172
 
173
- const {loading: labelNamesLoading, result} = useLabelNames(queryClient, profileType.toString());
173
+ const from = timeRangeSelection.getFromMs();
174
+ const to = timeRangeSelection.getToMs();
175
+
176
+ const {loading: labelNamesLoading, result} = useLabelNames(
177
+ queryClient,
178
+ profileType.toString(),
179
+ from,
180
+ to
181
+ );
174
182
  const {loading: selectedLabelNamesLoading, result: selectedLabelNamesResult} = useLabelNames(
175
183
  queryClient,
176
- selectedProfileType.toString()
184
+ selectedProfileType.toString(),
185
+ from,
186
+ to
177
187
  );
178
188
 
179
189
  const labels = useMemo(() => {
@@ -155,7 +155,6 @@ export const getDashboardItem = ({
155
155
  data={topTableData.arrow?.record}
156
156
  unit={topTableData.unit}
157
157
  profileType={profileSource?.ProfileType()}
158
- isHalfScreen={isHalfScreen}
159
158
  metadataMappingFiles={flamegraphData.metadataMappingFiles}
160
159
  profileSource={profileSource}
161
160
  queryClient={queryClient}
@@ -182,11 +182,17 @@ const ShareButton = ({
182
182
  element={
183
183
  <Button
184
184
  variant="neutral"
185
- className="flex items-center gap-2"
185
+ className="flex items-center gap-2 pr-[1.7rem]"
186
186
  id="h-share-dropdown-button"
187
187
  >
188
- <Icon icon="material-symbols:share" className="h-4 w-4" />
189
- Share
188
+ <div className="flex items-center gap-2">
189
+ <Icon icon="material-symbols:share" className="w-4 h-4" />
190
+
191
+ <span>Share</span>
192
+ </div>
193
+ <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2 text-gray-400">
194
+ <Icon icon="heroicons:chevron-down-20-solid" aria-hidden="true" />
195
+ </div>
190
196
  </Button>
191
197
  }
192
198
  >
@@ -16,7 +16,7 @@ import {FC} from 'react';
16
16
  import {Icon} from '@iconify/react';
17
17
 
18
18
  import {QueryServiceClient} from '@parca/client';
19
- import {Button, UserPreferencesModal} from '@parca/components';
19
+ import {Button} from '@parca/components';
20
20
  import {ProfileType} from '@parca/parser';
21
21
 
22
22
  import {CurrentPathFrame} from '../../../ProfileIcicleGraph/IcicleGraphArrow/utils';
@@ -52,8 +52,6 @@ export interface VisualisationToolbarProps {
52
52
  setGroupByLabels: (labels: string[]) => void;
53
53
  showVisualizationSelector?: boolean;
54
54
  sandwichFunctionName?: string;
55
- setSandwichFunctionName: (sandwichFunctionName: string | undefined) => void;
56
- resetSandwichFunctionName: () => void;
57
55
  }
58
56
 
59
57
  export interface TableToolbarProps {
@@ -150,7 +148,6 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
150
148
  groupByLabels,
151
149
  setGroupByLabels,
152
150
  profileType,
153
- preferencesModal,
154
151
  profileSource,
155
152
  queryClient,
156
153
  onDownloadPProf,
@@ -163,17 +160,13 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
163
160
  currentSearchString,
164
161
  clearSelection,
165
162
  showVisualizationSelector = true,
166
- resetSandwichFunctionName,
167
- sandwichFunctionName,
168
163
  }) => {
169
164
  const {dashboardItems} = useDashboard();
170
165
 
171
166
  const isTableViz = dashboardItems?.includes('table');
172
167
  const isTableVizOnly = dashboardItems?.length === 1 && isTableViz;
173
168
  const isGraphViz = dashboardItems?.includes('icicle');
174
- const isSandwichIcicleGraphViz = dashboardItems?.includes('sandwich');
175
-
176
- const isTableView = isTableVizOnly || isSandwichIcicleGraphViz;
169
+ const isGraphVizOnly = dashboardItems?.length === 1 && isGraphViz;
177
170
 
178
171
  const req = profileSource?.QueryRequest();
179
172
  if (req !== null && req !== undefined) {
@@ -186,7 +179,7 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
186
179
  <>
187
180
  <div className="flex w-full justify-between items-end">
188
181
  <div className="flex gap-3 items-end">
189
- {!isTableView && (
182
+ {isGraphViz && (
190
183
  <>
191
184
  <GroupByDropdown
192
185
  groupBy={groupBy}
@@ -203,13 +196,12 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
203
196
  {profileViewExternalSubActions != null ? profileViewExternalSubActions : null}
204
197
  </div>
205
198
  <div className="flex gap-3">
206
- {preferencesModal === true && <UserPreferencesModal />}
207
199
  <MultiLevelDropdown
208
200
  groupBy={groupBy}
209
201
  toggleGroupBy={toggleGroupBy}
210
202
  profileType={profileType}
211
203
  onSelect={() => {}}
212
- isTableVizOnly={isTableView}
204
+ isTableVizOnly={isTableVizOnly}
213
205
  />
214
206
 
215
207
  <ShareButton
@@ -224,13 +216,14 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
224
216
  {showVisualizationSelector ? <ViewSelector profileSource={profileSource} /> : null}
225
217
  </div>
226
218
  </div>
227
- {isGraphViz && !isTableViz && (
219
+
220
+ {isGraphVizOnly && (
228
221
  <>
229
222
  <Divider />
230
223
  <IcicleGraphToolbar curPath={curPath} setNewCurPath={setNewCurPath} />
231
224
  </>
232
225
  )}
233
- {isTableViz && !isGraphViz && (
226
+ {isTableVizOnly && (
234
227
  <>
235
228
  <Divider />
236
229
  <TableToolbar
@@ -242,15 +235,6 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
242
235
  />
243
236
  </>
244
237
  )}
245
- {isSandwichIcicleGraphViz && (
246
- <>
247
- <Divider />
248
- <SandwichIcicleGraphToolbar
249
- resetSandwichFunctionName={resetSandwichFunctionName}
250
- sandwichFunctionName={sandwichFunctionName}
251
- />
252
- </>
253
- )}
254
238
  </>
255
239
  );
256
240
  };
@@ -29,6 +29,7 @@ const ViewSelector = ({profileSource}: Props): JSX.Element => {
29
29
  alwaysReturnArray: true,
30
30
  }
31
31
  );
32
+ const [, setSandwichFunctionName] = useURLState<string | undefined>('sandwich_function_name');
32
33
  const {enableSourcesView, enableSandwichView} = useParcaContext();
33
34
 
34
35
  const allItems: Array<{
@@ -38,8 +39,8 @@ const ViewSelector = ({profileSource}: Props): JSX.Element => {
38
39
  supportingText?: string;
39
40
  disabledText?: string;
40
41
  }> = [
41
- {key: 'table', label: 'Table', canBeSelected: !dashboardItems.includes('table')},
42
42
  {key: 'icicle', label: 'icicle', canBeSelected: !dashboardItems.includes('icicle')},
43
+ {key: 'table', label: 'Table', canBeSelected: !dashboardItems.includes('table')},
43
44
  {
44
45
  key: 'iciclechart',
45
46
  label: (
@@ -105,14 +106,8 @@ const ViewSelector = ({profileSource}: Props): JSX.Element => {
105
106
  }): InnerAction | undefined => {
106
107
  if (dashboardItems.length === 1 && item.key === dashboardItems[0]) return undefined;
107
108
 
108
- // For sandwich view, return a no-op action
109
- if (item.key === 'sandwich') {
110
- return {
111
- text: 'Add Panel',
112
- onClick: () => {},
113
- isDisabled: true, // Custom property to control button state
114
- };
115
- }
109
+ // If we already have 2 panels and this item isn't selected, don't show any action
110
+ if (dashboardItems.length >= 2 && !dashboardItems.includes(item.key)) return undefined;
116
111
 
117
112
  return {
118
113
  text:
@@ -120,12 +115,20 @@ const ViewSelector = ({profileSource}: Props): JSX.Element => {
120
115
  ? 'Add Panel'
121
116
  : item.canBeSelected
122
117
  ? 'Add Panel'
123
- : 'Close Panel',
118
+ : dashboardItems.includes(item.key)
119
+ ? 'Close Panel'
120
+ : 'Add Panel',
124
121
  onClick: () => {
125
122
  if (item.canBeSelected) {
126
123
  setDashboardItems([...dashboardItems, item.key]);
127
124
  } else {
128
- setDashboardItems(dashboardItems.filter(v => v !== item.key));
125
+ const newDashboardItems = dashboardItems.filter(v => v !== item.key);
126
+ setDashboardItems(newDashboardItems);
127
+
128
+ // Reset sandwich function name when removing sandwich panel
129
+ if (item.key === 'sandwich') {
130
+ setSandwichFunctionName(undefined);
131
+ }
129
132
  }
130
133
  },
131
134
  isDisabled: dashboardItems.length === 1 && dashboardItems.includes('sandwich'),
@@ -142,6 +145,12 @@ const ViewSelector = ({profileSource}: Props): JSX.Element => {
142
145
 
143
146
  const onSelection = (value: string): void => {
144
147
  const isOnlyChart = dashboardItems.length === 1;
148
+
149
+ if (isOnlyChart && value === 'sandwich') {
150
+ setDashboardItems([...dashboardItems, value]);
151
+ return;
152
+ }
153
+
145
154
  if (isOnlyChart) {
146
155
  setDashboardItems([value]);
147
156
  return;
@@ -30,10 +30,16 @@ export const DashboardProvider: FC<PropsWithChildren> = ({children}) => {
30
30
  const [dashboardItems, setDashboardItems] = useURLState<string[]>('dashboard_items', {
31
31
  alwaysReturnArray: true,
32
32
  });
33
+ const [, setSandwichFunctionName] = useURLState<string | undefined>('sandwich_function_name');
33
34
 
34
35
  const handleClosePanel = (visualizationType: VisualizationType): void => {
35
36
  const newDashboardItems = dashboardItems.filter(item => item !== visualizationType);
36
37
  setDashboardItems(newDashboardItems);
38
+
39
+ // Reset sandwich function name when closing sandwich panel
40
+ if (visualizationType === 'sandwich') {
41
+ setSandwichFunctionName(undefined);
42
+ }
37
43
  };
38
44
 
39
45
  const isMultiPanelView = dashboardItems.length > 1;