@parca/profile 0.19.140 → 0.19.143

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 (182) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/dist/GraphTooltipArrow/Content.js +224 -30
  3. package/dist/GraphTooltipArrow/DockedGraphTooltip/index.js +192 -33
  4. package/dist/GraphTooltipArrow/ExpandOnHoverValue.js +53 -3
  5. package/dist/GraphTooltipArrow/index.d.ts.map +1 -1
  6. package/dist/GraphTooltipArrow/index.js +86 -56
  7. package/dist/GraphTooltipArrow/useGraphTooltip/index.js +37 -37
  8. package/dist/GraphTooltipArrow/useGraphTooltipMetaInfo/index.js +94 -68
  9. package/dist/MatchersInput/SuggestionItem.js +91 -12
  10. package/dist/MatchersInput/SuggestionsList.d.ts +2 -1
  11. package/dist/MatchersInput/SuggestionsList.d.ts.map +1 -1
  12. package/dist/MatchersInput/SuggestionsList.js +371 -157
  13. package/dist/MatchersInput/SuggestionsList.test.d.ts +2 -0
  14. package/dist/MatchersInput/SuggestionsList.test.d.ts.map +1 -0
  15. package/dist/MatchersInput/index.js +308 -115
  16. package/dist/MetricsCircle/index.js +39 -3
  17. package/dist/MetricsGraph/MetricsContextMenu/index.js +119 -19
  18. package/dist/MetricsGraph/MetricsInfoPanel/index.js +81 -20
  19. package/dist/MetricsGraph/MetricsTooltip/index.d.ts.map +1 -1
  20. package/dist/MetricsGraph/MetricsTooltip/index.js +107 -74
  21. package/dist/MetricsGraph/index.js +552 -203
  22. package/dist/MetricsGraph/useMetricsGraphDimensions.js +46 -25
  23. package/dist/MetricsGraph/utils/colorMapping.js +24 -17
  24. package/dist/MetricsSeries/index.js +70 -7
  25. package/dist/PreSelectedMatchers/index.d.ts.map +1 -1
  26. package/dist/PreSelectedMatchers/index.js +249 -102
  27. package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts.map +1 -1
  28. package/dist/ProfileExplorer/ProfileExplorerCompare.js +240 -45
  29. package/dist/ProfileExplorer/ProfileExplorerSingle.js +98 -11
  30. package/dist/ProfileExplorer/index.js +183 -32
  31. package/dist/ProfileFlameChart/SamplesStrips/SamplesGraph/index.js +333 -148
  32. package/dist/ProfileFlameChart/SamplesStrips/SamplesStrips.stories.js +69 -35
  33. package/dist/ProfileFlameChart/SamplesStrips/index.js +645 -134
  34. package/dist/ProfileFlameChart/SamplesStrips/labelSetUtils.js +114 -55
  35. package/dist/ProfileFlameChart/index.js +260 -126
  36. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.js +283 -85
  37. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenuWrapper.js +56 -20
  38. package/dist/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.js +211 -140
  39. package/dist/ProfileFlameGraph/FlameGraphArrow/MemoizedTooltip.js +133 -38
  40. package/dist/ProfileFlameGraph/FlameGraphArrow/MiniMap.js +261 -216
  41. package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.d.ts.map +1 -1
  42. package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.js +71 -45
  43. package/dist/ProfileFlameGraph/FlameGraphArrow/TooltipContext.d.ts.map +1 -1
  44. package/dist/ProfileFlameGraph/FlameGraphArrow/TooltipContext.js +58 -28
  45. package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.d.ts.map +1 -1
  46. package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.js +59 -8
  47. package/dist/ProfileFlameGraph/FlameGraphArrow/index.js +396 -179
  48. package/dist/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.d.ts.map +1 -1
  49. package/dist/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.js +68 -50
  50. package/dist/ProfileFlameGraph/FlameGraphArrow/useMappingList.js +62 -38
  51. package/dist/ProfileFlameGraph/FlameGraphArrow/useNodeColor.js +14 -6
  52. package/dist/ProfileFlameGraph/FlameGraphArrow/useScrollViewport.js +124 -82
  53. package/dist/ProfileFlameGraph/FlameGraphArrow/useVisibleNodes.js +160 -98
  54. package/dist/ProfileFlameGraph/FlameGraphArrow/useZoom.js +232 -112
  55. package/dist/ProfileFlameGraph/FlameGraphArrow/utils.js +137 -114
  56. package/dist/ProfileFlameGraph/benchmarks/benchdata/populateData.js +85 -0
  57. package/dist/ProfileFlameGraph/index.js +322 -147
  58. package/dist/ProfileMetricsGraph/hooks/useQueryRange.js +140 -32
  59. package/dist/ProfileMetricsGraph/index.js +515 -256
  60. package/dist/ProfileSelector/CompareButton.js +132 -12
  61. package/dist/ProfileSelector/MetricsGraphSection.js +228 -63
  62. package/dist/ProfileSelector/index.d.ts +1 -1
  63. package/dist/ProfileSelector/index.d.ts.map +1 -1
  64. package/dist/ProfileSelector/index.js +734 -142
  65. package/dist/ProfileSelector/useAutoQuerySelector.d.ts +1 -3
  66. package/dist/ProfileSelector/useAutoQuerySelector.d.ts.map +1 -1
  67. package/dist/ProfileSelector/useAutoQuerySelector.js +280 -132
  68. package/dist/ProfileSource.js +230 -163
  69. package/dist/ProfileTypeSelector/index.js +214 -125
  70. package/dist/ProfileView/components/ActionButtons/GroupByDropdown.js +50 -4
  71. package/dist/ProfileView/components/ActionButtons/SortByDropdown.js +137 -32
  72. package/dist/ProfileView/components/ColorStackLegend.js +182 -54
  73. package/dist/ProfileView/components/DashboardItems/index.js +87 -28
  74. package/dist/ProfileView/components/DashboardLayout/index.js +108 -16
  75. package/dist/ProfileView/components/DiffLegend.js +172 -29
  76. package/dist/ProfileView/components/GroupByLabelsDropdown/index.js +199 -55
  77. package/dist/ProfileView/components/InvertCallStack/index.js +97 -9
  78. package/dist/ProfileView/components/ProfileFilters/filterPresets.js +260 -315
  79. package/dist/ProfileView/components/ProfileFilters/index.js +518 -215
  80. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.js +370 -306
  81. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +191 -118
  82. package/dist/ProfileView/components/ProfileHeader/index.js +105 -11
  83. package/dist/ProfileView/components/ShareButton/ResultBox.js +119 -16
  84. package/dist/ProfileView/components/ShareButton/index.js +352 -62
  85. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.d.ts.map +1 -1
  86. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +664 -192
  87. package/dist/ProfileView/components/Toolbars/SwitchMenuItem.js +94 -7
  88. package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.js +196 -155
  89. package/dist/ProfileView/components/Toolbars/index.js +441 -21
  90. package/dist/ProfileView/components/ViewSelector/Dropdown.js +233 -22
  91. package/dist/ProfileView/components/ViewSelector/index.js +186 -82
  92. package/dist/ProfileView/components/VisualizationContainer/index.d.ts.map +1 -1
  93. package/dist/ProfileView/components/VisualizationContainer/index.js +52 -7
  94. package/dist/ProfileView/components/VisualizationPanel.js +185 -8
  95. package/dist/ProfileView/context/DashboardContext.js +74 -26
  96. package/dist/ProfileView/context/ProfileViewContext.js +56 -15
  97. package/dist/ProfileView/hooks/useAutoSelectDimension.js +71 -41
  98. package/dist/ProfileView/hooks/useProfileMetadata.js +50 -18
  99. package/dist/ProfileView/hooks/useResetFlameGraphState.js +31 -10
  100. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.js +71 -27
  101. package/dist/ProfileView/hooks/useResetStateOnSeriesChange.js +53 -17
  102. package/dist/ProfileView/hooks/useVisualizationState.js +229 -69
  103. package/dist/ProfileView/index.js +383 -45
  104. package/dist/ProfileView/types/visualization.js +1 -13
  105. package/dist/ProfileView/utils/colorUtils.js +8 -7
  106. package/dist/ProfileViewWithData.js +319 -225
  107. package/dist/QueryControls/index.js +418 -47
  108. package/dist/Sandwich/components/CalleesSection.js +54 -4
  109. package/dist/Sandwich/components/CallersSection.js +97 -27
  110. package/dist/Sandwich/components/TableSection.js +77 -4
  111. package/dist/Sandwich/index.js +125 -12
  112. package/dist/Sandwich/utils/processRowData.js +48 -39
  113. package/dist/SelectWithRefresh/index.js +102 -28
  114. package/dist/SimpleMatchers/Select.js +520 -187
  115. package/dist/SimpleMatchers/index.js +590 -288
  116. package/dist/SourceView/Highlighter.js +230 -70
  117. package/dist/SourceView/LineNo.js +72 -17
  118. package/dist/SourceView/index.js +177 -101
  119. package/dist/SourceView/lang-detector/ext-to-lang.json +798 -798
  120. package/dist/SourceView/lang-detector/index.js +28 -14
  121. package/dist/SourceView/useSelectedLineRange.js +72 -20
  122. package/dist/Table/ColorCell.js +42 -1
  123. package/dist/Table/ColumnsVisibility.js +114 -6
  124. package/dist/Table/MoreDropdown.js +107 -21
  125. package/dist/Table/TableContextMenu.js +144 -134
  126. package/dist/Table/TableContextMenuWrapper.js +59 -14
  127. package/dist/Table/hooks/useColorManagement.js +58 -16
  128. package/dist/Table/hooks/useTableConfiguration.d.ts.map +1 -1
  129. package/dist/Table/hooks/useTableConfiguration.js +323 -167
  130. package/dist/Table/index.js +217 -123
  131. package/dist/Table/utils/functions.js +169 -144
  132. package/dist/Table/utils/topAndBottomExpandedRowModel.js +69 -52
  133. package/dist/TimelineGuide/index.js +209 -16
  134. package/dist/TopTable/benchmarks/benchdata/populateData.js +91 -0
  135. package/dist/TopTable/index.js +325 -121
  136. package/dist/contexts/LabelsQueryProvider.js +94 -32
  137. package/dist/contexts/UnifiedLabelsContext.js +114 -49
  138. package/dist/contexts/utils.js +37 -15
  139. package/dist/hooks/urlParsers.js +27 -15
  140. package/dist/hooks/useColorBy.js +47 -10
  141. package/dist/hooks/useCompareModeMeta.js +112 -62
  142. package/dist/hooks/useDashboardItems.js +52 -11
  143. package/dist/hooks/useLabels.js +295 -52
  144. package/dist/hooks/useQueryState.d.ts +1 -1
  145. package/dist/hooks/useQueryState.d.ts.map +1 -1
  146. package/dist/hooks/useQueryState.js +375 -329
  147. package/dist/index.js +11 -6
  148. package/dist/testdata/fg-diff.json +3750 -0
  149. package/dist/testdata/fg-simple.json +1879 -0
  150. package/dist/testdata/link_data.json +56 -0
  151. package/dist/testdata/tabular.json +30 -0
  152. package/dist/testdata/test_flamegraph.json +26846 -0
  153. package/dist/testdata/test_graph.json +53 -0
  154. package/dist/useDelayedLoader.js +32 -18
  155. package/dist/useGrpcQuery/index.js +71 -11
  156. package/dist/useHasProfileData.js +90 -12
  157. package/dist/useQuery.js +205 -64
  158. package/dist/useSumBy.d.ts.map +1 -1
  159. package/dist/useSumBy.js +294 -138
  160. package/dist/utils.js +62 -30
  161. package/package.json +9 -9
  162. package/src/GraphTooltipArrow/index.tsx +3 -0
  163. package/src/MatchersInput/SuggestionsList.test.tsx +70 -0
  164. package/src/MatchersInput/SuggestionsList.tsx +11 -10
  165. package/src/MatchersInput/index.tsx +1 -1
  166. package/src/MetricsGraph/MetricsTooltip/index.tsx +22 -34
  167. package/src/PreSelectedMatchers/index.tsx +3 -0
  168. package/src/ProfileExplorer/ProfileExplorerCompare.tsx +9 -2
  169. package/src/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.tsx +3 -0
  170. package/src/ProfileFlameGraph/FlameGraphArrow/TooltipContext.tsx +3 -0
  171. package/src/ProfileFlameGraph/FlameGraphArrow/ZoomControls.tsx +3 -0
  172. package/src/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.ts +3 -0
  173. package/src/ProfileSelector/index.tsx +31 -9
  174. package/src/ProfileSelector/useAutoQuerySelector.ts +64 -42
  175. package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +3 -0
  176. package/src/ProfileView/components/VisualizationContainer/index.tsx +3 -0
  177. package/src/Table/hooks/useTableConfiguration.tsx +7 -13
  178. package/src/hooks/useQueryState.ts +18 -3
  179. package/src/useDelayedLoader.ts +10 -10
  180. package/src/useSumBy.ts +12 -18
  181. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.js +0 -455
  182. package/dist/hooks/useQueryState.test.js +0 -868
@@ -0,0 +1,70 @@
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 {useRef} from 'react';
15
+
16
+ import {act, fireEvent, render} from '@testing-library/react';
17
+ import {beforeAll, describe, expect, it, vi} from 'vitest';
18
+
19
+ import SuggestionsList, {Suggestion, Suggestions} from './SuggestionsList';
20
+
21
+ vi.mock('@parca/components', () => ({
22
+ RefreshButton: ({title}: {title: string}) => <button type="button">{title}</button>,
23
+ useParcaContext: () => ({
24
+ loader: <div>loading</div>,
25
+ }),
26
+ }));
27
+
28
+ beforeAll(() => {
29
+ Element.prototype.scrollIntoView = vi.fn();
30
+ });
31
+
32
+ const TestHarness = ({inputKey = 'initial'}: {inputKey?: string}): JSX.Element => {
33
+ const inputRef = useRef<HTMLTextAreaElement | null>(null);
34
+ const suggestions = new Suggestions();
35
+ suggestions.labelNames.push(new Suggestion('labelName', 'na', 'namespace'));
36
+
37
+ return (
38
+ <div>
39
+ <textarea key={inputKey} ref={inputRef} />
40
+ <SuggestionsList
41
+ suggestions={suggestions}
42
+ applySuggestion={vi.fn()}
43
+ inputRef={inputRef}
44
+ runQuery={vi.fn()}
45
+ focusedInput
46
+ isLabelNamesLoading={false}
47
+ isLabelValuesLoading={false}
48
+ shouldTrimPrefix={false}
49
+ refetchLabelValues={vi.fn(async () => {})}
50
+ refetchLabelNames={vi.fn(async () => {})}
51
+ />
52
+ </div>
53
+ );
54
+ };
55
+
56
+ describe('SuggestionsList', () => {
57
+ it('rebinds keyboard listeners when the textarea ref points to a remounted node', () => {
58
+ const {rerender, getByRole, getByText} = render(<TestHarness inputKey="first" />);
59
+
60
+ rerender(<TestHarness inputKey="second" />);
61
+
62
+ const textarea = getByRole('textbox');
63
+ act(() => {
64
+ fireEvent.keyDown(textarea, {key: 'ArrowDown'});
65
+ });
66
+
67
+ // eslint-disable-next-line jest-dom/prefer-to-have-class
68
+ expect(getByText('namespace').className).toContain('bg-indigo-600');
69
+ });
70
+ });
@@ -11,7 +11,7 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {Fragment, useCallback, useEffect, useState} from 'react';
14
+ import React, {Fragment, useCallback, useEffect, useState} from 'react';
15
15
 
16
16
  import {Transition} from '@headlessui/react';
17
17
  import {usePopper} from 'react-popper';
@@ -48,7 +48,7 @@ export class Suggestions {
48
48
  interface Props {
49
49
  suggestions: Suggestions;
50
50
  applySuggestion: (suggestion: Suggestion) => void;
51
- inputRef: HTMLTextAreaElement | null;
51
+ inputRef: React.RefObject<HTMLTextAreaElement | null>;
52
52
  runQuery: () => void;
53
53
  focusedInput: boolean;
54
54
  isLabelNamesLoading: boolean;
@@ -82,7 +82,7 @@ const SuggestionsList = ({
82
82
  refetchLabelNames,
83
83
  }: Props): JSX.Element => {
84
84
  const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
85
- const {styles, attributes} = usePopper(inputRef, popperElement, {
85
+ const {styles, attributes} = usePopper(inputRef.current, popperElement, {
86
86
  placement: 'bottom-start',
87
87
  });
88
88
  const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState<number>(-1);
@@ -227,18 +227,19 @@ const SuggestionsList = ({
227
227
  );
228
228
 
229
229
  useEffect(() => {
230
- if (inputRef == null) {
230
+ const el = inputRef.current;
231
+ if (el == null) {
231
232
  return;
232
233
  }
233
234
 
234
- inputRef.addEventListener('keydown', handleKeyDown);
235
- inputRef.addEventListener('keypress', handleKeyPress as any);
235
+ el.addEventListener('keydown', handleKeyDown);
236
+ el.addEventListener('keypress', handleKeyPress as any);
236
237
 
237
238
  return () => {
238
- inputRef.removeEventListener('keydown', handleKeyDown);
239
- inputRef.removeEventListener('keypress', handleKeyPress as any);
239
+ el.removeEventListener('keydown', handleKeyDown);
240
+ el.removeEventListener('keypress', handleKeyPress as any);
240
241
  };
241
- }, [inputRef, highlightedSuggestionIndex, suggestions, handleKeyPress, handleKeyDown]);
242
+ });
242
243
 
243
244
  useEffect(() => {
244
245
  if (suggestionsLength > 0 && focusedInput) {
@@ -263,7 +264,7 @@ const SuggestionsList = ({
263
264
  leaveTo="opacity-0"
264
265
  >
265
266
  <div
266
- style={{width: inputRef?.offsetWidth}}
267
+ style={{width: inputRef.current?.offsetWidth}}
267
268
  className="absolute z-10 mt-1 max-h-[400px] rounded-md bg-gray-50 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-900 sm:text-sm flex flex-col"
268
269
  >
269
270
  <div className="flex-1 overflow-auto min-h-0">
@@ -211,7 +211,7 @@ const MatchersInput = ({setDraftMatchers, draftParsedQuery, commitDraft}: Props)
211
211
  isLabelNamesLoading={isLabelNamesLoading}
212
212
  suggestions={suggestionSections}
213
213
  applySuggestion={applySuggestion}
214
- inputRef={inputRef.current}
214
+ inputRef={inputRef}
215
215
  runQuery={commitDraft}
216
216
  focusedInput={focusedInput}
217
217
  isLabelValuesLoading={
@@ -11,7 +11,9 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {useEffect, useMemo, useState} from 'react';
14
+ /* eslint-disable react-hooks/refs */
15
+
16
+ import {useLayoutEffect, useRef, useState} from 'react';
15
17
 
16
18
  import {usePopper} from 'react-popper';
17
19
 
@@ -28,21 +30,16 @@ interface Props {
28
30
  content: React.ReactNode;
29
31
  }
30
32
 
31
- const virtualElement: VirtualElement = {
32
- getBoundingClientRect: () => {
33
- const emptyRect: DOMRect = {
34
- width: 0,
35
- height: 0,
36
- top: 0,
37
- right: 0,
38
- bottom: 0,
39
- left: 0,
40
- x: 0,
41
- y: 0,
42
- toJSON: () => ({}),
43
- };
44
- return emptyRect;
45
- },
33
+ const emptyRect: DOMRect = {
34
+ width: 0,
35
+ height: 0,
36
+ top: 0,
37
+ right: 0,
38
+ bottom: 0,
39
+ left: 0,
40
+ x: 0,
41
+ y: 0,
42
+ toJSON: () => ({}),
46
43
  };
47
44
 
48
45
  const createDomRect = (x: number, y: number): DOMRect => {
@@ -61,9 +58,13 @@ const createDomRect = (x: number, y: number): DOMRect => {
61
58
  };
62
59
 
63
60
  const MetricsTooltip = ({x, y, contextElement, content}: Props): JSX.Element => {
61
+ 'use no memo';
64
62
  const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
63
+ const virtualElementRef = useRef<VirtualElement>({
64
+ getBoundingClientRect: () => emptyRect,
65
+ });
65
66
 
66
- const {styles, attributes, update} = usePopper(virtualElement, popperElement, {
67
+ const {styles, attributes, update} = usePopper(virtualElementRef.current, popperElement, {
67
68
  placement: 'auto-start',
68
69
  strategy: 'absolute',
69
70
  modifiers: [
@@ -82,26 +83,13 @@ const MetricsTooltip = ({x, y, contextElement, content}: Props): JSX.Element =>
82
83
  ],
83
84
  });
84
85
 
85
- useMemo(() => {
86
- virtualElement.getBoundingClientRect = (): DOMRect => {
87
- const domRect: DOMRect = (contextElement as Element)?.getBoundingClientRect() ?? {
88
- width: 0,
89
- height: 0,
90
- top: 0,
91
- right: 0,
92
- bottom: 0,
93
- left: 0,
94
- x: 0,
95
- y: 0,
96
- toJSON: () => ({}),
97
- };
86
+ useLayoutEffect(() => {
87
+ virtualElementRef.current.getBoundingClientRect = (): DOMRect => {
88
+ const domRect: DOMRect = (contextElement as Element)?.getBoundingClientRect() ?? emptyRect;
98
89
  return createDomRect(domRect.x + x, domRect.y + y);
99
90
  };
100
- }, [x, y, contextElement]);
101
-
102
- useEffect(() => {
103
91
  void update?.();
104
- }, [x, y, update]);
92
+ }, [x, y, contextElement, update]);
105
93
 
106
94
  // Don't render anything if content is null or undefined
107
95
  if (content == null) {
@@ -11,6 +11,8 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
+ /* eslint-disable react-hooks/refs */
15
+
14
16
  import React, {useCallback, useEffect, useRef, useState} from 'react';
15
17
 
16
18
  import {Icon} from '@iconify/react';
@@ -29,6 +31,7 @@ interface Props {
29
31
  }
30
32
 
31
33
  const PreSelectedMatchers: React.FC<Props> = ({labelNames}) => {
34
+ 'use no memo';
32
35
  const [labelValuesMap, setLabelValuesMap] = useState<Record<string, string[]>>({});
33
36
  const [isLoading, setIsLoading] = useState<Record<string, boolean>>({});
34
37
  const metadata = useGrpcMetadata();
@@ -65,8 +65,15 @@ const ProfileExplorerCompare = ({
65
65
  if (querySelectionB.expression === '' && querySelectionA.expression !== '') {
66
66
  setDraftExpressionB(querySelectionA.expression);
67
67
  setDraftTimeRangeB(querySelectionA.from, querySelectionA.to, querySelectionA.timeSelection);
68
- // Commit to update the URL and trigger metrics graph load
69
- commitDraftB();
68
+ // Commit with explicit values since draft useState hasn't applied yet
69
+ commitDraftB(
70
+ {
71
+ from: querySelectionA.from,
72
+ to: querySelectionA.to,
73
+ timeSelection: querySelectionA.timeSelection,
74
+ },
75
+ querySelectionA.expression
76
+ );
70
77
  }
71
78
  }, [
72
79
  isCompareMode,
@@ -11,6 +11,8 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
+ /* eslint-disable react-hooks/set-state-in-effect */
15
+
14
16
  import {useEffect, useRef, useState} from 'react';
15
17
 
16
18
  import {useQueryState} from 'nuqs';
@@ -66,6 +68,7 @@ function calculateTruncatedText(
66
68
  }
67
69
 
68
70
  function TextWithEllipsis({text, x, y, width}: Props): JSX.Element {
71
+ 'use no memo';
69
72
  const textRef = useRef<SVGTextElement>(null);
70
73
  const [displayText, setDisplayText] = useState(text);
71
74
  const [alignFunctionName] = useQueryState('align_function_name', stringParam.withDefault('left'));
@@ -11,6 +11,8 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
+ /* eslint-disable react-hooks/refs */
15
+
14
16
  import React, {createContext, useCallback, useContext, useMemo, useRef} from 'react';
15
17
 
16
18
  import {Table} from '@uwdata/flechette';
@@ -68,6 +70,7 @@ export const TooltipProvider: React.FC<TooltipProviderProps> = ({
68
70
  onTooltipUpdate,
69
71
  tooltipId = 'default',
70
72
  }) => {
73
+ 'use no memo';
71
74
  const tooltipStateRef = useRef<TooltipState>({row: null, x: 0, y: 0});
72
75
 
73
76
  const updateTooltip = useCallback(
@@ -11,6 +11,8 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
+ /* eslint-disable react-hooks/refs */
15
+
14
16
  import React from 'react';
15
17
 
16
18
  import {Icon} from '@iconify/react';
@@ -33,6 +35,7 @@ export const ZoomControls = ({
33
35
  resetZoom,
34
36
  portalRef,
35
37
  }: ZoomControlsProps): React.JSX.Element => {
38
+ 'use no memo';
36
39
  const controls = (
37
40
  <div className="flex items-center gap-1 rounded-md border border-gray-200 bg-white/90 px-1 py-0.5 shadow-sm backdrop-blur-sm dark:border-gray-600 dark:bg-gray-800/90">
38
41
  <button
@@ -11,6 +11,8 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
+ /* eslint-disable react-hooks/set-state-in-effect */
15
+
14
16
  import {useEffect, useRef, useState} from 'react';
15
17
 
16
18
  interface UseBatchedRenderingOptions {
@@ -29,6 +31,7 @@ export const useBatchedRendering = <T>(
29
31
  items: T[],
30
32
  options: UseBatchedRenderingOptions = {}
31
33
  ): UseBatchedRenderingResult<T> => {
34
+ 'use no memo';
32
35
  const {batchSize = 500, batchDelay = 0} = options;
33
36
 
34
37
  const [renderedCount, setRenderedCount] = useState(0);
@@ -11,7 +11,7 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState} from 'react';
14
+ import {Dispatch, SetStateAction, useCallback, useMemo, useRef, useState} from 'react';
15
15
 
16
16
  import {RpcError} from '@protobuf-ts/runtime-rpc';
17
17
  import {useQueryState as useNuqsQueryState} from 'nuqs';
@@ -104,7 +104,7 @@ const ProfileSelector = ({
104
104
  closeProfile,
105
105
  enforcedProfileName,
106
106
  comparing,
107
- navigateTo,
107
+ navigateTo: _navigateTo,
108
108
  showMetricsGraph = true,
109
109
  showSumBySelector = true,
110
110
  showProfileTypeSelector = true,
@@ -157,7 +157,20 @@ const ProfileSelector = ({
157
157
  );
158
158
 
159
159
  // Sync local timeRangeSelection when URL state changes externally (e.g., "Switch to 1 minute" button)
160
- useEffect(() => {
160
+ const [prevQueryTimeSelection, setPrevQueryTimeSelection] = useState(
161
+ querySelection.timeSelection
162
+ );
163
+ const [prevQueryFrom, setPrevQueryFrom] = useState(querySelection.from);
164
+ const [prevQueryTo, setPrevQueryTo] = useState(querySelection.to);
165
+
166
+ if (
167
+ prevQueryTimeSelection !== querySelection.timeSelection ||
168
+ prevQueryFrom !== querySelection.from ||
169
+ prevQueryTo !== querySelection.to
170
+ ) {
171
+ setPrevQueryTimeSelection(querySelection.timeSelection);
172
+ setPrevQueryFrom(querySelection.from);
173
+ setPrevQueryTo(querySelection.to);
161
174
  setTimeRangeSelection(
162
175
  DateTimeRange.fromRangeKey(
163
176
  querySelection.timeSelection,
@@ -165,7 +178,7 @@ const ProfileSelector = ({
165
178
  querySelection.to
166
179
  )
167
180
  );
168
- }, [querySelection.timeSelection, querySelection.from, querySelection.to]);
181
+ }
169
182
 
170
183
  const [queryExpressionString, setQueryExpressionString] = useState(draftSelection.expression);
171
184
 
@@ -201,18 +214,28 @@ const ProfileSelector = ({
201
214
  return result.response?.labelNames === undefined ? [] : result.response.labelNames;
202
215
  }, [result]);
203
216
 
204
- useEffect(() => {
217
+ const [prevEnforcedProfileName, setPrevEnforcedProfileName] = useState(enforcedProfileName);
218
+ const [prevQueryExpression, setPrevQueryExpression] = useState(querySelection.expression);
219
+
220
+ if (
221
+ prevEnforcedProfileName !== enforcedProfileName ||
222
+ prevQueryExpression !== querySelection.expression
223
+ ) {
224
+ setPrevEnforcedProfileName(enforcedProfileName);
225
+ setPrevQueryExpression(querySelection.expression);
205
226
  if (enforcedProfileName !== '') {
206
227
  const [q, changed] = Query.parse(querySelection.expression).setProfileName(
207
228
  enforcedProfileName
208
229
  );
209
230
  if (changed) {
210
231
  setQueryExpressionString(q.toString());
211
- return;
232
+ } else {
233
+ setQueryExpressionString(querySelection.expression);
212
234
  }
235
+ } else {
236
+ setQueryExpressionString(querySelection.expression);
213
237
  }
214
- setQueryExpressionString(querySelection.expression);
215
- }, [enforcedProfileName, querySelection.expression]);
238
+ }
216
239
 
217
240
  const enforcedProfileNameQuery = (): Query => {
218
241
  const pq = Query.parse(queryExpressionString);
@@ -271,7 +294,6 @@ const ProfileSelector = ({
271
294
  setProfileName,
272
295
  setQueryExpression,
273
296
  querySelection,
274
- navigateTo,
275
297
  loading: sumByLoading,
276
298
  defaultProfileType: externalProfilerComponent?.defaultProfileType,
277
299
  });
@@ -13,13 +13,16 @@
13
13
 
14
14
  import {useEffect, useRef} from 'react';
15
15
 
16
+ import {useQueryStates} from 'nuqs';
17
+
16
18
  import {ProfileTypesResponse} from '@parca/client';
17
19
  import {selectAutoQuery, setAutoQuery, useAppDispatch, useAppSelector} from '@parca/store';
18
- import {type NavigateFunction} from '@parca/utilities';
19
20
 
20
- import {ProfileSelectionFromParams, SuffixParams} from '..';
21
+ import {ProfileSelectionFromParams} from '..';
21
22
  import {QuerySelection} from '../ProfileSelector';
22
23
  import {constructProfileName} from '../ProfileTypeSelector';
24
+ import {boolParam, stringParam} from '../hooks/urlParsers';
25
+ import {useDashboardItems} from '../hooks/useDashboardItems';
23
26
 
24
27
  interface Props {
25
28
  selectedProfileName: string;
@@ -27,7 +30,6 @@ interface Props {
27
30
  setProfileName: (name: string) => void;
28
31
  setQueryExpression: () => void;
29
32
  querySelection: QuerySelection;
30
- navigateTo: NavigateFunction;
31
33
  loading: boolean;
32
34
  defaultProfileType?: string;
33
35
  }
@@ -38,18 +40,40 @@ export const useAutoQuerySelector = ({
38
40
  setProfileName,
39
41
  setQueryExpression,
40
42
  querySelection,
41
- navigateTo,
42
43
  loading,
43
44
  defaultProfileType,
44
45
  }: Props): void => {
45
46
  const autoQuery = useAppSelector(selectAutoQuery);
46
47
  const dispatch = useAppDispatch();
47
- const queryParams = new URLSearchParams(location.search);
48
- const compareA = queryParams.get('compare_a');
49
- const compareB = queryParams.get('compare_b');
50
- const comparing = compareA === 'true' || compareB === 'true';
51
- const expressionA = queryParams.get('expression_a');
52
- const expressionB = queryParams.get('expression_b');
48
+
49
+ const {setDashboardItems} = useDashboardItems();
50
+
51
+ const [compareState, setCompareParams] = useQueryStates(
52
+ {
53
+ compare_a: boolParam,
54
+ compare_b: boolParam,
55
+ expression_a: stringParam,
56
+ from_a: stringParam,
57
+ to_a: stringParam,
58
+ time_selection_a: stringParam,
59
+ sum_by_a: stringParam,
60
+ merge_from_a: stringParam,
61
+ merge_to_a: stringParam,
62
+ selection_a: stringParam,
63
+ expression_b: stringParam,
64
+ from_b: stringParam,
65
+ to_b: stringParam,
66
+ time_selection_b: stringParam,
67
+ sum_by_b: stringParam,
68
+ search_string: stringParam,
69
+ },
70
+ {history: 'replace'}
71
+ );
72
+
73
+ // Read compare params through nuqs (not location.search) to stay in sync
74
+ const comparing = compareState.compare_a === true || compareState.compare_b === true;
75
+ const expressionA = compareState.expression_a;
76
+ const expressionB = compareState.expression_b;
53
77
 
54
78
  // Track if we've already set up compare mode to prevent infinite loops
55
79
  const hasSetupCompareMode = useRef(false);
@@ -64,13 +88,7 @@ export const useAutoQuerySelector = ({
64
88
  // 2. expressionA exists
65
89
  // 3. expressionB doesn't exist yet (meaning we need to set it up)
66
90
  // 4. We haven't already set it up in this session
67
- if (
68
- comparing &&
69
- expressionA !== null &&
70
- expressionA !== undefined &&
71
- expressionB === null &&
72
- !hasSetupCompareMode.current
73
- ) {
91
+ if (comparing && expressionA !== null && expressionB === null && !hasSetupCompareMode.current) {
74
92
  if (querySelection.expression === undefined) {
75
93
  return;
76
94
  }
@@ -87,42 +105,46 @@ export const useAutoQuerySelector = ({
87
105
  sumBy: querySelection.sumBy,
88
106
  };
89
107
 
90
- const sumBy = queryA.sumBy?.join(',');
108
+ const sumBy = queryA.sumBy?.join(',') ?? null;
109
+
110
+ const mergeFromA = profileA != null ? profileA.HistoryParams().merge_from?.toString() : null;
111
+ const mergeToA = profileA != null ? profileA.HistoryParams().merge_to?.toString() : null;
112
+ const selectionA = profileA != null ? profileA.HistoryParams().selection?.toString() : null;
113
+
114
+ hasSetupCompareMode.current = true;
91
115
 
92
- let compareQuery: Record<string, string> = {
93
- compare_a: 'true',
116
+ // Set all compare params atomically via nuqs
117
+ void setCompareParams({
118
+ compare_a: true,
119
+ compare_b: true,
94
120
  expression_a: queryA.expression,
95
121
  from_a: queryA.from.toString(),
96
122
  to_a: queryA.to.toString(),
97
123
  time_selection_a: queryA.timeSelection,
98
-
99
- compare_b: 'true',
124
+ sum_by_a: sumBy,
125
+ merge_from_a: mergeFromA,
126
+ merge_to_a: mergeToA,
127
+ selection_a: selectionA,
100
128
  expression_b: queryA.expression,
101
129
  from_b: queryA.from.toString(),
102
130
  to_b: queryA.to.toString(),
103
131
  time_selection_b: queryA.timeSelection,
104
- };
105
-
106
- if (sumBy != null) {
107
- compareQuery.sum_by_a = sumBy;
108
- compareQuery.sum_by_b = sumBy;
109
- }
110
-
111
- if (profileA != null) {
112
- compareQuery = {
113
- ...SuffixParams(profileA.HistoryParams(), '_a'),
114
- ...compareQuery,
115
- };
116
- }
117
-
118
- hasSetupCompareMode.current = true;
119
- void navigateTo('/', {
120
- ...compareQuery,
121
- search_string: '',
122
- dashboard_items: ['flamegraph'],
132
+ sum_by_b: sumBy,
133
+ search_string: null,
123
134
  });
135
+
136
+ setDashboardItems(['flamegraph']);
124
137
  }
125
- }, [comparing, querySelection, navigateTo, expressionA, expressionB, dispatch, loading]);
138
+ }, [
139
+ comparing,
140
+ querySelection,
141
+ expressionA,
142
+ expressionB,
143
+ dispatch,
144
+ loading,
145
+ setCompareParams,
146
+ setDashboardItems,
147
+ ]);
126
148
 
127
149
  // Effect to load some initial data on load when is no selection
128
150
  useEffect(() => {
@@ -11,6 +11,8 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
+ /* eslint-disable react-hooks/set-state-in-effect */
15
+
14
16
  import React, {useCallback, useEffect, useRef, useState} from 'react';
15
17
 
16
18
  import {Menu} from '@headlessui/react';
@@ -74,6 +76,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
74
76
  customSubmenu,
75
77
  renderAsDiv = false,
76
78
  }) => {
79
+ 'use no memo';
77
80
  const menuRef = useRef<HTMLDivElement>(null);
78
81
  const [shouldOpenLeft, setShouldOpenLeft] = useState(false);
79
82
 
@@ -11,6 +11,8 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
+ /* eslint-disable react-hooks/refs */
15
+
14
16
  import {FC} from 'react';
15
17
 
16
18
  import cx from 'classnames';
@@ -42,6 +44,7 @@ export const VisualizationContainer: FC<VisualizationContainerProps> = ({
42
44
  index,
43
45
  actionButtons,
44
46
  }) => {
47
+ 'use no memo';
45
48
  const {handleClosePanel} = useDashboard();
46
49
 
47
50
  return (
@@ -11,7 +11,7 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {useEffect, useMemo, useState} from 'react';
14
+ import {useMemo} from 'react';
15
15
 
16
16
  import {createColumnHelper, type ColumnDef} from '@tanstack/table-core';
17
17
  import {useQueryState} from 'nuqs';
@@ -46,8 +46,8 @@ export function useTableConfiguration({
46
46
  const columnHelper = createColumnHelper<Row>();
47
47
  const [tableColumns] = useQueryState('table_columns', tableColumnsParser);
48
48
 
49
- const [columnVisibility, setColumnVisibility] = useState(() => {
50
- return {
49
+ const columnVisibility = useMemo(() => {
50
+ const defaults: Record<string, boolean> = {
51
51
  color: true,
52
52
  flat: true,
53
53
  flatPercentage: false,
@@ -62,19 +62,13 @@ export function useTableConfiguration({
62
62
  functionFileName: false,
63
63
  mappingFile: false,
64
64
  };
65
- });
66
-
67
- useEffect(() => {
68
65
  if (Array.isArray(tableColumns)) {
69
- setColumnVisibility(prevState => {
70
- const newState = {...prevState};
71
- (Object.keys(newState) as ColumnName[]).forEach(column => {
72
- newState[column] = tableColumns.includes(column);
73
- });
74
- return newState;
66
+ (Object.keys(defaults) as ColumnName[]).forEach(column => {
67
+ defaults[column] = tableColumns.includes(column);
75
68
  });
76
69
  }
77
- }, [tableColumns]);
70
+ return defaults;
71
+ }, [tableColumns, compareMode]);
78
72
 
79
73
  const columns = useMemo<Array<ColumnDef<Row>>>(() => {
80
74
  const baseColumns: Array<ColumnDef<Row>> = [