Skip to content

Commit e384c6d

Browse files
committed
Persist selected function in the URL.
1 parent 1b8f24b commit e384c6d

8 files changed

Lines changed: 130 additions & 24 deletions

File tree

src/actions/profile-view.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import {
8181
funcHasRecursiveCall,
8282
} from '../profile-logic/transforms';
8383
import { changeStoredProfileNameInDb } from 'firefox-profiler/app-logic/uploaded-profiles-db';
84+
import { withHistoryReplaceStateSync } from 'firefox-profiler/app-logic/url-handling';
8485
import type { TabSlug } from '../app-logic/tabs-handling';
8586
import type { CallNodeInfo } from '../profile-logic/call-node-info';
8687
import type { SingleColumnSortState } from '../components/shared/TreeView';
@@ -163,17 +164,25 @@ export function changeUpperWingSelectedCallNode(
163164

164165
/**
165166
* Select a function for a given thread in the function list.
167+
*
168+
* Uses replaceState rather than pushState so that holding e.g. the down arrow
169+
* key in the function list doesn't get rate-limited by the browser and doesn't
170+
* flood the back/forward history.
166171
*/
167172
export function changeSelectedFunctionIndex(
168173
threadsKey: ThreadsKey,
169174
selectedFunctionIndex: IndexIntoFuncTable | null,
170175
context: SelectionContext = { source: 'auto' }
171-
): Action {
172-
return {
173-
type: 'CHANGE_SELECTED_FUNCTION',
174-
selectedFunctionIndex,
175-
threadsKey,
176-
context,
176+
): ThunkAction<void> {
177+
return (dispatch) => {
178+
withHistoryReplaceStateSync(() => {
179+
dispatch({
180+
type: 'CHANGE_SELECTED_FUNCTION',
181+
selectedFunctionIndex,
182+
threadsKey,
183+
context,
184+
});
185+
});
177186
};
178187
}
179188

src/app-logic/url-handling.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import type {
4242
IndexIntoFrameTable,
4343
MarkerIndex,
4444
SelectedMarkersPerThread,
45+
SelectedFunctionsPerThread,
4546
FunctionListSectionsOpenState,
4647
} from 'firefox-profiler/types';
4748
import {
@@ -189,6 +190,7 @@ type CallTreeQuery = BaseQuery & {
189190
ctSummary: string;
190191
functionListSort?: string; // "total-desc~self-asc" — primary first
191192
funcListSections?: string; // "descendants,self" — comma-separated open sections
193+
selectedFunc?: number; // Selected function index for the current thread, e.g. 42
192194
};
193195

194196
type MarkersQuery = BaseQuery & {
@@ -238,6 +240,7 @@ type Query = BaseQuery & {
238240
// Function list specific
239241
functionListSort?: string;
240242
funcListSections?: string;
243+
selectedFunc?: number;
241244

242245
// Network specific
243246
networkSearch?: string;
@@ -400,6 +403,14 @@ export function getQueryStringFromUrlState(urlState: UrlState): string {
400403
query.funcListSections = convertFunctionListSectionsOpenToString(
401404
urlState.profileSpecific.functionListSectionsOpen
402405
);
406+
query.selectedFunc =
407+
selectedThreadsKey !== null &&
408+
urlState.profileSpecific.selectedFunctions[selectedThreadsKey] !==
409+
null &&
410+
urlState.profileSpecific.selectedFunctions[selectedThreadsKey] !==
411+
undefined
412+
? urlState.profileSpecific.selectedFunctions[selectedThreadsKey]
413+
: undefined;
403414
}
404415
break;
405416
}
@@ -563,6 +574,19 @@ export function stateFromLocation(
563574
}
564575
}
565576

577+
// Parse the selected function for the current thread
578+
const selectedFunctions: SelectedFunctionsPerThread = {};
579+
if (
580+
selectedThreadsKey !== null &&
581+
query.selectedFunc !== undefined &&
582+
query.selectedFunc !== null
583+
) {
584+
const funcIndex = Number(query.selectedFunc);
585+
if (Number.isInteger(funcIndex) && funcIndex >= 0) {
586+
selectedFunctions[selectedThreadsKey] = funcIndex;
587+
}
588+
}
589+
566590
// tabID is used for the tab selector that we have in our full view.
567591
let tabID = null;
568592
if (query.tabID && Number.isInteger(Number(query.tabID))) {
@@ -654,6 +678,7 @@ export function stateFromLocation(
654678
? query.hiddenThreads.split('-').map((index) => Number(index))
655679
: null,
656680
selectedMarkers,
681+
selectedFunctions,
657682
markerTableSort: convertMarkerTableSortFromString(query.markerSort),
658683
functionListSort: convertFunctionListSortFromString(
659684
query.functionListSort

src/reducers/profile-view.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ export const defaultThreadViewOptions: ThreadViewOptions = {
146146
expandedInvertedCallNodePaths: new PathSet(),
147147
expandedLowerWingCallNodePaths: new PathSet(),
148148
expandedUpperWingCallNodePaths: new PathSet(),
149-
selectedFunctionIndex: null,
150149
selectedNetworkMarker: null,
151150
lastSeenTransformCount: 0,
152151
};
@@ -296,17 +295,19 @@ const viewOptionsPerThread: Reducer<ThreadViewOptionsPerThreads> = (
296295

297296
const threadState = _getThreadViewOptions(state, threadsKey);
298297

299-
const previousSelectedFunction = threadState.selectedFunctionIndex;
298+
const previousLowerWingPath = threadState.selectedLowerWingCallNodePath;
299+
const isSameSelection =
300+
selectedFunctionIndex === null
301+
? previousLowerWingPath.length === 0
302+
: previousLowerWingPath.length === 1 &&
303+
previousLowerWingPath[0] === selectedFunctionIndex;
300304

301-
// If the selected function doesn't actually change, let's return the previous
302-
// state to avoid rerenders.
303-
if (selectedFunctionIndex === previousSelectedFunction) {
305+
if (isSameSelection) {
304306
return state;
305307
}
306308

307309
if (selectedFunctionIndex !== null) {
308310
return _updateThreadViewOptions(state, threadsKey, {
309-
selectedFunctionIndex,
310311
selectedLowerWingCallNodePath: [selectedFunctionIndex],
311312
expandedLowerWingCallNodePaths: new PathSet([
312313
[selectedFunctionIndex],
@@ -319,7 +320,10 @@ const viewOptionsPerThread: Reducer<ThreadViewOptionsPerThreads> = (
319320
}
320321

321322
return _updateThreadViewOptions(state, threadsKey, {
322-
selectedFunctionIndex,
323+
selectedLowerWingCallNodePath: [],
324+
expandedLowerWingCallNodePaths: new PathSet(),
325+
selectedUpperWingCallNodePath: [],
326+
expandedUpperWingCallNodePaths: new PathSet(),
323327
});
324328
}
325329
case 'CHANGE_INVERT_CALLSTACK': {
@@ -459,8 +463,48 @@ const viewOptionsPerThread: Reducer<ThreadViewOptionsPerThreads> = (
459463
return state;
460464
}
461465

462-
const { transforms } = action.newUrlState.profileSpecific;
463-
return objectMap(state, (viewOptions, threadsKey) => {
466+
const { transforms, selectedFunctions } = action.newUrlState.profileSpecific;
467+
468+
// The selected function lives in URL state; mirror it into the per-thread
469+
// wing paths so that initial loads and back/forward navigation restore the
470+
// wings to the right function.
471+
const newState: ThreadViewOptionsPerThreads = { ...state };
472+
for (const threadsKey of Object.keys(selectedFunctions)) {
473+
const selectedFunctionIndex = selectedFunctions[threadsKey];
474+
const viewOptions = _getThreadViewOptions(newState, threadsKey);
475+
const previousLowerWingPath = viewOptions.selectedLowerWingCallNodePath;
476+
const matchesExisting =
477+
selectedFunctionIndex === null
478+
? previousLowerWingPath.length === 0
479+
: previousLowerWingPath.length === 1 &&
480+
previousLowerWingPath[0] === selectedFunctionIndex;
481+
if (matchesExisting) {
482+
continue;
483+
}
484+
if (selectedFunctionIndex === null) {
485+
newState[threadsKey] = {
486+
...viewOptions,
487+
selectedLowerWingCallNodePath: [],
488+
expandedLowerWingCallNodePaths: new PathSet(),
489+
selectedUpperWingCallNodePath: [],
490+
expandedUpperWingCallNodePaths: new PathSet(),
491+
};
492+
} else {
493+
newState[threadsKey] = {
494+
...viewOptions,
495+
selectedLowerWingCallNodePath: [selectedFunctionIndex],
496+
expandedLowerWingCallNodePaths: new PathSet([
497+
[selectedFunctionIndex],
498+
]),
499+
selectedUpperWingCallNodePath: [selectedFunctionIndex],
500+
expandedUpperWingCallNodePaths: new PathSet([
501+
[selectedFunctionIndex],
502+
]),
503+
};
504+
}
505+
}
506+
507+
return objectMap(newState, (viewOptions, threadsKey) => {
464508
const transformStack = transforms[threadsKey] || [];
465509
const newTransformCount = transformStack.length;
466510
const oldTransformCount = viewOptions.lastSeenTransformCount;

src/reducers/url-state.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type {
2424
IsOpenPerPanelState,
2525
TabID,
2626
SelectedMarkersPerThread,
27+
SelectedFunctionsPerThread,
2728
FunctionListSectionsOpenState,
2829
} from 'firefox-profiler/types';
2930

@@ -795,6 +796,26 @@ const selectedMarkers: Reducer<SelectedMarkersPerThread> = (
795796
}
796797
};
797798

799+
const selectedFunctions: Reducer<SelectedFunctionsPerThread> = (
800+
state = {},
801+
action
802+
): SelectedFunctionsPerThread => {
803+
switch (action.type) {
804+
case 'CHANGE_SELECTED_FUNCTION': {
805+
const { threadsKey, selectedFunctionIndex } = action;
806+
if (state[threadsKey] === selectedFunctionIndex) {
807+
return state;
808+
}
809+
return {
810+
...state,
811+
[threadsKey]: selectedFunctionIndex,
812+
};
813+
}
814+
default:
815+
return state;
816+
}
817+
};
818+
798819
/**
799820
* These values are specific to an individual profile.
800821
*/
@@ -824,6 +845,7 @@ const profileSpecific = combineReducers({
824845
showJsTracerSummary,
825846
tabFilter,
826847
selectedMarkers,
848+
selectedFunctions,
827849
markerTableSort,
828850
functionListSort,
829851
functionListSectionsOpen,

src/selectors/per-thread/stack-sample.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,9 @@ export function getStackAndSampleSelectorsPerThread(
209209
}
210210
);
211211

212-
const getSelectedFunctionIndex: Selector<IndexIntoFuncTable | null> =
213-
createSelector(
214-
threadSelectors.getViewOptions,
215-
(threadViewOptions): IndexIntoFuncTable | null => {
216-
return threadViewOptions.selectedFunctionIndex;
217-
}
218-
);
212+
const getSelectedFunctionIndex: Selector<IndexIntoFuncTable | null> = (
213+
state
214+
) => UrlState.getSelectedFunction(state, threadsKey);
219215

220216
const getUpperWingCallNodeInfo: Selector<CallNodeInfo> = createSelector(
221217
_getNonInvertedCallNodeInfo,

src/selectors/url-state.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import type {
3232
TabID,
3333
IndexIntoSourceTable,
3434
MarkerIndex,
35+
IndexIntoFuncTable,
3536
FunctionListSectionsOpenState,
3637
} from 'firefox-profiler/types';
3738

@@ -254,6 +255,12 @@ export const getSelectedMarker: DangerousSelectorWithArguments<
254255
> = (state, threadsKey) =>
255256
getProfileSpecificState(state).selectedMarkers[threadsKey] ?? null;
256257

258+
export const getSelectedFunction: DangerousSelectorWithArguments<
259+
IndexIntoFuncTable | null,
260+
ThreadsKey
261+
> = (state, threadsKey) =>
262+
getProfileSpecificState(state).selectedFunctions[threadsKey] ?? null;
263+
257264
export const getIsBottomBoxOpen: Selector<boolean> = (state) => {
258265
const tab = getSelectedTab(state);
259266
return getProfileSpecificState(state).isBottomBoxOpenPerPanel[tab];

src/test/store/__snapshots__/profile-view.test.ts.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4261,7 +4261,6 @@ Object {
42614261
"_table": Map {},
42624262
},
42634263
"lastSeenTransformCount": 1,
4264-
"selectedFunctionIndex": null,
42654264
"selectedInvertedCallNodePath": Array [],
42664265
"selectedLowerWingCallNodePath": Array [],
42674266
"selectedNetworkMarker": null,

src/types/state.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ export type ThreadViewOptions = {
5959
readonly selectedInvertedCallNodePath: CallNodePath;
6060
readonly expandedNonInvertedCallNodePaths: PathSet;
6161
readonly expandedInvertedCallNodePaths: PathSet;
62-
readonly selectedFunctionIndex: IndexIntoFuncTable | null;
6362
readonly selectedLowerWingCallNodePath: CallNodePath;
6463
readonly expandedLowerWingCallNodePaths: PathSet;
6564
readonly selectedUpperWingCallNodePath: CallNodePath;
@@ -107,6 +106,10 @@ export type SelectedMarkersPerThread = {
107106
[key: ThreadsKey]: MarkerIndex | null;
108107
};
109108

109+
export type SelectedFunctionsPerThread = {
110+
[key: ThreadsKey]: IndexIntoFuncTable | null;
111+
};
112+
110113
/**
111114
* Profile view state
112115
*/
@@ -399,6 +402,7 @@ export type ProfileSpecificUrlState = {
399402
legacyThreadOrder: ThreadIndex[] | null;
400403
legacyHiddenThreads: ThreadIndex[] | null;
401404
selectedMarkers: SelectedMarkersPerThread;
405+
selectedFunctions: SelectedFunctionsPerThread;
402406
markerTableSort: SingleColumnSortState[];
403407
functionListSort: SingleColumnSortState[];
404408
functionListSectionsOpen: FunctionListSectionsOpenState;

0 commit comments

Comments
 (0)