@samitouri / QOS-React-2 / commits / 5634ed16aa

Scheduling Profiler: Misc UX and performance improvements (#22043)

Brian Vaughn committed Aug 9, 2021 at 17:11 UTC 5634ed16aaba4a507844f0502953abcf40e3a165
12 files changed +1076 -623
packages/react-devtools-scheduling-profiler/src/CanvasPage.js
+78 -48
@@ -7,19 +7,18 @@
7 * @flow
8 */
9
10 -import type {
11 - Point,
12 - HorizontalPanAndZoomViewOnChangeCallback,
13 -} from './view-base';
10 +import type {Point} from './view-base';
11 import type {
12 ReactHoverContextInfo,
13 ReactProfilerData,
14 ReactMeasure,
15 + ViewState,
16 } from './types';
17
18 import * as React from 'react';
19 import {
20 Fragment,
21 + useContext,
22 useEffect,
23 useLayoutEffect,
24 useRef,
@@ -54,12 +53,14 @@ import {
53 UserTimingMarksView,
54 } from './content-views';
55 import {COLORS} from './content-views/constants';
57 -
56 +import {clampState, moveStateToRange} from './view-base/utils/scrollState';
57 import EventTooltip from './EventTooltip';
58 +import {RegistryContext} from 'react-devtools-shared/src/devtools/ContextMenu/Contexts';
59 import ContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenu';
60 import ContextMenuItem from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem';
61 import useContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/useContextMenu';
62 import {getBatchRange} from './utils/getBatchRange';
63 +import {MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL} from './view-base/constants';
64
65 import styles from './CanvasPage.css';
66
@@ -67,16 +68,22 @@ const CONTEXT_MENU_ID = 'canvas';
68
69 type Props = {|
70 profilerData: ReactProfilerData,
71 + viewState: ViewState,
72 |};
73
72 -function CanvasPage({profilerData}: Props) {
74 +function CanvasPage({profilerData, viewState}: Props) {
75 return (
76 <div
77 className={styles.CanvasPage}
78 style={{backgroundColor: COLORS.BACKGROUND}}>
79 <AutoSizer>
80 {({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 + />
87 )}
88 </AutoSizer>
89 </div>
@@ -98,27 +105,43 @@ const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => {
105 );
106 };
107
101 -// TODO (scheduling profiler) Why is the "zoom" feature so much slower than normal rendering?
108 const zoomToBatch = (
109 data: ReactProfilerData,
110 measure: ReactMeasure,
105 - syncedHorizontalPanAndZoomViews: HorizontalPanAndZoomView[],
111 + viewState: ViewState,
112 + width: number,
113 ) => {
114 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);
130 };
131
132 type AutoSizedCanvasProps = {|
133 data: ReactProfilerData,
134 height: number,
135 + viewState: ViewState,
136 width: number,
137 |};
138
121 -function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
139 +function AutoSizedCanvas({
140 + data,
141 + height,
142 + viewState,
143 + width,
144 +}: AutoSizedCanvasProps) {
145 const canvasRef = useRef<HTMLCanvasElement | null>(null);
146
147 const [isContextMenuShown, setIsContextMenuShown] = useState<boolean>(false);
@@ -136,30 +159,31 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
159 const componentMeasuresViewRef = useRef(null);
160 const reactMeasuresViewRef = useRef(null);
161 const flamechartViewRef = useRef(null);
139 - const syncedHorizontalPanAndZoomViewsRef = useRef<HorizontalPanAndZoomView[]>(
140 - [],
141 - );
162 +
163 + const {hideMenu: hideContextMenu} = useContext(RegistryContext);
164
165 useLayoutEffect(() => {
166 const surface = surfaceRef.current;
167 const defaultFrame = {origin: zeroPoint, size: {width, height}};
168
147 - // Clear synced views
148 - syncedHorizontalPanAndZoomViewsRef.current = [];
169 + // Auto hide context menu when panning.
170 + viewState.onHorizontalScrollStateChange(scrollState => {
171 + hideContextMenu();
172 + });
173
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 + );
183
184 function createViewHelper(
185 view: View,
162 - resizeLabel: string = '',
186 + label: string,
187 shouldScrollVertically: boolean = false,
188 shouldResizeVertically: boolean = false,
189 ): View {
@@ -169,6 +193,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
193 surface,
194 defaultFrame,
195 view,
196 + viewState,
197 + label,
198 );
199 }
200
@@ -177,23 +203,22 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
203 defaultFrame,
204 verticalScrollView !== null ? verticalScrollView : view,
205 data.duration,
180 - syncAllHorizontalPanAndZoomViewStates,
206 + viewState,
207 );
208
183 - syncedHorizontalPanAndZoomViewsRef.current.push(horizontalPanAndZoomView);
184 -
185 - let viewToReturn = horizontalPanAndZoomView;
209 + let resizableView = null;
210 if (shouldResizeVertically) {
187 - viewToReturn = new ResizableView(
211 + resizableView = new ResizableView(
212 surface,
213 defaultFrame,
214 horizontalPanAndZoomView,
215 + viewState,
216 canvasRef,
192 - resizeLabel,
217 + label,
218 );
219 }
220
196 - return viewToReturn;
221 + return resizableView || horizontalPanAndZoomView;
222 }
223
224 const axisMarkersView = new TimeAxisMarkersView(
@@ -201,7 +226,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
226 defaultFrame,
227 data.duration,
228 );
204 - const axisMarkersViewWrapper = createViewHelper(axisMarkersView);
229 + const axisMarkersViewWrapper = createViewHelper(axisMarkersView, 'time');
230
231 let userTimingMarksViewWrapper = null;
232 if (data.otherUserTimingMarks.length > 0) {
@@ -212,7 +237,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
237 data.duration,
238 );
239 userTimingMarksViewRef.current = userTimingMarksView;
215 - userTimingMarksViewWrapper = createViewHelper(userTimingMarksView);
240 + userTimingMarksViewWrapper = createViewHelper(
241 + userTimingMarksView,
242 + 'user timing api',
243 + );
244 }
245
246 const nativeEventsView = new NativeEventsView(surface, defaultFrame, data);
@@ -230,7 +258,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
258 data,
259 );
260 schedulingEventsViewRef.current = schedulingEventsView;
233 - const schedulingEventsViewWrapper = createViewHelper(schedulingEventsView);
261 + const schedulingEventsViewWrapper = createViewHelper(
262 + schedulingEventsView,
263 + 'react updates',
264 + );
265
266 let suspenseEventsViewWrapper = null;
267 if (data.suspenseEvents.length > 0) {
@@ -256,7 +287,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
287 reactMeasuresViewRef.current = reactMeasuresView;
288 const reactMeasuresViewWrapper = createViewHelper(
289 reactMeasuresView,
259 - 'react',
290 + 'react scheduling',
291 true,
292 true,
293 );
@@ -269,7 +300,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
300 data,
301 );
302 componentMeasuresViewRef.current = componentMeasuresView;
272 - componentMeasuresViewWrapper = createViewHelper(componentMeasuresView);
303 + componentMeasuresViewWrapper = createViewHelper(
304 + componentMeasuresView,
305 + 'react components',
306 + );
307 }
308
309 const flamechartView = new FlamechartView(
@@ -329,7 +363,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
363 return;
364 }
365
332 - // Wheel events should always hide the current toolltip.
366 + // Wheel events should always hide the current tooltip.
367 switch (interaction.type) {
368 case 'wheel-control':
369 case 'wheel-meta':
@@ -617,11 +651,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
651 {measure !== null && (
652 <ContextMenuItem
653 onClick={() =>
620 - zoomToBatch(
621 - contextData.data,
622 - measure,
623 - syncedHorizontalPanAndZoomViewsRef.current,
624 - )
654 + zoomToBatch(contextData.data, measure, viewState, width)
655 }
656 title="Zoom to batch">
657 Zoom to batch
packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js
+10 -4
@@ -8,6 +8,7 @@
8 */
9
10 import type {DataResource} from './createDataResourceFromImportedFile';
11 +import type {ViewState} from './types';
12
13 import * as React from 'react';
14 import {
@@ -27,9 +28,11 @@ import CanvasPage from './CanvasPage';
28 import styles from './SchedulingProfiler.css';
29
30 export function SchedulingProfiler(_: {||}) {
30 - const {importSchedulingProfilerData, schedulingProfilerData} = useContext(
31 - SchedulingProfilerContext,
32 - );
31 + const {
32 + importSchedulingProfilerData,
33 + schedulingProfilerData,
34 + viewState,
35 + } = useContext(SchedulingProfilerContext);
36
37 const ref = useRef(null);
38
@@ -66,6 +69,7 @@ export function SchedulingProfiler(_: {||}) {
69 dataResource={schedulingProfilerData}
70 key={key}
71 onFileSelect={importSchedulingProfilerData}
72 + viewState={viewState}
73 />
74 </Suspense>
75 ) : (
@@ -130,9 +134,11 @@ const CouldNotLoadProfile = ({error, onFileSelect}) => (
134 const DataResourceComponent = ({
135 dataResource,
136 onFileSelect,
137 + viewState,
138 }: {|
139 dataResource: DataResource,
140 onFileSelect: (file: File) => void,
141 + viewState: ViewState,
142 |}) => {
143 const dataOrError = dataResource.read();
144 if (dataOrError instanceof Error) {
@@ -140,5 +146,5 @@ const DataResourceComponent = ({
146 <CouldNotLoadProfile error={dataOrError} onFileSelect={onFileSelect} />
147 );
148 }
143 - return <CanvasPage profilerData={dataOrError} />;
149 + return <CanvasPage profilerData={dataOrError} viewState={viewState} />;
150 };
packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js
+36 -3
@@ -11,12 +11,14 @@ import * as React from 'react';
11 import {createContext, useCallback, useMemo, useState} from 'react';
12 import createDataResourceFromImportedFile from './createDataResourceFromImportedFile';
13
14 +import type {HorizontalScrollStateChangeCallback, ViewState} from './types';
15 import type {DataResource} from './createDataResourceFromImportedFile';
16
17 export type Context = {|
18 clearSchedulingProfilerData: () => void,
19 importSchedulingProfilerData: (file: File) => void,
20 schedulingProfilerData: DataResource | null,
21 + viewState: ViewState,
22 |};
23
24 const SchedulingProfilerContext = createContext<Context>(
@@ -42,20 +44,51 @@ function SchedulingProfilerContextController({children}: Props) {
44 setSchedulingProfilerData(createDataResourceFromImportedFile(file));
45 }, []);
46
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]);
79
80 const value = useMemo(
81 () => ({
82 clearSchedulingProfilerData,
83 importSchedulingProfilerData,
84 schedulingProfilerData,
52 - // TODO (scheduling profiler)
85 + viewState,
86 }),
87 [
88 clearSchedulingProfilerData,
89 importSchedulingProfilerData,
90 schedulingProfilerData,
58 - // TODO (scheduling profiler)
91 + viewState,
92 ],
93 );
94
packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js
+9 -21
@@ -38,21 +38,12 @@ import {REACT_TOTAL_NUM_LANES} from '../constants';
38 const REACT_LANE_HEIGHT = REACT_MEASURE_HEIGHT + BORDER_SIZE;
39 const MAX_ROWS_TO_SHOW_INITIALLY = 5;
40
41 -function getMeasuresForLane(
42 - allMeasures: ReactMeasure[],
43 - lane: ReactLane,
44 -): ReactMeasure[] {
45 - return allMeasures.filter(measure => measure.lanes.includes(lane));
46 -}
47 -
41 export class ReactMeasuresView extends View {
49 - _profilerData: ReactProfilerData;
42 _intrinsicSize: IntrinsicSize;
51 -
43 _lanesToRender: ReactLane[];
53 - _laneToMeasures: Map<ReactLane, ReactMeasure[]>;
54 -
44 + _profilerData: ReactProfilerData;
45 _hoveredMeasure: ReactMeasure | null = null;
46 +
47 onHover: ((measure: ReactMeasure | null) => void) | null = null;
48
49 constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
@@ -63,17 +54,14 @@ export class ReactMeasuresView extends View {
54
55 _performPreflightComputations() {
56 this._lanesToRender = [];
66 - this._laneToMeasures = new Map();
57
58 for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) {
69 - const measuresForLane = getMeasuresForLane(
70 - this._profilerData.measures,
59 + const measuresForLane = this._profilerData.laneToReactMeasureMap.get(
60 lane,
61 );
62 // Only show lanes with measures
74 - if (measuresForLane.length) {
63 + if (measuresForLane != null && measuresForLane.length > 0) {
64 this._lanesToRender.push(lane);
76 - this._laneToMeasures.set(lane, measuresForLane);
65 }
66 }
67
@@ -213,7 +201,7 @@ export class ReactMeasuresView extends View {
201 frame,
202 _hoveredMeasure,
203 _lanesToRender,
216 - _laneToMeasures,
204 + _profilerData,
205 visibleArea,
206 } = this;
207
@@ -233,7 +221,7 @@ export class ReactMeasuresView extends View {
221 for (let i = 0; i < _lanesToRender.length; i++) {
222 const lane = _lanesToRender[i];
223 const baseY = frame.origin.y + i * REACT_LANE_HEIGHT;
236 - const measuresForLane = _laneToMeasures.get(lane);
224 + const measuresForLane = _profilerData.laneToReactMeasureMap.get(lane);
225
226 if (!measuresForLane) {
227 throw new Error(
@@ -242,7 +230,7 @@ export class ReactMeasuresView extends View {
230 }
231
232 // Render lane labels
245 - const label = this._profilerData.laneToLabelMap.get(lane);
233 + const label = _profilerData.laneToLabelMap.get(lane);
234 if (label == null) {
235 console.warn(`Could not find label for lane ${lane}.`);
236 } else {
@@ -316,8 +304,8 @@ export class ReactMeasuresView extends View {
304 frame,
305 _intrinsicSize,
306 _lanesToRender,
319 - _laneToMeasures,
307 onHover,
308 + _profilerData,
309 visibleArea,
310 } = this;
311 if (!onHover) {
@@ -347,7 +335,7 @@ export class ReactMeasuresView extends View {
335 // This will always be the one on "top" (the one the user is hovering over).
336 const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
337 const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
350 - const measures = _laneToMeasures.get(lane);
338 + const measures = _profilerData.laneToReactMeasureMap.get(lane);
339 if (!measures) {
340 onHover(null);
341 return;
packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
+692 -369
@@ -253,6 +253,7 @@ describe(preprocessData, () => {
253 ]);
254 expect(data).toMatchInlineSnapshot(`
255 Object {
256 + "batchUIDToMeasuresMap": Map {},
257 "componentMeasures": Array [],
258 "duration": 0.005,
259 "flamechart": Array [],
@@ -289,7 +290,39 @@ describe(preprocessData, () => {
290 29 => "Idle",
291 30 => "Offscreen",
292 },
292 - "measures": Array [],
293 + "laneToReactMeasureMap": Map {
294 + 0 => Array [],
295 + 1 => Array [],
296 + 2 => Array [],
297 + 3 => Array [],
298 + 4 => Array [],
299 + 5 => Array [],
300 + 6 => Array [],
301 + 7 => Array [],
302 + 8 => Array [],
303 + 9 => Array [],
304 + 10 => Array [],
305 + 11 => Array [],
306 + 12 => Array [],
307 + 13 => Array [],
308 + 14 => Array [],
309 + 15 => Array [],
310 + 16 => Array [],
311 + 17 => Array [],
312 + 18 => Array [],
313 + 19 => Array [],
314 + 20 => Array [],
315 + 21 => Array [],
316 + 22 => Array [],
317 + 23 => Array [],
318 + 24 => Array [],
319 + 25 => Array [],
320 + 26 => Array [],
321 + 27 => Array [],
322 + 28 => Array [],
323 + 29 => Array [],
324 + 30 => Array [],
325 + },
326 "nativeEvents": Array [],
327 "otherUserTimingMarks": Array [],
328 "reactVersion": "17.0.3",
@@ -340,102 +373,178 @@ describe(preprocessData, () => {
373 }),
374 ]);
375 expect(data).toMatchInlineSnapshot(`
376 + Object {
377 + "batchUIDToMeasuresMap": Map {
378 + 0 => Array [
379 + Object {
380 + "batchUID": 0,
381 + "depth": 0,
382 + "duration": 0.004999999999999999,
383 + "lanes": Array [
384 + 9,
385 + ],
386 + "timestamp": 0.006,
387 + "type": "render-idle",
388 + },
389 + Object {
390 + "batchUID": 0,
391 + "depth": 0,
392 + "duration": 0.001,
393 + "lanes": Array [
394 + 9,
395 + ],
396 + "timestamp": 0.006,
397 + "type": "render",
398 + },
399 + Object {
400 + "batchUID": 0,
401 + "depth": 0,
402 + "duration": 0.002999999999999999,
403 + "lanes": Array [
404 + 9,
405 + ],
406 + "timestamp": 0.008,
407 + "type": "commit",
408 + },
409 + Object {
410 + "batchUID": 0,
411 + "depth": 1,
412 + "duration": 0.0010000000000000009,
413 + "lanes": Array [
414 + 9,
415 + ],
416 + "timestamp": 0.009,
417 + "type": "layout-effects",
418 + },
419 + ],
420 + },
421 + "componentMeasures": Array [],
422 + "duration": 0.011,
423 + "flamechart": Array [],
424 + "laneToLabelMap": Map {
425 + 0 => "Sync",
426 + 1 => "InputContinuousHydration",
427 + 2 => "InputContinuous",
428 + 3 => "DefaultHydration",
429 + 4 => "Default",
430 + 5 => "TransitionHydration",
431 + 6 => "Transition",
432 + 7 => "Transition",
433 + 8 => "Transition",
434 + 9 => "Transition",
435 + 10 => "Transition",
436 + 11 => "Transition",
437 + 12 => "Transition",
438 + 13 => "Transition",
439 + 14 => "Transition",
440 + 15 => "Transition",
441 + 16 => "Transition",
442 + 17 => "Transition",
443 + 18 => "Transition",
444 + 19 => "Transition",
445 + 20 => "Transition",
446 + 21 => "Transition",
447 + 22 => "Retry",
448 + 23 => "Retry",
449 + 24 => "Retry",
450 + 25 => "Retry",
451 + 26 => "Retry",
452 + 27 => "SelectiveHydration",
453 + 28 => "IdleHydration",
454 + 29 => "Idle",
455 + 30 => "Offscreen",
456 + },
457 + "laneToReactMeasureMap": Map {
458 + 0 => Array [],
459 + 1 => Array [],
460 + 2 => Array [],
461 + 3 => Array [],
462 + 4 => Array [],
463 + 5 => Array [],
464 + 6 => Array [],
465 + 7 => Array [],
466 + 8 => Array [],
467 + 9 => Array [
468 + Object {
469 + "batchUID": 0,
470 + "depth": 0,
471 + "duration": 0.004999999999999999,
472 + "lanes": Array [
473 + 9,
474 + ],
475 + "timestamp": 0.006,
476 + "type": "render-idle",
477 + },
478 Object {
344 - "componentMeasures": Array [],
345 - "duration": 0.011,
346 - "flamechart": Array [],
347 - "laneToLabelMap": Map {
348 - 0 => "Sync",
349 - 1 => "InputContinuousHydration",
350 - 2 => "InputContinuous",
351 - 3 => "DefaultHydration",
352 - 4 => "Default",
353 - 5 => "TransitionHydration",
354 - 6 => "Transition",
355 - 7 => "Transition",
356 - 8 => "Transition",
357 - 9 => "Transition",
358 - 10 => "Transition",
359 - 11 => "Transition",
360 - 12 => "Transition",
361 - 13 => "Transition",
362 - 14 => "Transition",
363 - 15 => "Transition",
364 - 16 => "Transition",
365 - 17 => "Transition",
366 - 18 => "Transition",
367 - 19 => "Transition",
368 - 20 => "Transition",
369 - 21 => "Transition",
370 - 22 => "Retry",
371 - 23 => "Retry",
372 - 24 => "Retry",
373 - 25 => "Retry",
374 - 26 => "Retry",
375 - 27 => "SelectiveHydration",
376 - 28 => "IdleHydration",
377 - 29 => "Idle",
378 - 30 => "Offscreen",
379 - },
380 - "measures": Array [
381 - Object {
382 - "batchUID": 0,
383 - "depth": 0,
384 - "duration": 0.004999999999999999,
385 - "lanes": Array [
386 - 9,
387 - ],
388 - "timestamp": 0.006,
389 - "type": "render-idle",
390 - },
391 - Object {
392 - "batchUID": 0,
393 - "depth": 0,
394 - "duration": 0.001,
395 - "lanes": Array [
396 - 9,
397 - ],
398 - "timestamp": 0.006,
399 - "type": "render",
400 - },
401 - Object {
402 - "batchUID": 0,
403 - "depth": 0,
404 - "duration": 0.002999999999999999,
405 - "lanes": Array [
406 - 9,
407 - ],
408 - "timestamp": 0.008,
409 - "type": "commit",
410 - },
411 - Object {
412 - "batchUID": 0,
413 - "depth": 1,
414 - "duration": 0.0010000000000000009,
415 - "lanes": Array [
416 - 9,
417 - ],
418 - "timestamp": 0.009,
419 - "type": "layout-effects",
420 - },
479 + "batchUID": 0,
480 + "depth": 0,
481 + "duration": 0.001,
482 + "lanes": Array [
483 + 9,
484 ],
422 - "nativeEvents": Array [],
423 - "otherUserTimingMarks": Array [],
424 - "reactVersion": "17.0.3",
425 - "schedulingEvents": Array [
426 - Object {
427 - "lanes": Array [
428 - 9,
429 - ],
430 - "timestamp": 0.005,
431 - "type": "schedule-render",
432 - "warning": null,
433 - },
485 + "timestamp": 0.006,
486 + "type": "render",
487 + },
488 + Object {
489 + "batchUID": 0,
490 + "depth": 0,
491 + "duration": 0.002999999999999999,
492 + "lanes": Array [
493 + 9,
494 + ],
495 + "timestamp": 0.008,
496 + "type": "commit",
497 + },
498 + Object {
499 + "batchUID": 0,
500 + "depth": 1,
501 + "duration": 0.0010000000000000009,
502 + "lanes": Array [
503 + 9,
504 ],
435 - "startTime": 1,
436 - "suspenseEvents": Array [],
437 - }
438 - `);
505 + "timestamp": 0.009,
506 + "type": "layout-effects",
507 + },
508 + ],
509 + 10 => Array [],
510 + 11 => Array [],
511 + 12 => Array [],
512 + 13 => Array [],
513 + 14 => Array [],
514 + 15 => Array [],
515 + 16 => Array [],
516 + 17 => Array [],
517 + 18 => Array [],
518 + 19 => Array [],
519 + 20 => Array [],
520 + 21 => Array [],
521 + 22 => Array [],
522 + 23 => Array [],
523 + 24 => Array [],
524 + 25 => Array [],
525 + 26 => Array [],
526 + 27 => Array [],
527 + 28 => Array [],
528 + 29 => Array [],
529 + 30 => Array [],
530 + },
531 + "nativeEvents": Array [],
532 + "otherUserTimingMarks": Array [],
533 + "reactVersion": "17.0.3",
534 + "schedulingEvents": Array [
535 + Object {
536 + "lanes": Array [
537 + 9,
538 + ],
539 + "timestamp": 0.005,
540 + "type": "schedule-render",
541 + "warning": null,
542 + },
543 + ],
544 + "startTime": 1,
545 + "suspenseEvents": Array [],
546 + }
547 + `);
548 }
549 });
550
@@ -448,107 +557,183 @@ describe(preprocessData, () => {
557 ...createUserTimingData(clearedMarks),
558 ]);
559 expect(data).toMatchInlineSnapshot(`
560 + Object {
561 + "batchUIDToMeasuresMap": Map {
562 + 0 => Array [
563 Object {
452 - "componentMeasures": Array [],
453 - "duration": 0.013,
454 - "flamechart": Array [],
455 - "laneToLabelMap": Map {
456 - 0 => "Sync",
457 - 1 => "InputContinuousHydration",
458 - 2 => "InputContinuous",
459 - 3 => "DefaultHydration",
460 - 4 => "Default",
461 - 5 => "TransitionHydration",
462 - 6 => "Transition",
463 - 7 => "Transition",
464 - 8 => "Transition",
465 - 9 => "Transition",
466 - 10 => "Transition",
467 - 11 => "Transition",
468 - 12 => "Transition",
469 - 13 => "Transition",
470 - 14 => "Transition",
471 - 15 => "Transition",
472 - 16 => "Transition",
473 - 17 => "Transition",
474 - 18 => "Transition",
475 - 19 => "Transition",
476 - 20 => "Transition",
477 - 21 => "Transition",
478 - 22 => "Retry",
479 - 23 => "Retry",
480 - 24 => "Retry",
481 - 25 => "Retry",
482 - 26 => "Retry",
483 - 27 => "SelectiveHydration",
484 - 28 => "IdleHydration",
485 - 29 => "Idle",
486 - 30 => "Offscreen",
487 - },
488 - "measures": Array [
489 - Object {
490 - "batchUID": 0,
491 - "depth": 0,
492 - "duration": 0.008,
493 - "lanes": Array [
494 - 0,
495 - ],
496 - "timestamp": 0.005,
497 - "type": "render-idle",
498 - },
499 - Object {
500 - "batchUID": 0,
501 - "depth": 0,
502 - "duration": 0.001,
503 - "lanes": Array [
504 - 0,
505 - ],
506 - "timestamp": 0.005,
507 - "type": "render",
508 - },
509 - Object {
510 - "batchUID": 0,
511 - "depth": 0,
512 - "duration": 0.005999999999999999,
513 - "lanes": Array [
514 - 0,
515 - ],
516 - "timestamp": 0.007,
517 - "type": "commit",
518 - },
519 - Object {
520 - "batchUID": 0,
521 - "depth": 1,
522 - "duration": 0.0010000000000000009,
523 - "lanes": Array [
524 - 0,
525 - ],
526 - "timestamp": 0.011,
527 - "type": "layout-effects",
528 - },
564 + "batchUID": 0,
565 + "depth": 0,
566 + "duration": 0.008,
567 + "lanes": Array [
568 + 0,
569 ],
530 - "nativeEvents": Array [],
531 - "otherUserTimingMarks": Array [
532 - Object {
533 - "name": "__v3",
534 - "timestamp": 0.003,
535 - },
570 + "timestamp": 0.005,
571 + "type": "render-idle",
572 + },
573 + Object {
574 + "batchUID": 0,
575 + "depth": 0,
576 + "duration": 0.001,
577 + "lanes": Array [
578 + 0,
579 + ],
580 + "timestamp": 0.005,
581 + "type": "render",
582 + },
583 + Object {
584 + "batchUID": 0,
585 + "depth": 0,
586 + "duration": 0.005999999999999999,
587 + "lanes": Array [
588 + 0,
589 ],
537 - "reactVersion": "17.0.3",
538 - "schedulingEvents": Array [
539 - Object {
540 - "lanes": Array [
541 - 0,
542 - ],
543 - "timestamp": 0.004,
544 - "type": "schedule-render",
545 - "warning": null,
546 - },
590 + "timestamp": 0.007,
591 + "type": "commit",
592 + },
593 + Object {
594 + "batchUID": 0,
595 + "depth": 1,
596 + "duration": 0.0010000000000000009,
597 + "lanes": Array [
598 + 0,
599 ],
548 - "startTime": 4,
549 - "suspenseEvents": Array [],
550 - }
551 - `);
600 + "timestamp": 0.011,
601 + "type": "layout-effects",
602 + },
603 + ],
604 + },
605 + "componentMeasures": Array [],
606 + "duration": 0.013,
607 + "flamechart": Array [],
608 + "laneToLabelMap": Map {
609 + 0 => "Sync",
610 + 1 => "InputContinuousHydration",
611 + 2 => "InputContinuous",
612 + 3 => "DefaultHydration",
613 + 4 => "Default",
614 + 5 => "TransitionHydration",
615 + 6 => "Transition",
616 + 7 => "Transition",
617 + 8 => "Transition",
618 + 9 => "Transition",
619 + 10 => "Transition",
620 + 11 => "Transition",
621 + 12 => "Transition",
622 + 13 => "Transition",
623 + 14 => "Transition",
624 + 15 => "Transition",
625 + 16 => "Transition",
626 + 17 => "Transition",
627 + 18 => "Transition",
628 + 19 => "Transition",
629 + 20 => "Transition",
630 + 21 => "Transition",
631 + 22 => "Retry",
632 + 23 => "Retry",
633 + 24 => "Retry",
634 + 25 => "Retry",
635 + 26 => "Retry",
636 + 27 => "SelectiveHydration",
637 + 28 => "IdleHydration",
638 + 29 => "Idle",
639 + 30 => "Offscreen",
640 + },
641 + "laneToReactMeasureMap": Map {
642 + 0 => Array [
643 + Object {
644 + "batchUID": 0,
645 + "depth": 0,
646 + "duration": 0.008,
647 + "lanes": Array [
648 + 0,
649 + ],
650 + "timestamp": 0.005,
651 + "type": "render-idle",
652 + },
653 + Object {
654 + "batchUID": 0,
655 + "depth": 0,
656 + "duration": 0.001,
657 + "lanes": Array [
658 + 0,
659 + ],
660 + "timestamp": 0.005,
661 + "type": "render",
662 + },
663 + Object {
664 + "batchUID": 0,
665 + "depth": 0,
666 + "duration": 0.005999999999999999,
667 + "lanes": Array [
668 + 0,
669 + ],
670 + "timestamp": 0.007,
671 + "type": "commit",
672 + },
673 + Object {
674 + "batchUID": 0,
675 + "depth": 1,
676 + "duration": 0.0010000000000000009,
677 + "lanes": Array [
678 + 0,
679 + ],
680 + "timestamp": 0.011,
681 + "type": "layout-effects",
682 + },
683 + ],
684 + 1 => Array [],
685 + 2 => Array [],
686 + 3 => Array [],
687 + 4 => Array [],
688 + 5 => Array [],
689 + 6 => Array [],
690 + 7 => Array [],
691 + 8 => Array [],
692 + 9 => Array [],
693 + 10 => Array [],
694 + 11 => Array [],
695 + 12 => Array [],
696 + 13 => Array [],
697 + 14 => Array [],
698 + 15 => Array [],
699 + 16 => Array [],
700 + 17 => Array [],
701 + 18 => Array [],
702 + 19 => Array [],
703 + 20 => Array [],
704 + 21 => Array [],
705 + 22 => Array [],
706 + 23 => Array [],
707 + 24 => Array [],
708 + 25 => Array [],
709 + 26 => Array [],
710 + 27 => Array [],
711 + 28 => Array [],
712 + 29 => Array [],
713 + 30 => Array [],
714 + },
715 + "nativeEvents": Array [],
716 + "otherUserTimingMarks": Array [
717 + Object {
718 + "name": "__v3",
719 + "timestamp": 0.003,
720 + },
721 + ],
722 + "reactVersion": "17.0.3",
723 + "schedulingEvents": Array [
724 + Object {
725 + "lanes": Array [
726 + 0,
727 + ],
728 + "timestamp": 0.004,
729 + "type": "schedule-render",
730 + "warning": null,
731 + },
732 + ],
733 + "startTime": 4,
734 + "suspenseEvents": Array [],
735 + }
736 + `);
737 }
738 });
739
@@ -572,189 +757,327 @@ describe(preprocessData, () => {
757 ...createUserTimingData(clearedMarks),
758 ]);
759 expect(data).toMatchInlineSnapshot(`
760 + Object {
761 + "batchUIDToMeasuresMap": Map {
762 + 0 => Array [
763 + Object {
764 + "batchUID": 0,
765 + "depth": 0,
766 + "duration": 0.009999999999999998,
767 + "lanes": Array [
768 + 4,
769 + ],
770 + "timestamp": 0.005,
771 + "type": "render-idle",
772 + },
773 + Object {
774 + "batchUID": 0,
775 + "depth": 0,
776 + "duration": 0.003,
777 + "lanes": Array [
778 + 4,
779 + ],
780 + "timestamp": 0.005,
781 + "type": "render",
782 + },
783 + Object {
784 + "batchUID": 0,
785 + "depth": 0,
786 + "duration": 0.006,
787 + "lanes": Array [
788 + 4,
789 + ],
790 + "timestamp": 0.009,
791 + "type": "commit",
792 + },
793 + Object {
794 + "batchUID": 0,
795 + "depth": 1,
796 + "duration": 0.0010000000000000009,
797 + "lanes": Array [
798 + 4,
799 + ],
800 + "timestamp": 0.013,
801 + "type": "layout-effects",
802 + },
803 + Object {
804 + "batchUID": 0,
805 + "depth": 0,
806 + "duration": 0.0019999999999999983,
807 + "lanes": Array [
808 + 4,
809 + ],
810 + "timestamp": 0.016,
811 + "type": "passive-effects",
812 + },
813 + ],
814 + 1 => Array [
815 + Object {
816 + "batchUID": 1,
817 + "depth": 0,
818 + "duration": 0.010000000000000002,
819 + "lanes": Array [
820 + 4,
821 + ],
822 + "timestamp": 0.019,
823 + "type": "render-idle",
824 + },
825 Object {
576 - "componentMeasures": Array [
577 - Object {
578 - "componentName": "App",
579 - "duration": 0.001,
580 - "timestamp": 0.006,
581 - "warning": null,
582 - },
583 - Object {
584 - "componentName": "App",
585 - "duration": 0.0010000000000000009,
586 - "timestamp": 0.02,
587 - "warning": null,
588 - },
826 + "batchUID": 1,
827 + "depth": 0,
828 + "duration": 0.002999999999999999,
829 + "lanes": Array [
830 + 4,
831 ],
590 - "duration": 0.031,
591 - "flamechart": Array [],
592 - "laneToLabelMap": Map {
593 - 0 => "Sync",
594 - 1 => "InputContinuousHydration",
595 - 2 => "InputContinuous",
596 - 3 => "DefaultHydration",
597 - 4 => "Default",
598 - 5 => "TransitionHydration",
599 - 6 => "Transition",
600 - 7 => "Transition",
601 - 8 => "Transition",
602 - 9 => "Transition",
603 - 10 => "Transition",
604 - 11 => "Transition",
605 - 12 => "Transition",
606 - 13 => "Transition",
607 - 14 => "Transition",
608 - 15 => "Transition",
609 - 16 => "Transition",
610 - 17 => "Transition",
611 - 18 => "Transition",
612 - 19 => "Transition",
613 - 20 => "Transition",
614 - 21 => "Transition",
615 - 22 => "Retry",
616 - 23 => "Retry",
617 - 24 => "Retry",
618 - 25 => "Retry",
619 - 26 => "Retry",
620 - 27 => "SelectiveHydration",
621 - 28 => "IdleHydration",
622 - 29 => "Idle",
623 - 30 => "Offscreen",
624 - },
625 - "measures": Array [
626 - Object {
627 - "batchUID": 0,
628 - "depth": 0,
629 - "duration": 0.009999999999999998,
630 - "lanes": Array [
631 - 4,
632 - ],
633 - "timestamp": 0.005,
634 - "type": "render-idle",
635 - },
636 - Object {
637 - "batchUID": 0,
638 - "depth": 0,
639 - "duration": 0.003,
640 - "lanes": Array [
641 - 4,
642 - ],
643 - "timestamp": 0.005,
644 - "type": "render",
645 - },
646 - Object {
647 - "batchUID": 0,
648 - "depth": 0,
649 - "duration": 0.006,
650 - "lanes": Array [
651 - 4,
652 - ],
653 - "timestamp": 0.009,
654 - "type": "commit",
655 - },
656 - Object {
657 - "batchUID": 0,
658 - "depth": 1,
659 - "duration": 0.0010000000000000009,
660 - "lanes": Array [
661 - 4,
662 - ],
663 - "timestamp": 0.013,
664 - "type": "layout-effects",
665 - },
666 - Object {
667 - "batchUID": 0,
668 - "depth": 0,
669 - "duration": 0.0019999999999999983,
670 - "lanes": Array [
671 - 4,
672 - ],
673 - "timestamp": 0.016,
674 - "type": "passive-effects",
675 - },
676 - Object {
677 - "batchUID": 1,
678 - "depth": 0,
679 - "duration": 0.010000000000000002,
680 - "lanes": Array [
681 - 4,
682 - ],
683 - "timestamp": 0.019,
684 - "type": "render-idle",
685 - },
686 - Object {
687 - "batchUID": 1,
688 - "depth": 0,
689 - "duration": 0.002999999999999999,
690 - "lanes": Array [
691 - 4,
692 - ],
693 - "timestamp": 0.019,
694 - "type": "render",
695 - },
696 - Object {
697 - "batchUID": 1,
698 - "depth": 0,
699 - "duration": 0.006000000000000002,
700 - "lanes": Array [
701 - 4,
702 - ],
703 - "timestamp": 0.023,
704 - "type": "commit",
705 - },
706 - Object {
707 - "batchUID": 1,
708 - "depth": 1,
709 - "duration": 0.0010000000000000009,
710 - "lanes": Array [
711 - 4,
712 - ],
713 - "timestamp": 0.027,
714 - "type": "layout-effects",
715 - },
716 - Object {
717 - "batchUID": 1,
718 - "depth": 0,
719 - "duration": 0.0010000000000000009,
720 - "lanes": Array [
721 - 4,
722 - ],
723 - "timestamp": 0.03,
724 - "type": "passive-effects",
725 - },
832 + "timestamp": 0.019,
833 + "type": "render",
834 + },
835 + Object {
836 + "batchUID": 1,
837 + "depth": 0,
838 + "duration": 0.006000000000000002,
839 + "lanes": Array [
840 + 4,
841 ],
727 - "nativeEvents": Array [],
728 - "otherUserTimingMarks": Array [
729 - Object {
730 - "name": "__v3",
731 - "timestamp": 0.003,
732 - },
842 + "timestamp": 0.023,
843 + "type": "commit",
844 + },
845 + Object {
846 + "batchUID": 1,
847 + "depth": 1,
848 + "duration": 0.0010000000000000009,
849 + "lanes": Array [
850 + 4,
851 ],
734 - "reactVersion": "17.0.3",
735 - "schedulingEvents": Array [
736 - Object {
737 - "lanes": Array [
738 - 4,
739 - ],
740 - "timestamp": 0.004,
741 - "type": "schedule-render",
742 - "warning": null,
743 - },
744 - Object {
745 - "componentName": "App",
746 - "lanes": Array [
747 - 4,
748 - ],
749 - "timestamp": 0.017,
750 - "type": "schedule-state-update",
751 - "warning": null,
752 - },
852 + "timestamp": 0.027,
853 + "type": "layout-effects",
854 + },
855 + Object {
856 + "batchUID": 1,
857 + "depth": 0,
858 + "duration": 0.0010000000000000009,
859 + "lanes": Array [
860 + 4,
861 ],
754 - "startTime": 4,
755 - "suspenseEvents": Array [],
756 - }
757 - `);
862 + "timestamp": 0.03,
863 + "type": "passive-effects",
864 + },
865 + ],
866 + },
867 + "componentMeasures": Array [
868 + Object {
869 + "componentName": "App",
870 + "duration": 0.001,
871 + "timestamp": 0.006,
872 + "warning": null,
873 + },
874 + Object {
875 + "componentName": "App",
876 + "duration": 0.0010000000000000009,
877 + "timestamp": 0.02,
878 + "warning": null,
879 + },
880 + ],
881 + "duration": 0.031,
882 + "flamechart": Array [],
883 + "laneToLabelMap": Map {
884 + 0 => "Sync",
885 + 1 => "InputContinuousHydration",
886 + 2 => "InputContinuous",
887 + 3 => "DefaultHydration",
888 + 4 => "Default",
889 + 5 => "TransitionHydration",
890 + 6 => "Transition",
891 + 7 => "Transition",
892 + 8 => "Transition",
893 + 9 => "Transition",
894 + 10 => "Transition",
895 + 11 => "Transition",
896 + 12 => "Transition",
897 + 13 => "Transition",
898 + 14 => "Transition",
899 + 15 => "Transition",
900 + 16 => "Transition",
901 + 17 => "Transition",
902 + 18 => "Transition",
903 + 19 => "Transition",
904 + 20 => "Transition",
905 + 21 => "Transition",
906 + 22 => "Retry",
907 + 23 => "Retry",
908 + 24 => "Retry",
909 + 25 => "Retry",
910 + 26 => "Retry",
911 + 27 => "SelectiveHydration",
912 + 28 => "IdleHydration",
913 + 29 => "Idle",
914 + 30 => "Offscreen",
915 + },
916 + "laneToReactMeasureMap": Map {
917 + 0 => Array [],
918 + 1 => Array [],
919 + 2 => Array [],
920 + 3 => Array [],
921 + 4 => Array [
922 + Object {
923 + "batchUID": 0,
924 + "depth": 0,
925 + "duration": 0.009999999999999998,
926 + "lanes": Array [
927 + 4,
928 + ],
929 + "timestamp": 0.005,
930 + "type": "render-idle",
931 + },
932 + Object {
933 + "batchUID": 0,
934 + "depth": 0,
935 + "duration": 0.003,
936 + "lanes": Array [
937 + 4,
938 + ],
939 + "timestamp": 0.005,
940 + "type": "render",
941 + },
942 + Object {
943 + "batchUID": 0,
944 + "depth": 0,
945 + "duration": 0.006,
946 + "lanes": Array [
947 + 4,
948 + ],
949 + "timestamp": 0.009,
950 + "type": "commit",
951 + },
952 + Object {
953 + "batchUID": 0,
954 + "depth": 1,
955 + "duration": 0.0010000000000000009,
956 + "lanes": Array [
957 + 4,
958 + ],
959 + "timestamp": 0.013,
960 + "type": "layout-effects",
961 + },
962 + Object {
963 + "batchUID": 0,
964 + "depth": 0,
965 + "duration": 0.0019999999999999983,
966 + "lanes": Array [
967 + 4,
968 + ],
969 + "timestamp": 0.016,
970 + "type": "passive-effects",
971 + },
972 + Object {
973 + "batchUID": 1,
974 + "depth": 0,
975 + "duration": 0.010000000000000002,
976 + "lanes": Array [
977 + 4,
978 + ],
979 + "timestamp": 0.019,
980 + "type": "render-idle",
981 + },
982 + Object {
983 + "batchUID": 1,
984 + "depth": 0,
985 + "duration": 0.002999999999999999,
986 + "lanes": Array [
987 + 4,
988 + ],
989 + "timestamp": 0.019,
990 + "type": "render",
991 + },
992 + Object {
993 + "batchUID": 1,
994 + "depth": 0,
995 + "duration": 0.006000000000000002,
996 + "lanes": Array [
997 + 4,
998 + ],
999 + "timestamp": 0.023,
1000 + "type": "commit",
1001 + },
1002 + Object {
1003 + "batchUID": 1,
1004 + "depth": 1,
1005 + "duration": 0.0010000000000000009,
1006 + "lanes": Array [
1007 + 4,
1008 + ],
1009 + "timestamp": 0.027,
1010 + "type": "layout-effects",
1011 + },
1012 + Object {
1013 + "batchUID": 1,
1014 + "depth": 0,
1015 + "duration": 0.0010000000000000009,
1016 + "lanes": Array [
1017 + 4,
1018 + ],
1019 + "timestamp": 0.03,
1020 + "type": "passive-effects",
1021 + },
1022 + ],
1023 + 5 => Array [],
1024 + 6 => Array [],
1025 + 7 => Array [],
1026 + 8 => Array [],
1027 + 9 => Array [],
1028 + 10 => Array [],
1029 + 11 => Array [],
1030 + 12 => Array [],
1031 + 13 => Array [],
1032 + 14 => Array [],
1033 + 15 => Array [],
1034 + 16 => Array [],
1035 + 17 => Array [],
1036 + 18 => Array [],
1037 + 19 => Array [],
1038 + 20 => Array [],
1039 + 21 => Array [],
1040 + 22 => Array [],
1041 + 23 => Array [],
1042 + 24 => Array [],
1043 + 25 => Array [],
1044 + 26 => Array [],
1045 + 27 => Array [],
1046 + 28 => Array [],
1047 + 29 => Array [],
1048 + 30 => Array [],
1049 + },
1050 + "nativeEvents": Array [],
1051 + "otherUserTimingMarks": Array [
1052 + Object {
1053 + "name": "__v3",
1054 + "timestamp": 0.003,
1055 + },
1056 + ],
1057 + "reactVersion": "17.0.3",
1058 + "schedulingEvents": Array [
1059 + Object {
1060 + "lanes": Array [
1061 + 4,
1062 + ],
1063 + "timestamp": 0.004,
1064 + "type": "schedule-render",
1065 + "warning": null,
1066 + },
1067 + Object {
1068 + "componentName": "App",
1069 + "lanes": Array [
1070 + 4,
1071 + ],
1072 + "timestamp": 0.017,
1073 + "type": "schedule-state-update",
1074 + "warning": null,
1075 + },
1076 + ],
1077 + "startTime": 4,
1078 + "suspenseEvents": Array [],
1079 + }
1080 + `);
1081 }
1082 });
1083
packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+42 -16
@@ -13,13 +13,14 @@ import {
13 } from '@elg/speedscope';
14 import type {TimelineEvent} from '@elg/speedscope';
15 import type {
16 - Milliseconds,
16 BatchUID,
17 Flamechart,
18 + Milliseconds,
19 NativeEvent,
20 Phase,
21 ReactLane,
22 ReactComponentMeasure,
23 + ReactMeasure,
24 ReactMeasureType,
25 ReactProfilerData,
26 SchedulingEvent,
@@ -33,7 +34,7 @@ import {getBatchRange} from '../utils/getBatchRange';
34 type MeasureStackElement = {|
35 type: ReactMeasureType,
36 depth: number,
36 - index: number,
37 + measure: ReactMeasure,
38 startTime: Milliseconds,
39 stopTime?: Milliseconds,
40 |};
@@ -92,17 +93,19 @@ export function getLanesFromTransportDecimalBitmask(
93 return lanes;
94 }
95
95 -const laneToLabelMap: Map<number, string> = new Map();
96 -function updateLaneToLabelMap(laneLabelTuplesString: string): void {
96 +function updateLaneToLabelMap(
97 + profilerData: ReactProfilerData,
98 + laneLabelTuplesString: string,
99 +): void {
100 // These marks appear multiple times in the data;
101 // We only need to extact them once.
99 - if (laneToLabelMap.size === 0) {
102 + if (profilerData.laneToLabelMap.size === 0) {
103 const laneLabelTuples = laneLabelTuplesString.split(',');
104 for (let laneIndex = 0; laneIndex < laneLabelTuples.length; laneIndex++) {
105 // The numeric lane value (e.g. 64) isn't important.
106 // The profiler parses and stores the lane's position within the bitmap,
107 // (e.g. lane 1 is index 0, lane 16 is index 4).
105 - laneToLabelMap.set(laneIndex, laneLabelTuples[laneIndex]);
108 + profilerData.laneToLabelMap.set(laneIndex, laneLabelTuples[laneIndex]);
109 }
110 }
111 }
@@ -133,18 +136,32 @@ function markWorkStarted(
136 state: ProcessorState,
137 ) {
138 const {batchUID, measureStack} = state;
136 - const index = currentProfilerData.measures.length;
139 const depth = getDepth(measureStack);
140
139 - state.measureStack.push({depth, index, startTime, type});
140 -
141 - currentProfilerData.measures.push({
141 + const measure: ReactMeasure = {
142 type,
143 batchUID,
144 depth,
145 lanes,
146 timestamp: startTime,
147 duration: 0,
148 + };
149 +
150 + state.measureStack.push({depth, measure, startTime, type});
151 +
152 + // This array is pre-initialized when the batchUID is generated.
153 + const measures = currentProfilerData.batchUIDToMeasuresMap.get(batchUID);
154 + if (measures != null) {
155 + measures.push(measure);
156 + } else {
157 + currentProfilerData.batchUIDToMeasuresMap.set(state.batchUID, [measure]);
158 + }
159 +
160 + // This array is pre-initialized before processing starts.
161 + lanes.forEach(lane => {
162 + ((currentProfilerData.laneToReactMeasureMap.get(
163 + lane,
164 + ): any): ReactMeasure[]).push(measure);
165 });
166 }
167
@@ -174,8 +191,7 @@ function markWorkCompleted(
191 );
192 }
193
177 - const {index, startTime} = stack.pop();
178 - const measure = currentProfilerData.measures[index];
194 + const {measure, startTime} = stack.pop();
195 if (!measure) {
196 console.error('Could not find matching measure for type "%s".', type);
197 }
@@ -282,7 +298,7 @@ function processTimelineEvent(
298 }
299 } else if (name.startsWith('--react-lane-labels-')) {
300 const [laneLabelTuplesString] = name.substr(20).split('-');
285 - updateLaneToLabelMap(laneLabelTuplesString);
301 + updateLaneToLabelMap(currentProfilerData, laneLabelTuplesString);
302 } else if (name.startsWith('--component-render-start-')) {
303 const [componentName] = name.substr(25).split('-');
304
@@ -648,12 +664,18 @@ export default function preprocessData(
664 ): ReactProfilerData {
665 const flamechart = preprocessFlamechart(timeline);
666
667 + const laneToReactMeasureMap = new Map();
668 + for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) {
669 + laneToReactMeasureMap.set(lane, []);
670 + }
671 +
672 const profilerData: ReactProfilerData = {
673 + batchUIDToMeasuresMap: new Map(),
674 componentMeasures: [],
675 duration: 0,
676 flamechart,
655 - laneToLabelMap,
656 - measures: [],
677 + laneToLabelMap: new Map(),
678 + laneToReactMeasureMap,
679 nativeEvents: [],
680 otherUserTimingMarks: [],
681 reactVersion: null,
@@ -739,7 +761,11 @@ export default function preprocessData(
761 state.potentialSuspenseEventsOutsideOfTransition.forEach(
762 ([suspenseEvent, lanes]) => {
763 // HACK This is a bit gross but the numeric lane value might change between render versions.
742 - if (!lanes.some(lane => laneToLabelMap.get(lane) === 'Transition')) {
764 + if (
765 + !lanes.some(
766 + lane => profilerData.laneToLabelMap.get(lane) === 'Transition',
767 + )
768 + ) {
769 suspenseEvent.warning = WARNING_STRINGS.SUSPEND_DURING_UPATE;
770 }
771 },
packages/react-devtools-scheduling-profiler/src/types.js
+19 -2
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -// Type utilities
10 +import type {ScrollState} from './view-base/utils/scrollState';
11
12 // Source: https://github.com/facebook/flow/issues/4002#issuecomment-323612798
13 // eslint-disable-next-line no-unused-vars
@@ -123,12 +123,29 @@ export type FlamechartStackLayer = FlamechartStackFrame[];
123
124 export type Flamechart = FlamechartStackLayer[];
125
126 +export type HorizontalScrollStateChangeCallback = (
127 + scrollState: ScrollState,
128 +) => void;
129 +
130 +// Imperative view state that corresponds to profiler data.
131 +// This state lives outside of React's lifecycle
132 +// and should be erased/reset whenever new profiler data is loaded.
133 +export type ViewState = {|
134 + horizontalScrollState: ScrollState,
135 + onHorizontalScrollStateChange: (
136 + callback: HorizontalScrollStateChangeCallback,
137 + ) => void,
138 + updateHorizontalScrollState: (scrollState: ScrollState) => void,
139 + viewToMutableViewStateMap: Map<string, mixed>,
140 +|};
141 +
142 export type ReactProfilerData = {|
143 + batchUIDToMeasuresMap: Map<BatchUID, ReactMeasure[]>,
144 componentMeasures: ReactComponentMeasure[],
145 duration: number,
146 flamechart: Flamechart,
147 laneToLabelMap: Map<ReactLane, string>,
131 - measures: ReactMeasure[],
148 + laneToReactMeasureMap: Map<ReactLane, ReactMeasure[]>,
149 nativeEvents: NativeEvent[],
150 otherUserTimingMarks: UserTimingMark[],
151 reactVersion: string | null,
packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js
+20 -22
@@ -9,37 +9,35 @@
9
10 import memoize from 'memoize-one';
11
12 -import type {BatchUID, Milliseconds, ReactProfilerData} from '../types';
12 +import type {
13 + BatchUID,
14 + Milliseconds,
15 + ReactMeasure,
16 + ReactProfilerData,
17 +} from '../types';
18
19 function unmemoizedGetBatchRange(
20 batchUID: BatchUID,
21 data: ReactProfilerData,
17 - minStartTime?: ?number,
22 + minStartTime?: number = 0,
23 ): [Milliseconds, Milliseconds] {
19 - const {measures} = data;
20 -
21 - let startTime = 0;
22 - let stopTime = Infinity;
24 + const measures = data.batchUIDToMeasuresMap.get(batchUID);
25 + if (measures == null || measures.length === 0) {
26 + throw Error(`Could not find measures with batch UID "${batchUID}"`);
27 + }
28
24 - let i = 0;
29 + const lastMeasure = ((measures[measures.length - 1]: any): ReactMeasure);
30 + const stopTime = lastMeasure.timestamp + lastMeasure.duration;
31
26 - // Find the first measure in the current batch.
27 - for (i; i < measures.length; i++) {
28 - const measure = measures[i];
29 - if (measure.batchUID === batchUID) {
30 - if (minStartTime == null || measure.timestamp >= minStartTime) {
31 - startTime = measure.timestamp;
32 - break;
33 - }
34 - }
32 + if (stopTime < minStartTime) {
33 + return [0, 0];
34 }
35
37 - // Find the last measure in the current batch.
38 - for (i; i < measures.length; i++) {
39 - const measure = measures[i];
40 - if (measure.batchUID === batchUID) {
41 - stopTime = measure.timestamp;
42 - } else {
36 + let startTime = minStartTime;
37 + for (let index = 0; index < measures.length; index++) {
38 + const measure = measures[index];
39 + if (measure.timestamp >= minStartTime) {
40 + startTime = measure.timestamp;
41 break;
42 }
43 }
packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js
+58 -89
@@ -18,6 +18,7 @@ import type {
18 import type {Rect} from './geometry';
19 import type {ScrollState} from './utils/scrollState';
20 import type {ViewRefs} from './Surface';
21 +import type {ViewState} from '../types';
22
23 import {Surface} from './Surface';
24 import {View} from './View';
@@ -30,96 +31,84 @@ import {
31 zoomState,
32 } from './utils/scrollState';
33 import {
33 - DEFAULT_ZOOM_LEVEL,
34 MAX_ZOOM_LEVEL,
35 MIN_ZOOM_LEVEL,
36 MOVE_WHEEL_DELTA_THRESHOLD,
37 } from './constants';
38
39 -export type HorizontalPanAndZoomViewOnChangeCallback = (
40 - state: ScrollState,
41 - view: HorizontalPanAndZoomView,
42 -) => void;
43 -
39 export class HorizontalPanAndZoomView extends View {
40 + _contentView: View;
41 _intrinsicContentWidth: number;
42 _isPanning = false;
47 - _scrollState: ScrollState = {offset: 0, length: 0};
48 - _onStateChange: HorizontalPanAndZoomViewOnChangeCallback = () => {};
43 + _viewState: ViewState;
44
45 constructor(
46 surface: Surface,
47 frame: Rect,
48 contentView: View,
49 intrinsicContentWidth: number,
55 - onStateChange?: HorizontalPanAndZoomViewOnChangeCallback,
50 + viewState: ViewState,
51 ) {
52 super(surface, frame);
58 - this.addSubview(contentView);
59 - this._intrinsicContentWidth = intrinsicContentWidth;
60 - this._setScrollState({
61 - offset: 0,
62 - length: intrinsicContentWidth * DEFAULT_ZOOM_LEVEL,
63 - });
64 - if (onStateChange) this._onStateChange = onStateChange;
65 - }
53
67 - setFrame(newFrame: Rect) {
68 - super.setFrame(newFrame);
54 + this._contentView = contentView;
55 + this._intrinsicContentWidth = intrinsicContentWidth;
56 + this._viewState = viewState;
57
70 - // Revalidate scrollState
71 - this._setStateAndInformCallbacksIfChanged(this._scrollState);
72 - }
58 + viewState.onHorizontalScrollStateChange(scrollState => {
59 + this.zoomToRange(scrollState.offset, scrollState.length);
60 + });
61
74 - setScrollState(proposedState: ScrollState) {
75 - this._setScrollState(proposedState);
62 + this.addSubview(contentView);
63 }
64
65 /**
79 - * Just sets scroll state. Use `_setStateAndInformCallbacksIfChanged` if this
80 - * view's callbacks should also be called.
66 + * Just sets scroll state.
67 + * Use `_setStateAndInformCallbacksIfChanged` if this view's callbacks should also be called.
68 *
69 * @returns Whether state was changed
70 * @private
71 */
85 - _setScrollState(proposedState: ScrollState): boolean {
72 + setScrollState(proposedState: ScrollState) {
73 const clampedState = clampState({
74 state: proposedState,
75 minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
76 maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
77 containerLength: this.frame.size.width,
78 });
92 - if (areScrollStatesEqual(clampedState, this._scrollState)) {
93 - return false;
79 + if (
80 + !areScrollStatesEqual(clampedState, this._viewState.horizontalScrollState)
81 + ) {
82 + this.setNeedsDisplay();
83 }
95 - this._scrollState = clampedState;
96 - this.setNeedsDisplay();
97 - return true;
84 }
85
86 /**
101 - * @private
87 + * Zoom to a specific range of the content specified as a range of the
88 + * content view's intrinsic content size.
89 + *
90 + * Does not inform callbacks of state change since this is a public API.
91 */
103 - _setStateAndInformCallbacksIfChanged(proposedState: ScrollState) {
104 - if (this._setScrollState(proposedState)) {
105 - this._onStateChange(this._scrollState, this);
106 - }
92 + zoomToRange(rangeStart: number, rangeEnd: number) {
93 + const newState = moveStateToRange({
94 + state: this._viewState.horizontalScrollState,
95 + rangeStart,
96 + rangeEnd,
97 + contentLength: this._intrinsicContentWidth,
98 +
99 + minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
100 + maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
101 + containerLength: this.frame.size.width,
102 + });
103 + this.setScrollState(newState);
104 }
105
106 desiredSize() {
107 return this._contentView.desiredSize();
108 }
109
113 - /**
114 - * Reference to the content view. This view is also the only view in
115 - * `this.subviews`.
116 - */
117 - get _contentView() {
118 - return this.subviews[0];
119 - }
120 -
110 layoutSubviews() {
122 - const {offset, length} = this._scrollState;
111 + const {offset, length} = this._viewState.horizontalScrollState;
112 const proposedFrame = {
113 origin: {
114 x: this.frame.origin.x + offset,
@@ -134,24 +123,22 @@ export class HorizontalPanAndZoomView extends View {
123 super.layoutSubviews();
124 }
125
137 - /**
138 - * Zoom to a specific range of the content specified as a range of the
139 - * content view's intrinsic content size.
140 - *
141 - * Does not inform callbacks of state change since this is a public API.
142 - */
143 - zoomToRange(rangeStart: number, rangeEnd: number) {
144 - const newState = moveStateToRange({
145 - state: this._scrollState,
146 - rangeStart,
147 - rangeEnd,
148 - contentLength: this._intrinsicContentWidth,
149 -
150 - minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
151 - maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
152 - containerLength: this.frame.size.width,
153 - });
154 - this._setScrollState(newState);
126 + handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
127 + switch (interaction.type) {
128 + case 'mousedown':
129 + this._handleMouseDown(interaction, viewRefs);
130 + break;
131 + case 'mousemove':
132 + this._handleMouseMove(interaction, viewRefs);
133 + break;
134 + case 'mouseup':
135 + this._handleMouseUp(interaction, viewRefs);
136 + break;
137 + case 'wheel-plain':
138 + case 'wheel-shift':
139 + this._handleWheel(interaction);
140 + break;
141 + }
142 }
143
144 _handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
@@ -183,11 +170,11 @@ export class HorizontalPanAndZoomView extends View {
170 return;
171 }
172 const newState = translateState({
186 - state: this._scrollState,
173 + state: this._viewState.horizontalScrollState,
174 delta: interaction.payload.event.movementX,
175 containerLength: this.frame.size.width,
176 });
190 - this._setStateAndInformCallbacksIfChanged(newState);
177 + this._viewState.updateHorizontalScrollState(newState);
178 }
179
180 _handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
@@ -226,44 +213,26 @@ export class HorizontalPanAndZoomView extends View {
213 }
214
215 const newState = zoomState({
229 - state: this._scrollState,
216 + state: this._viewState.horizontalScrollState,
217 multiplier: 1 + 0.005 * -deltaY,
231 - fixedPoint: location.x - this._scrollState.offset,
218 + fixedPoint: location.x - this._viewState.horizontalScrollState.offset,
219
220 minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
221 maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
222 containerLength: this.frame.size.width,
223 });
237 - this._setStateAndInformCallbacksIfChanged(newState);
224 + this._viewState.updateHorizontalScrollState(newState);
225 } else {
226 if (absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD) {
227 return;
228 }
229
230 const newState = translateState({
244 - state: this._scrollState,
231 + state: this._viewState.horizontalScrollState,
232 delta: -deltaX,
233 containerLength: this.frame.size.width,
234 });
248 - this._setStateAndInformCallbacksIfChanged(newState);
249 - }
250 - }
251 -
252 - handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
253 - switch (interaction.type) {
254 - case 'mousedown':
255 - this._handleMouseDown(interaction, viewRefs);
256 - break;
257 - case 'mousemove':
258 - this._handleMouseMove(interaction, viewRefs);
259 - break;
260 - case 'mouseup':
261 - this._handleMouseUp(interaction, viewRefs);
262 - break;
263 - case 'wheel-plain':
264 - case 'wheel-shift':
265 - this._handleWheel(interaction);
266 - break;
235 + this._viewState.updateHorizontalScrollState(newState);
236 }
237 }
238 }
packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js
+48 -16
@@ -17,6 +17,7 @@ import type {
17 } from './useCanvasInteraction';
18 import type {Rect} from './geometry';
19 import type {ViewRefs} from './Surface';
20 +import type {ViewState} from '../types';
21
22 import {BORDER_SIZE, COLORS} from '../content-views/constants';
23 import {drawText} from '../content-views/utils/text';
@@ -35,10 +36,10 @@ type ResizingState = $ReadOnly<{|
36 mouseY: number,
37 |}>;
38
38 -type LayoutState = $ReadOnly<{|
39 +type LayoutState = {|
40 /** Resize bar's vertical position relative to resize view's frame.origin.y */
41 barOffsetY: number,
41 -|}>;
42 +|};
43
44 const RESIZE_BAR_DOT_RADIUS = 1;
45 const RESIZE_BAR_DOT_SPACING = 4;
@@ -217,36 +218,33 @@ class ResizeBar extends View {
218 export class ResizableView extends View {
219 _canvasRef: {current: HTMLCanvasElement | null};
220 _layoutState: LayoutState;
221 + _mutableViewStateKey: string;
222 _resizeBar: ResizeBar;
223 _resizingState: ResizingState | null = null;
224 _subview: View;
225 + _viewState: ViewState;
226
227 constructor(
228 surface: Surface,
229 frame: Rect,
230 subview: View,
231 + viewState: ViewState,
232 canvasRef: {current: HTMLCanvasElement | null},
233 label: string,
234 ) {
235 super(surface, frame, noopLayout);
236
237 this._canvasRef = canvasRef;
234 -
238 + this._layoutState = {barOffsetY: 0};
239 + this._mutableViewStateKey = label + ':ResizableView';
240 this._subview = subview;
241 this._resizeBar = new ResizeBar(surface, frame, label);
242 + this._viewState = viewState;
243
244 this.addSubview(this._subview);
245 this.addSubview(this._resizeBar);
246
241 - const subviewDesiredSize = subview.desiredSize();
242 - this._updateLayoutStateAndResizeBar(
243 - subviewDesiredSize.maxInitialHeight != null
244 - ? Math.min(
245 - subviewDesiredSize.maxInitialHeight,
246 - subviewDesiredSize.height,
247 - )
248 - : subviewDesiredSize.height,
249 - );
247 + this._restoreMutableViewState();
248 }
249
250 desiredSize() {
@@ -274,6 +272,35 @@ export class ResizableView extends View {
272 super.layoutSubviews();
273 }
274
275 + _restoreMutableViewState() {
276 + if (
277 + this._viewState.viewToMutableViewStateMap.has(this._mutableViewStateKey)
278 + ) {
279 + this._layoutState = ((this._viewState.viewToMutableViewStateMap.get(
280 + this._mutableViewStateKey,
281 + ): any): LayoutState);
282 +
283 + this._updateLayoutStateAndResizeBar(this._layoutState.barOffsetY);
284 + } else {
285 + this._viewState.viewToMutableViewStateMap.set(
286 + this._mutableViewStateKey,
287 + this._layoutState,
288 + );
289 +
290 + const subviewDesiredSize = this._subview.desiredSize();
291 + this._updateLayoutStateAndResizeBar(
292 + subviewDesiredSize.maxInitialHeight != null
293 + ? Math.min(
294 + subviewDesiredSize.maxInitialHeight,
295 + subviewDesiredSize.height,
296 + )
297 + : subviewDesiredSize.height,
298 + );
299 + }
300 +
301 + this.setNeedsDisplay();
302 + }
303 +
304 _shouldRenderResizeBar() {
305 const subviewDesiredSize = this._subview.desiredSize();
306 return subviewDesiredSize.hideScrollBarIfLessThanHeight != null
@@ -287,10 +314,7 @@ export class ResizableView extends View {
314 barOffsetY = 0;
315 }
316
290 - this._layoutState = {
291 - ...this._layoutState,
292 - barOffsetY,
293 - };
317 + this._layoutState.barOffsetY = barOffsetY;
318
319 this._resizeBar.showLabel = barOffsetY === 0;
320 }
@@ -341,6 +365,10 @@ export class ResizableView extends View {
365 }
366
367 _handleClick(interaction: ClickInteraction) {
368 + if (!this._shouldRenderResizeBar()) {
369 + return;
370 + }
371 +
372 const cursorInView = rectContainsPoint(
373 interaction.payload.location,
374 this.frame,
@@ -356,6 +384,10 @@ export class ResizableView extends View {
384 }
385
386 _handleDoubleClick(interaction: DoubleClickInteraction) {
387 + if (!this._shouldRenderResizeBar()) {
388 + return;
389 + }
390 +
391 const cursorInView = rectContainsPoint(
392 interaction.payload.location,
393 this.frame,
packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js
+62 -31
@@ -17,6 +17,7 @@ import type {
17 import type {Rect} from './geometry';
18 import type {ScrollState} from './utils/scrollState';
19 import type {ViewRefs} from './Surface';
20 +import type {ViewState} from '../types';
21
22 import {Surface} from './Surface';
23 import {View} from './View';
@@ -34,13 +35,33 @@ const CARET_WIDTH = 5;
35 const CARET_HEIGHT = 3;
36
37 export class VerticalScrollView extends View {
37 - _scrollState: ScrollState = {offset: 0, length: 0};
38 - _isPanning = false;
39 -
40 - constructor(surface: Surface, frame: Rect, contentView: View) {
38 + _contentView: View;
39 + _isPanning: boolean;
40 + _mutableViewStateKey: string;
41 + _scrollState: ScrollState;
42 + _viewState: ViewState;
43 +
44 + constructor(
45 + surface: Surface,
46 + frame: Rect,
47 + contentView: View,
48 + viewState: ViewState,
49 + label: string,
50 + ) {
51 super(surface, frame);
52 +
53 + this._contentView = contentView;
54 + this._isPanning = false;
55 + this._mutableViewStateKey = label + ':VerticalScrollView';
56 + this._scrollState = {
57 + offset: 0,
58 + length: 0,
59 + };
60 + this._viewState = viewState;
61 +
62 this.addSubview(contentView);
43 - this._setScrollState(this._scrollState);
63 +
64 + this._restoreMutableViewState();
65 }
66
67 setFrame(newFrame: Rect) {
@@ -102,14 +123,6 @@ export class VerticalScrollView extends View {
123 }
124 }
125
105 - /**
106 - * Reference to the content view. This view is also the only view in
107 - * `this.subviews`.
108 - */
109 - get _contentView() {
110 - return this.subviews[0];
111 - }
112 -
126 layoutSubviews() {
127 const {offset} = this._scrollState;
128 const desiredSize = this._contentView.desiredSize();
@@ -133,6 +146,23 @@ export class VerticalScrollView extends View {
146 super.layoutSubviews();
147 }
148
149 + handleInteraction(interaction: Interaction) {
150 + switch (interaction.type) {
151 + case 'mousedown':
152 + this._handleMouseDown(interaction);
153 + break;
154 + case 'mousemove':
155 + this._handleMouseMove(interaction);
156 + break;
157 + case 'mouseup':
158 + this._handleMouseUp(interaction);
159 + break;
160 + case 'wheel-shift':
161 + this._handleWheelShift(interaction);
162 + break;
163 + }
164 + }
165 +
166 _handleMouseDown(interaction: MouseDownInteraction) {
167 if (rectContainsPoint(interaction.payload.location, this.frame)) {
168 this._isPanning = true;
@@ -184,21 +214,21 @@ export class VerticalScrollView extends View {
214 this._setScrollState(newState);
215 }
216
187 - handleInteraction(interaction: Interaction) {
188 - switch (interaction.type) {
189 - case 'mousedown':
190 - this._handleMouseDown(interaction);
191 - break;
192 - case 'mousemove':
193 - this._handleMouseMove(interaction);
194 - break;
195 - case 'mouseup':
196 - this._handleMouseUp(interaction);
197 - break;
198 - case 'wheel-shift':
199 - this._handleWheelShift(interaction);
200 - break;
217 + _restoreMutableViewState() {
218 + if (
219 + this._viewState.viewToMutableViewStateMap.has(this._mutableViewStateKey)
220 + ) {
221 + this._scrollState = ((this._viewState.viewToMutableViewStateMap.get(
222 + this._mutableViewStateKey,
223 + ): any): ScrollState);
224 + } else {
225 + this._viewState.viewToMutableViewStateMap.set(
226 + this._mutableViewStateKey,
227 + this._scrollState,
228 + );
229 }
230 +
231 + this.setNeedsDisplay();
232 }
233
234 /**
@@ -212,10 +242,11 @@ export class VerticalScrollView extends View {
242 maxContentLength: height,
243 containerLength: this.frame.size.height,
244 });
215 - if (areScrollStatesEqual(clampedState, this._scrollState)) {
216 - return;
245 + if (!areScrollStatesEqual(clampedState, this._scrollState)) {
246 + this._scrollState.offset = clampedState.offset;
247 + this._scrollState.length = clampedState.length;
248 +
249 + this.setNeedsDisplay();
250 }
218 - this._scrollState = clampedState;
219 - this.setNeedsDisplay();
251 }
252 }
packages/react-devtools-scheduling-profiler/src/view-base/utils/scrollState.js
+2 -2
@@ -18,10 +18,10 @@ import {clamp} from './clamp';
18 * |<-------------------length------------------->|
19 * ```
20 */
21 -export type ScrollState = $ReadOnly<{|
21 +export type ScrollState = {|
22 offset: number,
23 length: number,
24 -|}>;
24 +|};
25
26 function clampOffset(state: ScrollState, containerLength: number): ScrollState {
27 return {