Skip to content

Commit 8779b6a

Browse files
committed
More render loop prevention fixes
1 parent b6eb46d commit 8779b6a

5 files changed

Lines changed: 91 additions & 107 deletions

File tree

.claude/settings.local.json

Lines changed: 0 additions & 59 deletions
This file was deleted.

ui/packages/shared/profile/src/ProfileExplorer/ProfileExplorerCompare.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,15 @@ const ProfileExplorerCompare = ({
6565
if (querySelectionB.expression === '' && querySelectionA.expression !== '') {
6666
setDraftExpressionB(querySelectionA.expression);
6767
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+
);
7077
}
7178
}, [
7279
isCompareMode,

ui/packages/shared/profile/src/ProfileSelector/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,6 @@ const ProfileSelector = ({
294294
setProfileName,
295295
setQueryExpression,
296296
querySelection,
297-
navigateTo,
298297
loading: sumByLoading,
299298
defaultProfileType: externalProfilerComponent?.defaultProfileType,
300299
});

ui/packages/shared/profile/src/ProfileSelector/useAutoQuerySelector.ts

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,23 @@
1313

1414
import {useEffect, useRef} from 'react';
1515

16+
import {useQueryStates} from 'nuqs';
17+
1618
import {ProfileTypesResponse} from '@parca/client';
1719
import {selectAutoQuery, setAutoQuery, useAppDispatch, useAppSelector} from '@parca/store';
18-
import {type NavigateFunction} from '@parca/utilities';
1920

20-
import {ProfileSelectionFromParams, SuffixParams} from '..';
21+
import {ProfileSelectionFromParams} from '..';
2122
import {QuerySelection} from '../ProfileSelector';
2223
import {constructProfileName} from '../ProfileTypeSelector';
24+
import {boolParam, stringParam} from '../hooks/urlParsers';
25+
import {useDashboardItems} from '../hooks/useDashboardItems';
2326

2427
interface Props {
2528
selectedProfileName: string;
2629
profileTypesData: ProfileTypesResponse | undefined;
2730
setProfileName: (name: string) => void;
2831
setQueryExpression: () => void;
2932
querySelection: QuerySelection;
30-
navigateTo: NavigateFunction;
3133
loading: boolean;
3234
defaultProfileType?: string;
3335
}
@@ -38,18 +40,40 @@ export const useAutoQuerySelector = ({
3840
setProfileName,
3941
setQueryExpression,
4042
querySelection,
41-
navigateTo,
4243
loading,
4344
defaultProfileType,
4445
}: Props): void => {
4546
const autoQuery = useAppSelector(selectAutoQuery);
4647
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;
5377

5478
// Track if we've already set up compare mode to prevent infinite loops
5579
const hasSetupCompareMode = useRef(false);
@@ -64,13 +88,7 @@ export const useAutoQuerySelector = ({
6488
// 2. expressionA exists
6589
// 3. expressionB doesn't exist yet (meaning we need to set it up)
6690
// 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) {
7492
if (querySelection.expression === undefined) {
7593
return;
7694
}
@@ -87,42 +105,46 @@ export const useAutoQuerySelector = ({
87105
sumBy: querySelection.sumBy,
88106
};
89107

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;
91115

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,
94120
expression_a: queryA.expression,
95121
from_a: queryA.from.toString(),
96122
to_a: queryA.to.toString(),
97123
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,
100128
expression_b: queryA.expression,
101129
from_b: queryA.from.toString(),
102130
to_b: queryA.to.toString(),
103131
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,
123134
});
135+
136+
setDashboardItems(['flamegraph']);
124137
}
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+
]);
126148

127149
// Effect to load some initial data on load when is no selection
128150
useEffect(() => {

ui/packages/shared/profile/src/hooks/useQueryState.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// See the License for the specific language governing permissions and
1212
// limitations under the License.
1313

14-
import {useCallback, useEffect, useMemo, useState} from 'react';
14+
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
1515

1616
import {useQueryState as useNuqsQueryState, useQueryStates} from 'nuqs';
1717

@@ -50,7 +50,10 @@ interface UseQueryStateReturn {
5050
setDraftMatchers: (matchers: string) => void;
5151

5252
// Commit function
53-
commitDraft: (refreshedTimeRange?: {from: number; to: number; timeSelection: string}) => void;
53+
commitDraft: (
54+
refreshedTimeRange?: {from: number; to: number; timeSelection: string},
55+
expression?: string
56+
) => void;
5457

5558
// ProfileSelection state (separate from QuerySelection)
5659
profileSelection: ProfileSelection | null;
@@ -229,8 +232,20 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
229232

230233
// Sync computed sumBy to URL if URL doesn't already have a value
231234
// to ensure the shared URL can always pick it up.
235+
// Only run once (when sumByParam first becomes available), not on every change,
236+
// to avoid oscillation with external writers (useViewQueryState).
237+
const hasSyncedSumByRef = useRef(false);
232238
useEffect(() => {
233-
if (sumByParam === null && computedSumByFromURL !== undefined && !sumBySelectionLoading) {
239+
if (sumByParam !== null) {
240+
hasSyncedSumByRef.current = true;
241+
}
242+
if (
243+
!hasSyncedSumByRef.current &&
244+
sumByParam === null &&
245+
computedSumByFromURL !== undefined &&
246+
!sumBySelectionLoading
247+
) {
248+
hasSyncedSumByRef.current = true;
234249
void setSumByParam(sumByToParam(computedSumByFromURL));
235250
}
236251
}, [sumByParam, computedSumByFromURL, sumBySelectionLoading, setSumByParam]);

0 commit comments

Comments
 (0)