Skip to content

Commit d5f817c

Browse files
author
Brian Vaughn
committed
Lifted view state into context so it persists between tabs
This almost works perfectly, except for a small problem restoring vertical scroll position. Will address in a follow up.
1 parent 27d2678 commit d5f817c

File tree

8 files changed

+291
-188
lines changed

8 files changed

+291
-188
lines changed

packages/react-devtools-scheduling-profiler/src/CanvasPage.js

Lines changed: 70 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@
77
* @flow
88
*/
99

10-
import type {
11-
Point,
12-
HorizontalPanAndZoomViewOnChangeCallback,
13-
} from './view-base';
10+
import type {Point} from './view-base';
1411
import type {
1512
ReactHoverContextInfo,
1613
ReactProfilerData,
1714
ReactMeasure,
15+
ViewState,
1816
} from './types';
1917

2018
import * as React from 'react';
@@ -55,30 +53,37 @@ import {
5553
UserTimingMarksView,
5654
} from './content-views';
5755
import {COLORS} from './content-views/constants';
58-
56+
import {clampState} from './view-base/utils/scrollState';
5957
import EventTooltip from './EventTooltip';
6058
import {RegistryContext} from 'react-devtools-shared/src/devtools/ContextMenu/Contexts';
6159
import ContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenu';
6260
import ContextMenuItem from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem';
6361
import useContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/useContextMenu';
6462
import {getBatchRange} from './utils/getBatchRange';
63+
import {MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL} from './view-base/constants';
6564

6665
import styles from './CanvasPage.css';
6766

6867
const CONTEXT_MENU_ID = 'canvas';
6968

7069
type Props = {|
7170
profilerData: ReactProfilerData,
71+
viewState: ViewState,
7272
|};
7373

74-
function CanvasPage({profilerData}: Props) {
74+
function CanvasPage({profilerData, viewState}: Props) {
7575
return (
7676
<div
7777
className={styles.CanvasPage}
7878
style={{backgroundColor: COLORS.BACKGROUND}}>
7979
<AutoSizer>
8080
{({height, width}: {height: number, width: number}) => (
81-
<AutoSizedCanvas data={profilerData} height={height} width={width} />
81+
<AutoSizedCanvas
82+
data={profilerData}
83+
height={height}
84+
viewState={viewState}
85+
width={width}
86+
/>
8287
)}
8388
</AutoSizer>
8489
</div>
@@ -103,23 +108,30 @@ const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => {
103108
const zoomToBatch = (
104109
data: ReactProfilerData,
105110
measure: ReactMeasure,
106-
syncedHorizontalPanAndZoomViews: HorizontalPanAndZoomView[],
111+
viewState: ViewState,
107112
) => {
108113
const {batchUID} = measure;
109114
const [startTime, stopTime] = getBatchRange(batchUID, data);
110-
syncedHorizontalPanAndZoomViews.forEach(syncedView =>
111-
// Using time as range works because the views' intrinsic content size is based on time.
112-
syncedView.zoomToRange(startTime, stopTime),
113-
);
115+
116+
viewState.updateHorizontalScrollState({
117+
offset: startTime,
118+
length: stopTime - startTime,
119+
});
114120
};
115121

116122
type AutoSizedCanvasProps = {|
117123
data: ReactProfilerData,
118124
height: number,
125+
viewState: ViewState,
119126
width: number,
120127
|};
121128

122-
function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
129+
function AutoSizedCanvas({
130+
data,
131+
height,
132+
viewState,
133+
width,
134+
}: AutoSizedCanvasProps) {
123135
const canvasRef = useRef<HTMLCanvasElement | null>(null);
124136

125137
const [isContextMenuShown, setIsContextMenuShown] = useState<boolean>(false);
@@ -137,35 +149,31 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
137149
const componentMeasuresViewRef = useRef(null);
138150
const reactMeasuresViewRef = useRef(null);
139151
const flamechartViewRef = useRef(null);
140-
const syncedHorizontalPanAndZoomViewsRef = useRef<HorizontalPanAndZoomView[]>(
141-
[],
142-
);
143152

144153
const {hideMenu: hideContextMenu} = useContext(RegistryContext);
145154

146155
useLayoutEffect(() => {
147156
const surface = surfaceRef.current;
148157
const defaultFrame = {origin: zeroPoint, size: {width, height}};
149158

150-
// Clear synced views
151-
syncedHorizontalPanAndZoomViewsRef.current = [];
152-
153-
const syncAllHorizontalPanAndZoomViewStates: HorizontalPanAndZoomViewOnChangeCallback = (
154-
newState,
155-
triggeringView?: HorizontalPanAndZoomView,
156-
) => {
157-
// Hide context menu when panning.
159+
// Auto hide context menu when panning.
160+
viewState.onHorizontalScrollStateChange(scrollState => {
158161
hideContextMenu();
162+
});
159163

160-
syncedHorizontalPanAndZoomViewsRef.current.forEach(
161-
syncedView =>
162-
triggeringView !== syncedView && syncedView.setScrollState(newState),
163-
);
164-
};
164+
// Initialize horizontal view state
165+
viewState.updateHorizontalScrollState(
166+
clampState({
167+
state: viewState.horizontalScrollState,
168+
minContentLength: data.duration * MIN_ZOOM_LEVEL,
169+
maxContentLength: data.duration * MAX_ZOOM_LEVEL,
170+
containerLength: defaultFrame.size.width,
171+
}),
172+
);
165173

166174
function createViewHelper(
167175
view: View,
168-
resizeLabel: string = '',
176+
label: string,
169177
shouldScrollVertically: boolean = false,
170178
shouldResizeVertically: boolean = false,
171179
): View {
@@ -175,6 +183,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
175183
surface,
176184
defaultFrame,
177185
view,
186+
viewState,
187+
label,
178188
);
179189
}
180190

@@ -183,31 +193,39 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
183193
defaultFrame,
184194
verticalScrollView !== null ? verticalScrollView : view,
185195
data.duration,
186-
syncAllHorizontalPanAndZoomViewStates,
196+
viewState,
187197
);
188198

189-
syncedHorizontalPanAndZoomViewsRef.current.push(horizontalPanAndZoomView);
190-
191-
let viewToReturn = horizontalPanAndZoomView;
199+
let resizableView = null;
192200
if (shouldResizeVertically) {
193-
viewToReturn = new ResizableView(
201+
resizableView = new ResizableView(
194202
surface,
195203
defaultFrame,
196204
horizontalPanAndZoomView,
205+
viewState,
197206
canvasRef,
198-
resizeLabel,
207+
label,
199208
);
200209
}
201210

202-
return viewToReturn;
211+
// Restore persisted view state in this specific order
212+
// to allow Resize view to adjust the size of the scroll views first.
213+
if (resizableView !== null) {
214+
resizableView.restoreMutableViewState();
215+
}
216+
if (verticalScrollView !== null) {
217+
verticalScrollView.restoreMutableViewState();
218+
}
219+
220+
return resizableView || horizontalPanAndZoomView;
203221
}
204222

205223
const axisMarkersView = new TimeAxisMarkersView(
206224
surface,
207225
defaultFrame,
208226
data.duration,
209227
);
210-
const axisMarkersViewWrapper = createViewHelper(axisMarkersView);
228+
const axisMarkersViewWrapper = createViewHelper(axisMarkersView, 'time');
211229

212230
let userTimingMarksViewWrapper = null;
213231
if (data.otherUserTimingMarks.length > 0) {
@@ -218,7 +236,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
218236
data.duration,
219237
);
220238
userTimingMarksViewRef.current = userTimingMarksView;
221-
userTimingMarksViewWrapper = createViewHelper(userTimingMarksView);
239+
userTimingMarksViewWrapper = createViewHelper(
240+
userTimingMarksView,
241+
'user timing api',
242+
);
222243
}
223244

224245
const nativeEventsView = new NativeEventsView(surface, defaultFrame, data);
@@ -236,7 +257,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
236257
data,
237258
);
238259
schedulingEventsViewRef.current = schedulingEventsView;
239-
const schedulingEventsViewWrapper = createViewHelper(schedulingEventsView);
260+
const schedulingEventsViewWrapper = createViewHelper(
261+
schedulingEventsView,
262+
'react updates',
263+
);
240264

241265
let suspenseEventsViewWrapper = null;
242266
if (data.suspenseEvents.length > 0) {
@@ -262,7 +286,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
262286
reactMeasuresViewRef.current = reactMeasuresView;
263287
const reactMeasuresViewWrapper = createViewHelper(
264288
reactMeasuresView,
265-
'react',
289+
'react scheduling',
266290
true,
267291
true,
268292
);
@@ -275,7 +299,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
275299
data,
276300
);
277301
componentMeasuresViewRef.current = componentMeasuresView;
278-
componentMeasuresViewWrapper = createViewHelper(componentMeasuresView);
302+
componentMeasuresViewWrapper = createViewHelper(
303+
componentMeasuresView,
304+
'react components',
305+
);
279306
}
280307

281308
const flamechartView = new FlamechartView(
@@ -623,11 +650,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
623650
{measure !== null && (
624651
<ContextMenuItem
625652
onClick={() =>
626-
zoomToBatch(
627-
contextData.data,
628-
measure,
629-
syncedHorizontalPanAndZoomViewsRef.current,
630-
)
653+
zoomToBatch(contextData.data, measure, viewState)
631654
}
632655
title="Zoom to batch">
633656
Zoom to batch

packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import type {DataResource} from './createDataResourceFromImportedFile';
11+
import type {ViewState} from './types';
1112

1213
import * as React from 'react';
1314
import {
@@ -27,9 +28,11 @@ import CanvasPage from './CanvasPage';
2728
import styles from './SchedulingProfiler.css';
2829

2930
export function SchedulingProfiler(_: {||}) {
30-
const {importSchedulingProfilerData, schedulingProfilerData} = useContext(
31-
SchedulingProfilerContext,
32-
);
31+
const {
32+
importSchedulingProfilerData,
33+
schedulingProfilerData,
34+
viewState,
35+
} = useContext(SchedulingProfilerContext);
3336

3437
const ref = useRef(null);
3538

@@ -66,6 +69,7 @@ export function SchedulingProfiler(_: {||}) {
6669
dataResource={schedulingProfilerData}
6770
key={key}
6871
onFileSelect={importSchedulingProfilerData}
72+
viewState={viewState}
6973
/>
7074
</Suspense>
7175
) : (
@@ -130,15 +134,17 @@ const CouldNotLoadProfile = ({error, onFileSelect}) => (
130134
const DataResourceComponent = ({
131135
dataResource,
132136
onFileSelect,
137+
viewState,
133138
}: {|
134139
dataResource: DataResource,
135140
onFileSelect: (file: File) => void,
141+
viewState: ViewState,
136142
|}) => {
137143
const dataOrError = dataResource.read();
138144
if (dataOrError instanceof Error) {
139145
return (
140146
<CouldNotLoadProfile error={dataOrError} onFileSelect={onFileSelect} />
141147
);
142148
}
143-
return <CanvasPage profilerData={dataOrError} />;
149+
return <CanvasPage profilerData={dataOrError} viewState={viewState} />;
144150
};

packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import * as React from 'react';
1111
import {createContext, useCallback, useMemo, useState} from 'react';
1212
import createDataResourceFromImportedFile from './createDataResourceFromImportedFile';
1313

14+
import type {HorizontalScrollStateChangeCallback, ViewState} from './types';
1415
import type {DataResource} from './createDataResourceFromImportedFile';
1516

1617
export type Context = {|
1718
clearSchedulingProfilerData: () => void,
1819
importSchedulingProfilerData: (file: File) => void,
1920
schedulingProfilerData: DataResource | null,
21+
viewState: ViewState,
2022
|};
2123

2224
const SchedulingProfilerContext = createContext<Context>(
@@ -42,20 +44,51 @@ function SchedulingProfilerContextController({children}: Props) {
4244
setSchedulingProfilerData(createDataResourceFromImportedFile(file));
4345
}, []);
4446

45-
// TODO (scheduling profiler) Start/stop time ref here?
47+
// Recreate view state any time new profiling data is imported.
48+
const viewState = useMemo<ViewState>(() => {
49+
const horizontalScrollStateChangeCallbacks: Set<HorizontalScrollStateChangeCallback> = new Set();
50+
51+
const horizontalScrollState = {
52+
offset: 0,
53+
length: 0,
54+
};
55+
56+
return {
57+
horizontalScrollState,
58+
onHorizontalScrollStateChange: callback => {
59+
horizontalScrollStateChangeCallbacks.add(callback);
60+
},
61+
updateHorizontalScrollState: scrollState => {
62+
if (
63+
horizontalScrollState.offset === scrollState.offset &&
64+
horizontalScrollState.length === scrollState.length
65+
) {
66+
return;
67+
}
68+
69+
horizontalScrollState.offset = scrollState.offset;
70+
horizontalScrollState.length = scrollState.length;
71+
72+
horizontalScrollStateChangeCallbacks.forEach(callback =>
73+
callback(scrollState),
74+
);
75+
},
76+
viewToMutableViewStateMap: new Map(),
77+
};
78+
}, [schedulingProfilerData]);
4679

4780
const value = useMemo(
4881
() => ({
4982
clearSchedulingProfilerData,
5083
importSchedulingProfilerData,
5184
schedulingProfilerData,
52-
// TODO (scheduling profiler)
85+
viewState,
5386
}),
5487
[
5588
clearSchedulingProfilerData,
5689
importSchedulingProfilerData,
5790
schedulingProfilerData,
58-
// TODO (scheduling profiler)
91+
viewState,
5992
],
6093
);
6194

0 commit comments

Comments
 (0)