Skip to content

Commit 5634ed1

Browse files
author
Brian Vaughn
authored
Scheduling Profiler: Misc UX and performance improvements (#22043)
1 parent ecd73e1 commit 5634ed1

File tree

12 files changed

+1076
-623
lines changed

12 files changed

+1076
-623
lines changed

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

Lines changed: 78 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,18 @@
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';
2119
import {
2220
Fragment,
21+
useContext,
2322
useEffect,
2423
useLayoutEffect,
2524
useRef,
@@ -54,29 +53,37 @@ import {
5453
UserTimingMarksView,
5554
} from './content-views';
5655
import {COLORS} from './content-views/constants';
57-
56+
import {clampState, moveStateToRange} from './view-base/utils/scrollState';
5857
import EventTooltip from './EventTooltip';
58+
import {RegistryContext} from 'react-devtools-shared/src/devtools/ContextMenu/Contexts';
5959
import ContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenu';
6060
import ContextMenuItem from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem';
6161
import useContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/useContextMenu';
6262
import {getBatchRange} from './utils/getBatchRange';
63+
import {MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL} from './view-base/constants';
6364

6465
import styles from './CanvasPage.css';
6566

6667
const CONTEXT_MENU_ID = 'canvas';
6768

6869
type Props = {|
6970
profilerData: ReactProfilerData,
71+
viewState: ViewState,
7072
|};
7173

72-
function CanvasPage({profilerData}: Props) {
74+
function CanvasPage({profilerData, viewState}: Props) {
7375
return (
7476
<div
7577
className={styles.CanvasPage}
7678
style={{backgroundColor: COLORS.BACKGROUND}}>
7779
<AutoSizer>
7880
{({height, width}: {height: number, width: number}) => (
79-
<AutoSizedCanvas data={profilerData} height={height} width={width} />
81+
<AutoSizedCanvas
82+
data={profilerData}
83+
height={height}
84+
viewState={viewState}
85+
width={width}
86+
/>
8087
)}
8188
</AutoSizer>
8289
</div>
@@ -98,27 +105,43 @@ const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => {
98105
);
99106
};
100107

101-
// TODO (scheduling profiler) Why is the "zoom" feature so much slower than normal rendering?
102108
const zoomToBatch = (
103109
data: ReactProfilerData,
104110
measure: ReactMeasure,
105-
syncedHorizontalPanAndZoomViews: HorizontalPanAndZoomView[],
111+
viewState: ViewState,
112+
width: number,
106113
) => {
107114
const {batchUID} = measure;
108-
const [startTime, stopTime] = getBatchRange(batchUID, data);
109-
syncedHorizontalPanAndZoomViews.forEach(syncedView =>
110-
// Using time as range works because the views' intrinsic content size is based on time.
111-
syncedView.zoomToRange(startTime, stopTime),
112-
);
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);
113130
};
114131

115132
type AutoSizedCanvasProps = {|
116133
data: ReactProfilerData,
117134
height: number,
135+
viewState: ViewState,
118136
width: number,
119137
|};
120138

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

124147
const [isContextMenuShown, setIsContextMenuShown] = useState<boolean>(false);
@@ -136,30 +159,31 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
136159
const componentMeasuresViewRef = useRef(null);
137160
const reactMeasuresViewRef = useRef(null);
138161
const flamechartViewRef = useRef(null);
139-
const syncedHorizontalPanAndZoomViewsRef = useRef<HorizontalPanAndZoomView[]>(
140-
[],
141-
);
162+
163+
const {hideMenu: hideContextMenu} = useContext(RegistryContext);
142164

143165
useLayoutEffect(() => {
144166
const surface = surfaceRef.current;
145167
const defaultFrame = {origin: zeroPoint, size: {width, height}};
146168

147-
// Clear synced views
148-
syncedHorizontalPanAndZoomViewsRef.current = [];
169+
// Auto hide context menu when panning.
170+
viewState.onHorizontalScrollStateChange(scrollState => {
171+
hideContextMenu();
172+
});
149173

150-
const syncAllHorizontalPanAndZoomViewStates: HorizontalPanAndZoomViewOnChangeCallback = (
151-
newState,
152-
triggeringView?: HorizontalPanAndZoomView,
153-
) => {
154-
syncedHorizontalPanAndZoomViewsRef.current.forEach(
155-
syncedView =>
156-
triggeringView !== syncedView && syncedView.setScrollState(newState),
157-
);
158-
};
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+
);
159183

160184
function createViewHelper(
161185
view: View,
162-
resizeLabel: string = '',
186+
label: string,
163187
shouldScrollVertically: boolean = false,
164188
shouldResizeVertically: boolean = false,
165189
): View {
@@ -169,6 +193,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
169193
surface,
170194
defaultFrame,
171195
view,
196+
viewState,
197+
label,
172198
);
173199
}
174200

@@ -177,31 +203,30 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
177203
defaultFrame,
178204
verticalScrollView !== null ? verticalScrollView : view,
179205
data.duration,
180-
syncAllHorizontalPanAndZoomViewStates,
206+
viewState,
181207
);
182208

183-
syncedHorizontalPanAndZoomViewsRef.current.push(horizontalPanAndZoomView);
184-
185-
let viewToReturn = horizontalPanAndZoomView;
209+
let resizableView = null;
186210
if (shouldResizeVertically) {
187-
viewToReturn = new ResizableView(
211+
resizableView = new ResizableView(
188212
surface,
189213
defaultFrame,
190214
horizontalPanAndZoomView,
215+
viewState,
191216
canvasRef,
192-
resizeLabel,
217+
label,
193218
);
194219
}
195220

196-
return viewToReturn;
221+
return resizableView || horizontalPanAndZoomView;
197222
}
198223

199224
const axisMarkersView = new TimeAxisMarkersView(
200225
surface,
201226
defaultFrame,
202227
data.duration,
203228
);
204-
const axisMarkersViewWrapper = createViewHelper(axisMarkersView);
229+
const axisMarkersViewWrapper = createViewHelper(axisMarkersView, 'time');
205230

206231
let userTimingMarksViewWrapper = null;
207232
if (data.otherUserTimingMarks.length > 0) {
@@ -212,7 +237,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
212237
data.duration,
213238
);
214239
userTimingMarksViewRef.current = userTimingMarksView;
215-
userTimingMarksViewWrapper = createViewHelper(userTimingMarksView);
240+
userTimingMarksViewWrapper = createViewHelper(
241+
userTimingMarksView,
242+
'user timing api',
243+
);
216244
}
217245

218246
const nativeEventsView = new NativeEventsView(surface, defaultFrame, data);
@@ -230,7 +258,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
230258
data,
231259
);
232260
schedulingEventsViewRef.current = schedulingEventsView;
233-
const schedulingEventsViewWrapper = createViewHelper(schedulingEventsView);
261+
const schedulingEventsViewWrapper = createViewHelper(
262+
schedulingEventsView,
263+
'react updates',
264+
);
234265

235266
let suspenseEventsViewWrapper = null;
236267
if (data.suspenseEvents.length > 0) {
@@ -256,7 +287,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
256287
reactMeasuresViewRef.current = reactMeasuresView;
257288
const reactMeasuresViewWrapper = createViewHelper(
258289
reactMeasuresView,
259-
'react',
290+
'react scheduling',
260291
true,
261292
true,
262293
);
@@ -269,7 +300,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
269300
data,
270301
);
271302
componentMeasuresViewRef.current = componentMeasuresView;
272-
componentMeasuresViewWrapper = createViewHelper(componentMeasuresView);
303+
componentMeasuresViewWrapper = createViewHelper(
304+
componentMeasuresView,
305+
'react components',
306+
);
273307
}
274308

275309
const flamechartView = new FlamechartView(
@@ -329,7 +363,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
329363
return;
330364
}
331365

332-
// Wheel events should always hide the current toolltip.
366+
// Wheel events should always hide the current tooltip.
333367
switch (interaction.type) {
334368
case 'wheel-control':
335369
case 'wheel-meta':
@@ -617,11 +651,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
617651
{measure !== null && (
618652
<ContextMenuItem
619653
onClick={() =>
620-
zoomToBatch(
621-
contextData.data,
622-
measure,
623-
syncedHorizontalPanAndZoomViewsRef.current,
624-
)
654+
zoomToBatch(contextData.data, measure, viewState, width)
625655
}
626656
title="Zoom to batch">
627657
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)