@parca/profile 0.19.142 → 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 (135) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/GraphTooltipArrow/useGraphTooltipMetaInfo/index.d.ts.map +1 -1
  3. package/dist/GraphTooltipArrow/useGraphTooltipMetaInfo/index.js +22 -28
  4. package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts.map +1 -1
  5. package/dist/ProfileExplorer/ProfileExplorerCompare.js +72 -73
  6. package/dist/ProfileFlameChart/SamplesStrips/index.d.ts +2 -2
  7. package/dist/ProfileFlameChart/SamplesStrips/index.d.ts.map +1 -1
  8. package/dist/ProfileFlameChart/index.d.ts.map +1 -1
  9. package/dist/ProfileFlameChart/index.js +20 -24
  10. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.d.ts.map +1 -1
  11. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.js +13 -14
  12. package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.d.ts.map +1 -1
  13. package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.js +6 -5
  14. package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
  15. package/dist/ProfileFlameGraph/index.js +8 -7
  16. package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
  17. package/dist/ProfileMetricsGraph/index.js +6 -8
  18. package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
  19. package/dist/ProfileSelector/MetricsGraphSection.js +48 -55
  20. package/dist/ProfileSelector/index.d.ts +1 -1
  21. package/dist/ProfileSelector/index.d.ts.map +1 -1
  22. package/dist/ProfileSelector/index.js +216 -210
  23. package/dist/ProfileSelector/useAutoQuerySelector.d.ts +1 -3
  24. package/dist/ProfileSelector/useAutoQuerySelector.d.ts.map +1 -1
  25. package/dist/ProfileSelector/useAutoQuerySelector.js +133 -104
  26. package/dist/ProfileView/components/ActionButtons/SortByDropdown.d.ts.map +1 -1
  27. package/dist/ProfileView/components/ActionButtons/SortByDropdown.js +24 -25
  28. package/dist/ProfileView/components/ColorStackLegend.d.ts.map +1 -1
  29. package/dist/ProfileView/components/ColorStackLegend.js +3 -5
  30. package/dist/ProfileView/components/InvertCallStack/index.d.ts.map +1 -1
  31. package/dist/ProfileView/components/InvertCallStack/index.js +47 -47
  32. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts +1 -2
  33. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -1
  34. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +37 -34
  35. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.d.ts.map +1 -1
  36. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +282 -294
  37. package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.d.ts.map +1 -1
  38. package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.js +7 -8
  39. package/dist/ProfileView/components/Toolbars/index.d.ts +2 -2
  40. package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
  41. package/dist/ProfileView/components/Toolbars/index.js +1 -1
  42. package/dist/ProfileView/components/ViewSelector/index.d.ts.map +1 -1
  43. package/dist/ProfileView/components/ViewSelector/index.js +53 -75
  44. package/dist/ProfileView/context/DashboardContext.d.ts.map +1 -1
  45. package/dist/ProfileView/context/DashboardContext.js +36 -44
  46. package/dist/ProfileView/hooks/useResetFlameGraphState.d.ts.map +1 -1
  47. package/dist/ProfileView/hooks/useResetFlameGraphState.js +8 -7
  48. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.d.ts.map +1 -1
  49. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.js +59 -59
  50. package/dist/ProfileView/hooks/useResetStateOnSeriesChange.d.ts.map +1 -1
  51. package/dist/ProfileView/hooks/useResetStateOnSeriesChange.js +37 -22
  52. package/dist/ProfileView/hooks/useVisualizationState.d.ts +3 -3
  53. package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
  54. package/dist/ProfileView/hooks/useVisualizationState.js +116 -147
  55. package/dist/ProfileViewWithData.d.ts.map +1 -1
  56. package/dist/ProfileViewWithData.js +35 -45
  57. package/dist/Sandwich/index.d.ts.map +1 -1
  58. package/dist/Sandwich/index.js +6 -5
  59. package/dist/SourceView/index.d.ts.map +1 -1
  60. package/dist/SourceView/index.js +6 -4
  61. package/dist/SourceView/useSelectedLineRange.d.ts.map +1 -1
  62. package/dist/SourceView/useSelectedLineRange.js +52 -76
  63. package/dist/Table/MoreDropdown.d.ts.map +1 -1
  64. package/dist/Table/MoreDropdown.js +42 -53
  65. package/dist/Table/TableContextMenu.d.ts.map +1 -1
  66. package/dist/Table/TableContextMenu.js +15 -19
  67. package/dist/Table/hooks/useTableConfiguration.d.ts.map +1 -1
  68. package/dist/Table/hooks/useTableConfiguration.js +107 -115
  69. package/dist/Table/index.d.ts.map +1 -1
  70. package/dist/Table/index.js +16 -16
  71. package/dist/TopTable/index.d.ts.map +1 -1
  72. package/dist/TopTable/index.js +112 -127
  73. package/dist/hooks/urlParsers.d.ts +18 -0
  74. package/dist/hooks/urlParsers.d.ts.map +1 -0
  75. package/dist/hooks/urlParsers.js +44 -0
  76. package/dist/hooks/useColorBy.d.ts +5 -0
  77. package/dist/hooks/useColorBy.d.ts.map +1 -0
  78. package/dist/hooks/useColorBy.js +63 -0
  79. package/dist/hooks/useCompareModeMeta.d.ts.map +1 -1
  80. package/dist/hooks/useCompareModeMeta.js +94 -138
  81. package/dist/hooks/useDashboardItems.d.ts +5 -0
  82. package/dist/hooks/useDashboardItems.d.ts.map +1 -0
  83. package/dist/hooks/useDashboardItems.js +68 -0
  84. package/dist/hooks/useQueryState.d.ts +4 -4
  85. package/dist/hooks/useQueryState.d.ts.map +1 -1
  86. package/dist/hooks/useQueryState.js +127 -122
  87. package/dist/index.d.ts +3 -2
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +3 -12
  90. package/dist/useSumBy.d.ts +1 -1
  91. package/dist/useSumBy.d.ts.map +1 -1
  92. package/dist/useSumBy.js +2 -2
  93. package/package.json +4 -3
  94. package/src/GraphTooltipArrow/useGraphTooltipMetaInfo/index.ts +11 -13
  95. package/src/ProfileExplorer/ProfileExplorerCompare.tsx +11 -9
  96. package/src/ProfileFlameChart/SamplesStrips/index.tsx +2 -2
  97. package/src/ProfileFlameChart/index.tsx +21 -28
  98. package/src/ProfileFlameGraph/FlameGraphArrow/ContextMenu.tsx +10 -9
  99. package/src/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.tsx +5 -3
  100. package/src/ProfileFlameGraph/index.tsx +6 -9
  101. package/src/ProfileMetricsGraph/index.tsx +6 -8
  102. package/src/ProfileSelector/MetricsGraphSection.tsx +5 -10
  103. package/src/ProfileSelector/index.tsx +33 -33
  104. package/src/ProfileSelector/useAutoQuerySelector.ts +64 -42
  105. package/src/ProfileView/components/ActionButtons/SortByDropdown.tsx +10 -6
  106. package/src/ProfileView/components/ColorStackLegend.tsx +2 -4
  107. package/src/ProfileView/components/InvertCallStack/index.tsx +5 -4
  108. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.tsx +94 -192
  109. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +21 -21
  110. package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +24 -25
  111. package/src/ProfileView/components/Toolbars/TableColumnsDropdown.tsx +4 -5
  112. package/src/ProfileView/components/Toolbars/index.tsx +3 -3
  113. package/src/ProfileView/components/ViewSelector/index.tsx +9 -16
  114. package/src/ProfileView/context/DashboardContext.tsx +6 -6
  115. package/src/ProfileView/hooks/useResetFlameGraphState.ts +6 -4
  116. package/src/ProfileView/hooks/useResetStateOnProfileTypeChange.ts +24 -26
  117. package/src/ProfileView/hooks/useResetStateOnSeriesChange.ts +16 -8
  118. package/src/ProfileView/hooks/useVisualizationState.ts +61 -69
  119. package/src/ProfileViewWithData.tsx +29 -35
  120. package/src/Sandwich/index.tsx +4 -3
  121. package/src/SourceView/index.tsx +4 -2
  122. package/src/SourceView/useSelectedLineRange.ts +34 -19
  123. package/src/Table/MoreDropdown.tsx +9 -11
  124. package/src/Table/TableContextMenu.tsx +10 -13
  125. package/src/Table/hooks/useTableConfiguration.tsx +3 -4
  126. package/src/Table/index.tsx +12 -21
  127. package/src/TopTable/index.tsx +3 -4
  128. package/src/hooks/urlParsers.ts +38 -0
  129. package/src/hooks/useColorBy.ts +42 -0
  130. package/src/hooks/useCompareModeMeta.ts +61 -91
  131. package/src/hooks/useDashboardItems.ts +46 -0
  132. package/src/hooks/useQueryState.test.tsx +275 -345
  133. package/src/hooks/useQueryState.ts +153 -120
  134. package/src/index.tsx +16 -15
  135. package/src/useSumBy.ts +3 -3
@@ -13,14 +13,10 @@
13
13
 
14
14
  import {useEffect, useMemo, useRef} from 'react';
15
15
 
16
+ import {createParser, useQueryState} from 'nuqs';
17
+
16
18
  import {LabelSet, QueryRequest_ReportType, QueryServiceClient} from '@parca/client';
17
- import {
18
- Button,
19
- useParcaContext,
20
- useURLState,
21
- useURLStateCustom,
22
- type OptionsCustom,
23
- } from '@parca/components';
19
+ import {Button, useParcaContext} from '@parca/components';
24
20
  import {Matcher, MatcherTypes, ProfileType, Query} from '@parca/parser';
25
21
  import {TimeUnits, formatDate, formatDuration} from '@parca/utilities';
26
22
 
@@ -29,6 +25,7 @@ import {boundsFromProfileSource} from '../ProfileFlameGraph/FlameGraphArrow/util
29
25
  import {MergedProfileSource, ProfileSource, timeFormat} from '../ProfileSource';
30
26
  import {useProfileFilters} from '../ProfileView/components/ProfileFilters/useProfileFilters';
31
27
  import type {SamplesData} from '../ProfileView/types/visualization';
28
+ import {flamechartDimensionParser} from '../hooks/urlParsers';
32
29
  import {useQuery} from '../useQuery';
33
30
  import {NumberDuo} from '../utils';
34
31
  import {SamplesStrip} from './SamplesStrips';
@@ -38,11 +35,8 @@ interface SelectedTimeframe {
38
35
  bounds: NumberDuo;
39
36
  }
40
37
 
41
- const TimeframeStateSerializer: OptionsCustom<SelectedTimeframe | undefined> = {
42
- parse: (value: string | string[] | undefined) => {
43
- if (value == null || value === '' || value === 'undefined' || Array.isArray(value)) {
44
- return undefined;
45
- }
38
+ const timeframeParser = createParser<SelectedTimeframe>({
39
+ parse: (value: string) => {
46
40
  try {
47
41
  const [labelPart, boundsPart] = value.split('|');
48
42
  if (labelPart != null && boundsPart != null) {
@@ -61,16 +55,13 @@ const TimeframeStateSerializer: OptionsCustom<SelectedTimeframe | undefined> = {
61
55
  } catch {
62
56
  // Ignore parsing errors
63
57
  }
64
- return undefined;
58
+ return null;
65
59
  },
66
- stringify: (value: SelectedTimeframe | undefined) => {
67
- if (value == null) {
68
- return '';
69
- }
60
+ serialize: (value: SelectedTimeframe) => {
70
61
  const labelsStr = value.labels.labels.map(l => `${l.name}:${l.value}`).join(',');
71
62
  return `${labelsStr}|${value.bounds[0]},${value.bounds[1]}`;
72
63
  },
73
- };
64
+ }).withOptions({history: 'replace'});
74
65
 
75
66
  interface ProfileFlameChartProps {
76
67
  samplesData?: SamplesData;
@@ -132,14 +123,16 @@ export const ProfileFlameChart = ({
132
123
  const {protoFilters} = useProfileFilters();
133
124
  const zoomControlsRef = useRef<HTMLDivElement>(null);
134
125
 
135
- const [selectedTimeframe, setSelectedTimeframe] = useURLStateCustom<
136
- SelectedTimeframe | undefined
137
- >('flamechart_timeframe', TimeframeStateSerializer);
126
+ const [selectedTimeframe, setSelectedTimeframe] = useQueryState(
127
+ 'flamechart_timeframe',
128
+ timeframeParser
129
+ );
138
130
 
139
131
  // Read flamechart dimension from URL state to detect changes
140
- const [flamechartDimension] = useURLState<string[]>('flamechart_dimension', {
141
- alwaysReturnArray: true,
142
- });
132
+ const [flamechartDimension] = useQueryState(
133
+ 'flamechart_dimension',
134
+ flamechartDimensionParser.withDefault([])
135
+ );
143
136
 
144
137
  // Reset selection when the parent time range (profileSource) changes
145
138
  const timeBoundsKey = boundsFromProfileSource(profileSource).join(',');
@@ -147,7 +140,7 @@ export const ProfileFlameChart = ({
147
140
  useEffect(() => {
148
141
  if (prevTimeBoundsKey.current !== timeBoundsKey) {
149
142
  prevTimeBoundsKey.current = timeBoundsKey;
150
- setSelectedTimeframe(undefined);
143
+ void setSelectedTimeframe(null);
151
144
  }
152
145
  }, [timeBoundsKey, setSelectedTimeframe]);
153
146
 
@@ -157,16 +150,16 @@ export const ProfileFlameChart = ({
157
150
  useEffect(() => {
158
151
  if (prevDimensionKey.current !== dimensionKey) {
159
152
  prevDimensionKey.current = dimensionKey;
160
- setSelectedTimeframe(undefined);
153
+ void setSelectedTimeframe(null);
161
154
  }
162
155
  }, [dimensionKey, setSelectedTimeframe]);
163
156
 
164
157
  // Handle timeframe selection from strips
165
158
  const handleSelectedTimeframe = (labels: LabelSet, bounds: NumberDuo | undefined): void => {
166
159
  if (bounds === undefined) {
167
- setSelectedTimeframe(undefined);
160
+ void setSelectedTimeframe(null);
168
161
  } else {
169
- setSelectedTimeframe({labels, bounds});
162
+ void setSelectedTimeframe({labels, bounds});
170
163
  }
171
164
  };
172
165
 
@@ -14,10 +14,11 @@
14
14
  import {Icon} from '@iconify/react';
15
15
  import {Table} from '@uwdata/flechette';
16
16
  import cx from 'classnames';
17
+ import {useQueryState} from 'nuqs';
17
18
  import {Item, Menu, Separator, Submenu} from 'react-contexify';
18
19
  import {Tooltip} from 'react-tooltip';
19
20
 
20
- import {useParcaContext, useURLState} from '@parca/components';
21
+ import {useParcaContext} from '@parca/components';
21
22
  import {USER_PREFERENCES, useUserPreference} from '@parca/hooks';
22
23
  import {ProfileType} from '@parca/parser';
23
24
  import {TEST_IDS} from '@parca/test-utils';
@@ -25,6 +26,8 @@ import {getLastItem} from '@parca/utilities';
25
26
 
26
27
  import {useGraphTooltip} from '../../GraphTooltipArrow/useGraphTooltip';
27
28
  import {useGraphTooltipMetaInfo} from '../../GraphTooltipArrow/useGraphTooltipMetaInfo';
29
+ import {stringParam} from '../../hooks/urlParsers';
30
+ import {useDashboardItems} from '../../hooks/useDashboardItems';
28
31
  import {hexifyAddress, truncateString} from '../../utils';
29
32
 
30
33
  interface ContextMenuProps {
@@ -83,12 +86,10 @@ const ContextMenu = ({
83
86
  inlined,
84
87
  } = useGraphTooltipMetaInfo({table, row});
85
88
 
86
- const [dashboardItems, setDashboardItems] = useURLState<string[]>('dashboard_items', {
87
- alwaysReturnArray: true,
88
- });
89
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
90
- const [sandwichFunctionName, setSandwichFunctionName] = useURLState<string | undefined>(
91
- 'sandwich_function_name'
89
+ const {dashboardItems, setDashboardItems} = useDashboardItems();
90
+ const [_sandwichFunctionName, setSandwichFunctionName] = useQueryState(
91
+ 'sandwich_function_name',
92
+ stringParam
92
93
  );
93
94
 
94
95
  if (contextMenuData === null) {
@@ -195,12 +196,12 @@ const ContextMenu = ({
195
196
  }
196
197
 
197
198
  if (dashboardItems.includes('sandwich')) {
198
- setSandwichFunctionName(functionName);
199
+ void setSandwichFunctionName(functionName);
199
200
  hideMenu();
200
201
  return;
201
202
  }
202
203
 
203
- setSandwichFunctionName(functionName);
204
+ void setSandwichFunctionName(functionName);
204
205
  setDashboardItems([...dashboardItems, 'sandwich']);
205
206
  hideMenu();
206
207
  }}
@@ -15,7 +15,9 @@
15
15
 
16
16
  import {useEffect, useRef, useState} from 'react';
17
17
 
18
- import {useURLState} from '@parca/components';
18
+ import {useQueryState} from 'nuqs';
19
+
20
+ import {stringParam} from '../../hooks/urlParsers';
19
21
 
20
22
  interface Props {
21
23
  text: string;
@@ -69,9 +71,9 @@ function TextWithEllipsis({text, x, y, width}: Props): JSX.Element {
69
71
  'use no memo';
70
72
  const textRef = useRef<SVGTextElement>(null);
71
73
  const [displayText, setDisplayText] = useState(text);
72
- const [alignFunctionName] = useURLState('align_function_name');
74
+ const [alignFunctionName] = useQueryState('align_function_name', stringParam.withDefault('left'));
73
75
 
74
- const showFunctionNameFromLeft = alignFunctionName === 'left' || alignFunctionName === undefined;
76
+ const showFunctionNameFromLeft = alignFunctionName === 'left';
75
77
 
76
78
  useEffect(() => {
77
79
  const textElement = textRef.current;
@@ -15,15 +15,11 @@ import React, {LegacyRef, ReactNode, useCallback, useEffect, useMemo, useState}
15
15
 
16
16
  import cx from 'classnames';
17
17
  import {AnimatePresence, motion} from 'framer-motion';
18
+ import {useQueryState} from 'nuqs';
18
19
  import {useMeasure} from 'react-use';
19
20
 
20
21
  import {FlamegraphArrow} from '@parca/client';
21
- import {
22
- FlameGraphSkeleton,
23
- SandwichFlameGraphSkeleton,
24
- useParcaContext,
25
- useURLState,
26
- } from '@parca/components';
22
+ import {FlameGraphSkeleton, SandwichFlameGraphSkeleton, useParcaContext} from '@parca/components';
27
23
  import {ProfileType} from '@parca/parser';
28
24
  import {TEST_IDS, testId} from '@parca/test-utils';
29
25
  import {capitalizeOnlyFirstLetter, divide} from '@parca/utilities';
@@ -33,6 +29,7 @@ import DiffLegend from '../ProfileView/components/DiffLegend';
33
29
  import {useProfileViewContext} from '../ProfileView/context/ProfileViewContext';
34
30
  import {useProfileMetadata} from '../ProfileView/hooks/useProfileMetadata';
35
31
  import {useVisualizationState} from '../ProfileView/hooks/useVisualizationState';
32
+ import {boolParam} from '../hooks/urlParsers';
36
33
  import {FlameGraphArrow} from './FlameGraphArrow';
37
34
  import {CurrentPathFrame} from './FlameGraphArrow/utils';
38
35
 
@@ -137,8 +134,8 @@ const ProfileFlameGraph = function ProfileFlameGraphNonMemo({
137
134
  // For non-delta profiles, like goroutines or memory, we want the profiles to be compared absolutely.
138
135
  const compareAbsoluteDefault = profileType?.delta === false ? 'true' : 'false';
139
136
 
140
- const [compareAbsolute = compareAbsoluteDefault] = useURLState('compare_absolute');
141
- const isCompareAbsolute = compareAbsolute === 'true';
137
+ const [compareAbsolute] = useQueryState('compare_absolute', boolParam);
138
+ const isCompareAbsolute = compareAbsolute ?? compareAbsoluteDefault === 'true';
142
139
 
143
140
  const mappingsListCount = useMemo(
144
141
  () => mappingsList.filter(m => m !== '').length,
@@ -180,7 +177,7 @@ const ProfileFlameGraph = function ProfileFlameGraphNonMemo({
180
177
  // If there is only one mapping file, we want to color by filename by default.
181
178
  useEffect(() => {
182
179
  if (mappingsListCount === 1 && colorBy !== 'filename') {
183
- setColorBy('filename');
180
+ void setColorBy('filename');
184
181
  }
185
182
  // eslint-disable-next-line react-hooks/exhaustive-deps
186
183
  }, [mappingsListCount]);
@@ -15,6 +15,7 @@ import {useEffect, useMemo, useState} from 'react';
15
15
 
16
16
  import {Icon} from '@iconify/react';
17
17
  import {AnimatePresence, motion} from 'framer-motion';
18
+ import {useQueryState} from 'nuqs';
18
19
 
19
20
  import {
20
21
  Label,
@@ -25,11 +26,8 @@ import {
25
26
  import {
26
27
  DateTimeRange,
27
28
  MetricsGraphSkeleton,
28
- NumberParser,
29
- NumberSerializer,
30
29
  TextWithTooltip,
31
30
  useParcaContext,
32
- useURLStateCustom,
33
31
  } from '@parca/components';
34
32
  import {Query} from '@parca/parser';
35
33
  import {TEST_IDS, testId} from '@parca/test-utils';
@@ -38,6 +36,7 @@ import {capitalizeOnlyFirstLetter, formatDate, timePattern, valueFormatter} from
38
36
  import {MergedProfileSelection, ProfileSelection} from '..';
39
37
  import MetricsGraph, {ContextMenuItemOrSubmenu, Series, SeriesPoint} from '../MetricsGraph';
40
38
  import {useMetricsGraphDimensions} from '../MetricsGraph/useMetricsGraphDimensions';
39
+ import {intParam} from '../hooks/urlParsers';
41
40
  import {getStepCountFromScreenWidth, useQueryRange} from './hooks/useQueryRange';
42
41
 
43
42
  const createProfileContextMenuItems = (
@@ -200,11 +199,10 @@ const ProfileMetricsGraph = ({
200
199
  comparing = false,
201
200
  sumBy,
202
201
  }: ProfileMetricsGraphProps): JSX.Element => {
203
- const [rawStepCount] = useURLStateCustom<number>('step_count', {
204
- defaultValue: String(getStepCountFromScreenWidth(10)),
205
- parse: NumberParser,
206
- stringify: NumberSerializer,
207
- });
202
+ const [rawStepCount] = useQueryState(
203
+ 'step_count',
204
+ intParam.withDefault(getStepCountFromScreenWidth(10))
205
+ );
208
206
  // Clamp step count so the step duration is at least 1 second as we don't have this enforced server-side anymore.
209
207
  const stepCount = useMemo(() => {
210
208
  const maxForOneSecond = Math.floor((to - from) / 1000);
@@ -14,7 +14,7 @@
14
14
  import cx from 'classnames';
15
15
 
16
16
  import {Label, QueryServiceClient} from '@parca/client';
17
- import {DateTimeRange, useParcaContext, useURLStateBatch} from '@parca/components';
17
+ import {DateTimeRange, useParcaContext} from '@parca/components';
18
18
  import {Query} from '@parca/parser';
19
19
 
20
20
  import {ProfileSelection} from '..';
@@ -67,7 +67,6 @@ export function MetricsGraphSection({
67
67
  hasNoProfileTypes = false,
68
68
  }: MetricsGraphSectionProps): JSX.Element {
69
69
  const resetStateOnSeriesChange = useResetStateOnSeriesChange();
70
- const batchUpdates = useURLStateBatch();
71
70
  const {profileExplorer} = useParcaContext();
72
71
  const {heightStyle} = useMetricsGraphDimensions(comparing, profileExplorer?.metricsGraph.height);
73
72
  const handleTimeRangeChange = (range: DateTimeRange): void => {
@@ -117,10 +116,8 @@ export function MetricsGraphSection({
117
116
 
118
117
  if (hasChanged) {
119
118
  // Immediately apply the filter when adding label matchers from the graph
120
- batchUpdates(() => {
121
- setNewQueryExpression(newQuery.toString());
122
- commitDraft(undefined, newQuery.toString());
123
- });
119
+ setNewQueryExpression(newQuery.toString());
120
+ commitDraft(undefined, newQuery.toString());
124
121
  }
125
122
  };
126
123
 
@@ -141,10 +138,8 @@ export function MetricsGraphSection({
141
138
 
142
139
  const mergeFrom = timestamp;
143
140
  const mergeTo = query.profileType().delta ? mergeFrom + BigInt(duration) : mergeFrom;
144
- batchUpdates(() => {
145
- resetStateOnSeriesChange(); // reset some state when a new series is selected
146
- setProfileSelection(mergeFrom, mergeTo, query);
147
- });
141
+ resetStateOnSeriesChange(); // reset some state when a new series is selected
142
+ setProfileSelection(mergeFrom, mergeTo, query);
148
143
  };
149
144
 
150
145
  return (
@@ -14,16 +14,10 @@
14
14
  import {Dispatch, SetStateAction, useCallback, useMemo, useRef, useState} from 'react';
15
15
 
16
16
  import {RpcError} from '@protobuf-ts/runtime-rpc';
17
+ import {useQueryState as useNuqsQueryState} from 'nuqs';
17
18
 
18
19
  import {ProfileTypesRequest, ProfileTypesResponse, QueryServiceClient} from '@parca/client';
19
- import {
20
- DateTimeRange,
21
- IconButton,
22
- useGrpcMetadata,
23
- useParcaContext,
24
- useURLState,
25
- useURLStateBatch,
26
- } from '@parca/components';
20
+ import {DateTimeRange, IconButton, useGrpcMetadata, useParcaContext} from '@parca/components';
27
21
  import {CloseIcon} from '@parca/icons';
28
22
  import {Query} from '@parca/parser';
29
23
  import {TEST_IDS, testId} from '@parca/test-utils';
@@ -36,6 +30,7 @@ import {
36
30
  import {QueryControls} from '../QueryControls';
37
31
  import {LabelsQueryProvider, useLabelsQueryProvider} from '../contexts/LabelsQueryProvider';
38
32
  import {UnifiedLabelsProvider} from '../contexts/UnifiedLabelsContext';
33
+ import {stringParam} from '../hooks/urlParsers';
39
34
  import {useLabelNames} from '../hooks/useLabels';
40
35
  import {useQueryState} from '../hooks/useQueryState';
41
36
  import useGrpcQuery from '../useGrpcQuery';
@@ -109,7 +104,7 @@ const ProfileSelector = ({
109
104
  closeProfile,
110
105
  enforcedProfileName,
111
106
  comparing,
112
- navigateTo,
107
+ navigateTo: _navigateTo,
113
108
  showMetricsGraph = true,
114
109
  showSumBySelector = true,
115
110
  showProfileTypeSelector = true,
@@ -118,8 +113,16 @@ const ProfileSelector = ({
118
113
  onSearchHook,
119
114
  }: ProfileSelectorProps): JSX.Element => {
120
115
  const {externalProfilerComponent, additionalMetricsGraph} = useParcaContext();
121
- const [queryBrowserMode, setQueryBrowserMode] = useURLState('query_browser_mode');
122
- const batchUpdates = useURLStateBatch();
116
+ const [queryBrowserMode, setRawQueryBrowserMode] = useNuqsQueryState(
117
+ 'query_browser_mode',
118
+ stringParam
119
+ );
120
+ const setQueryBrowserMode = useCallback(
121
+ (mode: string | null) => {
122
+ void setRawQueryBrowserMode(mode);
123
+ },
124
+ [setRawQueryBrowserMode]
125
+ );
123
126
 
124
127
  const profileFilterDefaults = externalProfilerComponent?.profileFilterDefaults as
125
128
  | ProfileFilter[]
@@ -245,27 +248,25 @@ const ProfileSelector = ({
245
248
  const selectedProfileName = query.profileName();
246
249
 
247
250
  const setQueryExpression = (updateTs = false): void => {
248
- batchUpdates(() => {
249
- if (onSearchHook != null) {
250
- onSearchHook();
251
- }
252
- // When updateTs is true, re-evaluate the time range to current values
253
- if (updateTs) {
254
- // Force re-evaluation of time range (important for relative ranges like "last 15 minutes")
255
- const currentFrom = timeRangeSelection.getFromMs(true);
256
- const currentTo = timeRangeSelection.getToMs(true);
257
- const currentRangeKey = timeRangeSelection.getRangeKey();
258
- // Commit with refreshed time range
259
- commitDraft({
260
- from: currentFrom,
261
- to: currentTo,
262
- timeSelection: currentRangeKey,
263
- });
264
- } else {
265
- // Commit the draft with existing values
266
- commitDraft();
267
- }
268
- });
251
+ if (onSearchHook != null) {
252
+ onSearchHook();
253
+ }
254
+ // When updateTs is true, re-evaluate the time range to current values
255
+ if (updateTs) {
256
+ // Force re-evaluation of time range (important for relative ranges like "last 15 minutes")
257
+ const currentFrom = timeRangeSelection.getFromMs(true);
258
+ const currentTo = timeRangeSelection.getToMs(true);
259
+ const currentRangeKey = timeRangeSelection.getRangeKey();
260
+ // Commit with refreshed time range
261
+ commitDraft({
262
+ from: currentFrom,
263
+ to: currentTo,
264
+ timeSelection: currentRangeKey,
265
+ });
266
+ } else {
267
+ // Commit the draft with existing values
268
+ commitDraft();
269
+ }
269
270
  };
270
271
 
271
272
  const setMatchersString = (matchers: string): void => {
@@ -293,7 +294,6 @@ const ProfileSelector = ({
293
294
  setProfileName,
294
295
  setQueryExpression,
295
296
  querySelection,
296
- navigateTo,
297
297
  loading: sumByLoading,
298
298
  defaultProfileType: externalProfilerComponent?.defaultProfileType,
299
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,19 +11,23 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {Select, useURLState} from '@parca/components';
14
+ import {useQueryState} from 'nuqs';
15
+
16
+ import {Select} from '@parca/components';
15
17
 
16
18
  import {
17
19
  FIELD_CUMULATIVE,
18
20
  FIELD_DIFF,
19
21
  FIELD_FUNCTION_NAME,
20
22
  } from '../../../ProfileFlameGraph/FlameGraphArrow';
23
+ import {stringParam} from '../../../hooks/urlParsers';
21
24
  import {useProfileViewContext} from '../../context/ProfileViewContext';
22
25
 
23
26
  const SortByDropdown = (): React.JSX.Element => {
24
- const [storeSortBy, setStoreSortBy] = useURLState('sort_by', {
25
- defaultValue: FIELD_FUNCTION_NAME,
26
- });
27
+ const [storeSortBy, setStoreSortBy] = useQueryState(
28
+ 'sort_by',
29
+ stringParam.withDefault(FIELD_FUNCTION_NAME)
30
+ );
27
31
 
28
32
  const {compareMode} = useProfileViewContext();
29
33
 
@@ -70,8 +74,8 @@ const SortByDropdown = (): React.JSX.Element => {
70
74
  },
71
75
  },
72
76
  ]}
73
- selectedKey={storeSortBy as string}
74
- onSelection={key => setStoreSortBy(key)}
77
+ selectedKey={storeSortBy}
78
+ onSelection={key => void setStoreSortBy(key)}
75
79
  placeholder={'Sort By'}
76
80
  primary={false}
77
81
  disabled={false}
@@ -16,12 +16,12 @@ import React, {useMemo} from 'react';
16
16
  import {Icon} from '@iconify/react';
17
17
  import cx from 'classnames';
18
18
 
19
- import {useURLState} from '@parca/components';
20
19
  import {USER_PREFERENCES, useCurrentColorProfile, useUserPreference} from '@parca/hooks';
21
20
  import {EVERYTHING_ELSE, selectDarkMode, useAppSelector} from '@parca/store';
22
21
 
23
22
  import {getMappingColors} from '../../ProfileFlameGraph/FlameGraphArrow';
24
23
  import useMappingList from '../../ProfileFlameGraph/FlameGraphArrow/useMappingList';
24
+ import {useColorBy} from '../../hooks/useColorBy';
25
25
  import {useProfileFilters} from './ProfileFilters/useProfileFilters';
26
26
 
27
27
  interface Props {
@@ -37,9 +37,7 @@ const ColorStackLegend = ({mappings, compareMode = false, loading}: Props): Reac
37
37
  USER_PREFERENCES.FLAMEGRAPH_COLOR_PROFILE.key
38
38
  );
39
39
 
40
- const [colorByValue, _] = useURLState('color_by');
41
-
42
- const colorBy = colorByValue === 'binary' || colorByValue === undefined ? 'binary' : 'filename';
40
+ const {colorBy} = useColorBy();
43
41
 
44
42
  const {appliedFilters, removeExcludeBinary, excludeBinary} = useProfileFilters();
45
43