Skip to content

Commit 40342c1

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 4cee86b commit 40342c1

File tree

8 files changed

+298
-196
lines changed

8 files changed

+298
-196
lines changed

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

Lines changed: 73 additions & 49 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, moveStateToRange} 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,40 @@ const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => {
103108
const zoomToBatch = (
104109
data: ReactProfilerData,
105110
measure: ReactMeasure,
106-
syncedHorizontalPanAndZoomViews: HorizontalPanAndZoomView[],
111+
viewState: ViewState,
112+
width: number,
107113
) => {
108114
const {batchUID} = measure;
109-
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+
const [rangeStart, rangeEnd] = getBatchRange(batchUID, data);
116+
117+
// Convert from time range to ScrollState
118+
const scrollState = moveStateToRange({
119+
state: viewState.horizontalScrollState,
120+
rangeStart,
121+
rangeEnd,
122+
contentLength: data.duration,
123+
124+
minContentLength: data.duration * MIN_ZOOM_LEVEL,
125+
maxContentLength: data.duration * MAX_ZOOM_LEVEL,
126+
containerLength: width,
127+
});
128+
129+
viewState.updateHorizontalScrollState(scrollState);
114130
};
115131

116132
type AutoSizedCanvasProps = {|
117133
data: ReactProfilerData,
118134
height: number,
135+
viewState: ViewState,
119136
width: number,
120137
|};
121138

122-
function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
139+
function AutoSizedCanvas({
140+
data,
141+
height,
142+
viewState,
143+
width,
144+
}: AutoSizedCanvasProps) {
123145
const canvasRef = useRef<HTMLCanvasElement | null>(null);
124146

125147
const [isContextMenuShown, setIsContextMenuShown] = useState<boolean>(false);
@@ -137,35 +159,31 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
137159
const componentMeasuresViewRef = useRef(null);
138160
const reactMeasuresViewRef = useRef(null);
139161
const flamechartViewRef = useRef(null);
140-
const syncedHorizontalPanAndZoomViewsRef = useRef<HorizontalPanAndZoomView[]>(
141-
[],
142-
);
143162

144163
const {hideMenu: hideContextMenu} = useContext(RegistryContext);
145164

146165
useLayoutEffect(() => {
147166
const surface = surfaceRef.current;
148167
const defaultFrame = {origin: zeroPoint, size: {width, height}};
149168

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

160-
syncedHorizontalPanAndZoomViewsRef.current.forEach(
161-
syncedView =>
162-
triggeringView !== syncedView && syncedView.setScrollState(newState),
163-
);
164-
};
174+
// Initialize horizontal view state
175+
viewState.updateHorizontalScrollState(
176+
clampState({
177+
state: viewState.horizontalScrollState,
178+
minContentLength: data.duration * MIN_ZOOM_LEVEL,
179+
maxContentLength: data.duration * MAX_ZOOM_LEVEL,
180+
containerLength: defaultFrame.size.width,
181+
}),
182+
);
165183

166184
function createViewHelper(
167185
view: View,
168-
resizeLabel: string = '',
186+
label: string,
169187
shouldScrollVertically: boolean = false,
170188
shouldResizeVertically: boolean = false,
171189
): View {
@@ -175,6 +193,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
175193
surface,
176194
defaultFrame,
177195
view,
196+
viewState,
197+
label,
178198
);
179199
}
180200

@@ -183,31 +203,30 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
183203
defaultFrame,
184204
verticalScrollView !== null ? verticalScrollView : view,
185205
data.duration,
186-
syncAllHorizontalPanAndZoomViewStates,
206+
viewState,
187207
);
188208

189-
syncedHorizontalPanAndZoomViewsRef.current.push(horizontalPanAndZoomView);
190-
191-
let viewToReturn = horizontalPanAndZoomView;
209+
let resizableView = null;
192210
if (shouldResizeVertically) {
193-
viewToReturn = new ResizableView(
211+
resizableView = new ResizableView(
194212
surface,
195213
defaultFrame,
196214
horizontalPanAndZoomView,
215+
viewState,
197216
canvasRef,
198-
resizeLabel,
217+
label,
199218
);
200219
}
201220

202-
return viewToReturn;
221+
return resizableView || horizontalPanAndZoomView;
203222
}
204223

205224
const axisMarkersView = new TimeAxisMarkersView(
206225
surface,
207226
defaultFrame,
208227
data.duration,
209228
);
210-
const axisMarkersViewWrapper = createViewHelper(axisMarkersView);
229+
const axisMarkersViewWrapper = createViewHelper(axisMarkersView, 'time');
211230

212231
let userTimingMarksViewWrapper = null;
213232
if (data.otherUserTimingMarks.length > 0) {
@@ -218,7 +237,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
218237
data.duration,
219238
);
220239
userTimingMarksViewRef.current = userTimingMarksView;
221-
userTimingMarksViewWrapper = createViewHelper(userTimingMarksView);
240+
userTimingMarksViewWrapper = createViewHelper(
241+
userTimingMarksView,
242+
'user timing api',
243+
);
222244
}
223245

224246
const nativeEventsView = new NativeEventsView(surface, defaultFrame, data);
@@ -236,7 +258,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
236258
data,
237259
);
238260
schedulingEventsViewRef.current = schedulingEventsView;
239-
const schedulingEventsViewWrapper = createViewHelper(schedulingEventsView);
261+
const schedulingEventsViewWrapper = createViewHelper(
262+
schedulingEventsView,
263+
'react updates',
264+
);
240265

241266
let suspenseEventsViewWrapper = null;
242267
if (data.suspenseEvents.length > 0) {
@@ -262,7 +287,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
262287
reactMeasuresViewRef.current = reactMeasuresView;
263288
const reactMeasuresViewWrapper = createViewHelper(
264289
reactMeasuresView,
265-
'react',
290+
'react scheduling',
266291
true,
267292
true,
268293
);
@@ -275,7 +300,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
275300
data,
276301
);
277302
componentMeasuresViewRef.current = componentMeasuresView;
278-
componentMeasuresViewWrapper = createViewHelper(componentMeasuresView);
303+
componentMeasuresViewWrapper = createViewHelper(
304+
componentMeasuresView,
305+
'react components',
306+
);
279307
}
280308

281309
const flamechartView = new FlamechartView(
@@ -335,7 +363,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
335363
return;
336364
}
337365

338-
// Wheel events should always hide the current toolltip.
366+
// Wheel events should always hide the current tooltip.
339367
switch (interaction.type) {
340368
case 'wheel-control':
341369
case 'wheel-meta':
@@ -623,11 +651,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
623651
{measure !== null && (
624652
<ContextMenuItem
625653
onClick={() =>
626-
zoomToBatch(
627-
contextData.data,
628-
measure,
629-
syncedHorizontalPanAndZoomViewsRef.current,
630-
)
654+
zoomToBatch(contextData.data, measure, viewState, width)
631655
}
632656
title="Zoom to batch">
633657
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)