diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js
index e699959013b7d..59939cd948692 100644
--- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js
+++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js
@@ -7,19 +7,18 @@
* @flow
*/
-import type {
- Point,
- HorizontalPanAndZoomViewOnChangeCallback,
-} from './view-base';
+import type {Point} from './view-base';
import type {
ReactHoverContextInfo,
ReactProfilerData,
ReactMeasure,
+ ViewState,
} from './types';
import * as React from 'react';
import {
Fragment,
+ useContext,
useEffect,
useLayoutEffect,
useRef,
@@ -54,12 +53,14 @@ import {
UserTimingMarksView,
} from './content-views';
import {COLORS} from './content-views/constants';
-
+import {clampState, moveStateToRange} from './view-base/utils/scrollState';
import EventTooltip from './EventTooltip';
+import {RegistryContext} from 'react-devtools-shared/src/devtools/ContextMenu/Contexts';
import ContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenu';
import ContextMenuItem from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem';
import useContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/useContextMenu';
import {getBatchRange} from './utils/getBatchRange';
+import {MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL} from './view-base/constants';
import styles from './CanvasPage.css';
@@ -67,16 +68,22 @@ const CONTEXT_MENU_ID = 'canvas';
type Props = {|
profilerData: ReactProfilerData,
+ viewState: ViewState,
|};
-function CanvasPage({profilerData}: Props) {
+function CanvasPage({profilerData, viewState}: Props) {
return (
{({height, width}: {height: number, width: number}) => (
-
+
)}
@@ -98,27 +105,43 @@ const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => {
);
};
-// TODO (scheduling profiler) Why is the "zoom" feature so much slower than normal rendering?
const zoomToBatch = (
data: ReactProfilerData,
measure: ReactMeasure,
- syncedHorizontalPanAndZoomViews: HorizontalPanAndZoomView[],
+ viewState: ViewState,
+ width: number,
) => {
const {batchUID} = measure;
- const [startTime, stopTime] = getBatchRange(batchUID, data);
- syncedHorizontalPanAndZoomViews.forEach(syncedView =>
- // Using time as range works because the views' intrinsic content size is based on time.
- syncedView.zoomToRange(startTime, stopTime),
- );
+ const [rangeStart, rangeEnd] = getBatchRange(batchUID, data);
+
+ // Convert from time range to ScrollState
+ const scrollState = moveStateToRange({
+ state: viewState.horizontalScrollState,
+ rangeStart,
+ rangeEnd,
+ contentLength: data.duration,
+
+ minContentLength: data.duration * MIN_ZOOM_LEVEL,
+ maxContentLength: data.duration * MAX_ZOOM_LEVEL,
+ containerLength: width,
+ });
+
+ viewState.updateHorizontalScrollState(scrollState);
};
type AutoSizedCanvasProps = {|
data: ReactProfilerData,
height: number,
+ viewState: ViewState,
width: number,
|};
-function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
+function AutoSizedCanvas({
+ data,
+ height,
+ viewState,
+ width,
+}: AutoSizedCanvasProps) {
const canvasRef = useRef(null);
const [isContextMenuShown, setIsContextMenuShown] = useState(false);
@@ -136,30 +159,31 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
const componentMeasuresViewRef = useRef(null);
const reactMeasuresViewRef = useRef(null);
const flamechartViewRef = useRef(null);
- const syncedHorizontalPanAndZoomViewsRef = useRef(
- [],
- );
+
+ const {hideMenu: hideContextMenu} = useContext(RegistryContext);
useLayoutEffect(() => {
const surface = surfaceRef.current;
const defaultFrame = {origin: zeroPoint, size: {width, height}};
- // Clear synced views
- syncedHorizontalPanAndZoomViewsRef.current = [];
+ // Auto hide context menu when panning.
+ viewState.onHorizontalScrollStateChange(scrollState => {
+ hideContextMenu();
+ });
- const syncAllHorizontalPanAndZoomViewStates: HorizontalPanAndZoomViewOnChangeCallback = (
- newState,
- triggeringView?: HorizontalPanAndZoomView,
- ) => {
- syncedHorizontalPanAndZoomViewsRef.current.forEach(
- syncedView =>
- triggeringView !== syncedView && syncedView.setScrollState(newState),
- );
- };
+ // Initialize horizontal view state
+ viewState.updateHorizontalScrollState(
+ clampState({
+ state: viewState.horizontalScrollState,
+ minContentLength: data.duration * MIN_ZOOM_LEVEL,
+ maxContentLength: data.duration * MAX_ZOOM_LEVEL,
+ containerLength: defaultFrame.size.width,
+ }),
+ );
function createViewHelper(
view: View,
- resizeLabel: string = '',
+ label: string,
shouldScrollVertically: boolean = false,
shouldResizeVertically: boolean = false,
): View {
@@ -169,6 +193,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
surface,
defaultFrame,
view,
+ viewState,
+ label,
);
}
@@ -177,23 +203,22 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
defaultFrame,
verticalScrollView !== null ? verticalScrollView : view,
data.duration,
- syncAllHorizontalPanAndZoomViewStates,
+ viewState,
);
- syncedHorizontalPanAndZoomViewsRef.current.push(horizontalPanAndZoomView);
-
- let viewToReturn = horizontalPanAndZoomView;
+ let resizableView = null;
if (shouldResizeVertically) {
- viewToReturn = new ResizableView(
+ resizableView = new ResizableView(
surface,
defaultFrame,
horizontalPanAndZoomView,
+ viewState,
canvasRef,
- resizeLabel,
+ label,
);
}
- return viewToReturn;
+ return resizableView || horizontalPanAndZoomView;
}
const axisMarkersView = new TimeAxisMarkersView(
@@ -201,7 +226,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
defaultFrame,
data.duration,
);
- const axisMarkersViewWrapper = createViewHelper(axisMarkersView);
+ const axisMarkersViewWrapper = createViewHelper(axisMarkersView, 'time');
let userTimingMarksViewWrapper = null;
if (data.otherUserTimingMarks.length > 0) {
@@ -212,7 +237,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
data.duration,
);
userTimingMarksViewRef.current = userTimingMarksView;
- userTimingMarksViewWrapper = createViewHelper(userTimingMarksView);
+ userTimingMarksViewWrapper = createViewHelper(
+ userTimingMarksView,
+ 'user timing api',
+ );
}
const nativeEventsView = new NativeEventsView(surface, defaultFrame, data);
@@ -230,7 +258,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
data,
);
schedulingEventsViewRef.current = schedulingEventsView;
- const schedulingEventsViewWrapper = createViewHelper(schedulingEventsView);
+ const schedulingEventsViewWrapper = createViewHelper(
+ schedulingEventsView,
+ 'react updates',
+ );
let suspenseEventsViewWrapper = null;
if (data.suspenseEvents.length > 0) {
@@ -256,7 +287,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
reactMeasuresViewRef.current = reactMeasuresView;
const reactMeasuresViewWrapper = createViewHelper(
reactMeasuresView,
- 'react',
+ 'react scheduling',
true,
true,
);
@@ -269,7 +300,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
data,
);
componentMeasuresViewRef.current = componentMeasuresView;
- componentMeasuresViewWrapper = createViewHelper(componentMeasuresView);
+ componentMeasuresViewWrapper = createViewHelper(
+ componentMeasuresView,
+ 'react components',
+ );
}
const flamechartView = new FlamechartView(
@@ -329,7 +363,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
return;
}
- // Wheel events should always hide the current toolltip.
+ // Wheel events should always hide the current tooltip.
switch (interaction.type) {
case 'wheel-control':
case 'wheel-meta':
@@ -617,11 +651,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
{measure !== null && (
- zoomToBatch(
- contextData.data,
- measure,
- syncedHorizontalPanAndZoomViewsRef.current,
- )
+ zoomToBatch(contextData.data, measure, viewState, width)
}
title="Zoom to batch">
Zoom to batch
diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js
index 9b3220c5ae1b7..98cd61d312447 100644
--- a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js
+++ b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js
@@ -8,6 +8,7 @@
*/
import type {DataResource} from './createDataResourceFromImportedFile';
+import type {ViewState} from './types';
import * as React from 'react';
import {
@@ -27,9 +28,11 @@ import CanvasPage from './CanvasPage';
import styles from './SchedulingProfiler.css';
export function SchedulingProfiler(_: {||}) {
- const {importSchedulingProfilerData, schedulingProfilerData} = useContext(
- SchedulingProfilerContext,
- );
+ const {
+ importSchedulingProfilerData,
+ schedulingProfilerData,
+ viewState,
+ } = useContext(SchedulingProfilerContext);
const ref = useRef(null);
@@ -66,6 +69,7 @@ export function SchedulingProfiler(_: {||}) {
dataResource={schedulingProfilerData}
key={key}
onFileSelect={importSchedulingProfilerData}
+ viewState={viewState}
/>
) : (
@@ -130,9 +134,11 @@ const CouldNotLoadProfile = ({error, onFileSelect}) => (
const DataResourceComponent = ({
dataResource,
onFileSelect,
+ viewState,
}: {|
dataResource: DataResource,
onFileSelect: (file: File) => void,
+ viewState: ViewState,
|}) => {
const dataOrError = dataResource.read();
if (dataOrError instanceof Error) {
@@ -140,5 +146,5 @@ const DataResourceComponent = ({
);
}
- return ;
+ return ;
};
diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js b/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js
index dc95fc31a9da6..ff81482c09e74 100644
--- a/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js
+++ b/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js
@@ -11,12 +11,14 @@ import * as React from 'react';
import {createContext, useCallback, useMemo, useState} from 'react';
import createDataResourceFromImportedFile from './createDataResourceFromImportedFile';
+import type {HorizontalScrollStateChangeCallback, ViewState} from './types';
import type {DataResource} from './createDataResourceFromImportedFile';
export type Context = {|
clearSchedulingProfilerData: () => void,
importSchedulingProfilerData: (file: File) => void,
schedulingProfilerData: DataResource | null,
+ viewState: ViewState,
|};
const SchedulingProfilerContext = createContext(
@@ -42,20 +44,51 @@ function SchedulingProfilerContextController({children}: Props) {
setSchedulingProfilerData(createDataResourceFromImportedFile(file));
}, []);
- // TODO (scheduling profiler) Start/stop time ref here?
+ // Recreate view state any time new profiling data is imported.
+ const viewState = useMemo(() => {
+ const horizontalScrollStateChangeCallbacks: Set = new Set();
+
+ const horizontalScrollState = {
+ offset: 0,
+ length: 0,
+ };
+
+ return {
+ horizontalScrollState,
+ onHorizontalScrollStateChange: callback => {
+ horizontalScrollStateChangeCallbacks.add(callback);
+ },
+ updateHorizontalScrollState: scrollState => {
+ if (
+ horizontalScrollState.offset === scrollState.offset &&
+ horizontalScrollState.length === scrollState.length
+ ) {
+ return;
+ }
+
+ horizontalScrollState.offset = scrollState.offset;
+ horizontalScrollState.length = scrollState.length;
+
+ horizontalScrollStateChangeCallbacks.forEach(callback => {
+ callback(scrollState);
+ });
+ },
+ viewToMutableViewStateMap: new Map(),
+ };
+ }, [schedulingProfilerData]);
const value = useMemo(
() => ({
clearSchedulingProfilerData,
importSchedulingProfilerData,
schedulingProfilerData,
- // TODO (scheduling profiler)
+ viewState,
}),
[
clearSchedulingProfilerData,
importSchedulingProfilerData,
schedulingProfilerData,
- // TODO (scheduling profiler)
+ viewState,
],
);
diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js
index 976e37ebb9d39..7009356b4ce46 100644
--- a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js
+++ b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js
@@ -38,21 +38,12 @@ import {REACT_TOTAL_NUM_LANES} from '../constants';
const REACT_LANE_HEIGHT = REACT_MEASURE_HEIGHT + BORDER_SIZE;
const MAX_ROWS_TO_SHOW_INITIALLY = 5;
-function getMeasuresForLane(
- allMeasures: ReactMeasure[],
- lane: ReactLane,
-): ReactMeasure[] {
- return allMeasures.filter(measure => measure.lanes.includes(lane));
-}
-
export class ReactMeasuresView extends View {
- _profilerData: ReactProfilerData;
_intrinsicSize: IntrinsicSize;
-
_lanesToRender: ReactLane[];
- _laneToMeasures: Map;
-
+ _profilerData: ReactProfilerData;
_hoveredMeasure: ReactMeasure | null = null;
+
onHover: ((measure: ReactMeasure | null) => void) | null = null;
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
@@ -63,17 +54,14 @@ export class ReactMeasuresView extends View {
_performPreflightComputations() {
this._lanesToRender = [];
- this._laneToMeasures = new Map();
for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) {
- const measuresForLane = getMeasuresForLane(
- this._profilerData.measures,
+ const measuresForLane = this._profilerData.laneToReactMeasureMap.get(
lane,
);
// Only show lanes with measures
- if (measuresForLane.length) {
+ if (measuresForLane != null && measuresForLane.length > 0) {
this._lanesToRender.push(lane);
- this._laneToMeasures.set(lane, measuresForLane);
}
}
@@ -213,7 +201,7 @@ export class ReactMeasuresView extends View {
frame,
_hoveredMeasure,
_lanesToRender,
- _laneToMeasures,
+ _profilerData,
visibleArea,
} = this;
@@ -233,7 +221,7 @@ export class ReactMeasuresView extends View {
for (let i = 0; i < _lanesToRender.length; i++) {
const lane = _lanesToRender[i];
const baseY = frame.origin.y + i * REACT_LANE_HEIGHT;
- const measuresForLane = _laneToMeasures.get(lane);
+ const measuresForLane = _profilerData.laneToReactMeasureMap.get(lane);
if (!measuresForLane) {
throw new Error(
@@ -242,7 +230,7 @@ export class ReactMeasuresView extends View {
}
// Render lane labels
- const label = this._profilerData.laneToLabelMap.get(lane);
+ const label = _profilerData.laneToLabelMap.get(lane);
if (label == null) {
console.warn(`Could not find label for lane ${lane}.`);
} else {
@@ -316,8 +304,8 @@ export class ReactMeasuresView extends View {
frame,
_intrinsicSize,
_lanesToRender,
- _laneToMeasures,
onHover,
+ _profilerData,
visibleArea,
} = this;
if (!onHover) {
@@ -347,7 +335,7 @@ export class ReactMeasuresView extends View {
// This will always be the one on "top" (the one the user is hovering over).
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
- const measures = _laneToMeasures.get(lane);
+ const measures = _profilerData.laneToReactMeasureMap.get(lane);
if (!measures) {
onHover(null);
return;
diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
index 4c59a68efe3be..0fa434b9489da 100644
--- a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
+++ b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
@@ -253,6 +253,7 @@ describe(preprocessData, () => {
]);
expect(data).toMatchInlineSnapshot(`
Object {
+ "batchUIDToMeasuresMap": Map {},
"componentMeasures": Array [],
"duration": 0.005,
"flamechart": Array [],
@@ -289,7 +290,39 @@ describe(preprocessData, () => {
29 => "Idle",
30 => "Offscreen",
},
- "measures": Array [],
+ "laneToReactMeasureMap": Map {
+ 0 => Array [],
+ 1 => Array [],
+ 2 => Array [],
+ 3 => Array [],
+ 4 => Array [],
+ 5 => Array [],
+ 6 => Array [],
+ 7 => Array [],
+ 8 => Array [],
+ 9 => Array [],
+ 10 => Array [],
+ 11 => Array [],
+ 12 => Array [],
+ 13 => Array [],
+ 14 => Array [],
+ 15 => Array [],
+ 16 => Array [],
+ 17 => Array [],
+ 18 => Array [],
+ 19 => Array [],
+ 20 => Array [],
+ 21 => Array [],
+ 22 => Array [],
+ 23 => Array [],
+ 24 => Array [],
+ 25 => Array [],
+ 26 => Array [],
+ 27 => Array [],
+ 28 => Array [],
+ 29 => Array [],
+ 30 => Array [],
+ },
"nativeEvents": Array [],
"otherUserTimingMarks": Array [],
"reactVersion": "17.0.3",
@@ -340,102 +373,178 @@ describe(preprocessData, () => {
}),
]);
expect(data).toMatchInlineSnapshot(`
+ Object {
+ "batchUIDToMeasuresMap": Map {
+ 0 => Array [
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.004999999999999999,
+ "lanes": Array [
+ 9,
+ ],
+ "timestamp": 0.006,
+ "type": "render-idle",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.001,
+ "lanes": Array [
+ 9,
+ ],
+ "timestamp": 0.006,
+ "type": "render",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.002999999999999999,
+ "lanes": Array [
+ 9,
+ ],
+ "timestamp": 0.008,
+ "type": "commit",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 1,
+ "duration": 0.0010000000000000009,
+ "lanes": Array [
+ 9,
+ ],
+ "timestamp": 0.009,
+ "type": "layout-effects",
+ },
+ ],
+ },
+ "componentMeasures": Array [],
+ "duration": 0.011,
+ "flamechart": Array [],
+ "laneToLabelMap": Map {
+ 0 => "Sync",
+ 1 => "InputContinuousHydration",
+ 2 => "InputContinuous",
+ 3 => "DefaultHydration",
+ 4 => "Default",
+ 5 => "TransitionHydration",
+ 6 => "Transition",
+ 7 => "Transition",
+ 8 => "Transition",
+ 9 => "Transition",
+ 10 => "Transition",
+ 11 => "Transition",
+ 12 => "Transition",
+ 13 => "Transition",
+ 14 => "Transition",
+ 15 => "Transition",
+ 16 => "Transition",
+ 17 => "Transition",
+ 18 => "Transition",
+ 19 => "Transition",
+ 20 => "Transition",
+ 21 => "Transition",
+ 22 => "Retry",
+ 23 => "Retry",
+ 24 => "Retry",
+ 25 => "Retry",
+ 26 => "Retry",
+ 27 => "SelectiveHydration",
+ 28 => "IdleHydration",
+ 29 => "Idle",
+ 30 => "Offscreen",
+ },
+ "laneToReactMeasureMap": Map {
+ 0 => Array [],
+ 1 => Array [],
+ 2 => Array [],
+ 3 => Array [],
+ 4 => Array [],
+ 5 => Array [],
+ 6 => Array [],
+ 7 => Array [],
+ 8 => Array [],
+ 9 => Array [
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.004999999999999999,
+ "lanes": Array [
+ 9,
+ ],
+ "timestamp": 0.006,
+ "type": "render-idle",
+ },
Object {
- "componentMeasures": Array [],
- "duration": 0.011,
- "flamechart": Array [],
- "laneToLabelMap": Map {
- 0 => "Sync",
- 1 => "InputContinuousHydration",
- 2 => "InputContinuous",
- 3 => "DefaultHydration",
- 4 => "Default",
- 5 => "TransitionHydration",
- 6 => "Transition",
- 7 => "Transition",
- 8 => "Transition",
- 9 => "Transition",
- 10 => "Transition",
- 11 => "Transition",
- 12 => "Transition",
- 13 => "Transition",
- 14 => "Transition",
- 15 => "Transition",
- 16 => "Transition",
- 17 => "Transition",
- 18 => "Transition",
- 19 => "Transition",
- 20 => "Transition",
- 21 => "Transition",
- 22 => "Retry",
- 23 => "Retry",
- 24 => "Retry",
- 25 => "Retry",
- 26 => "Retry",
- 27 => "SelectiveHydration",
- 28 => "IdleHydration",
- 29 => "Idle",
- 30 => "Offscreen",
- },
- "measures": Array [
- Object {
- "batchUID": 0,
- "depth": 0,
- "duration": 0.004999999999999999,
- "lanes": Array [
- 9,
- ],
- "timestamp": 0.006,
- "type": "render-idle",
- },
- Object {
- "batchUID": 0,
- "depth": 0,
- "duration": 0.001,
- "lanes": Array [
- 9,
- ],
- "timestamp": 0.006,
- "type": "render",
- },
- Object {
- "batchUID": 0,
- "depth": 0,
- "duration": 0.002999999999999999,
- "lanes": Array [
- 9,
- ],
- "timestamp": 0.008,
- "type": "commit",
- },
- Object {
- "batchUID": 0,
- "depth": 1,
- "duration": 0.0010000000000000009,
- "lanes": Array [
- 9,
- ],
- "timestamp": 0.009,
- "type": "layout-effects",
- },
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.001,
+ "lanes": Array [
+ 9,
],
- "nativeEvents": Array [],
- "otherUserTimingMarks": Array [],
- "reactVersion": "17.0.3",
- "schedulingEvents": Array [
- Object {
- "lanes": Array [
- 9,
- ],
- "timestamp": 0.005,
- "type": "schedule-render",
- "warning": null,
- },
+ "timestamp": 0.006,
+ "type": "render",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.002999999999999999,
+ "lanes": Array [
+ 9,
+ ],
+ "timestamp": 0.008,
+ "type": "commit",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 1,
+ "duration": 0.0010000000000000009,
+ "lanes": Array [
+ 9,
],
- "startTime": 1,
- "suspenseEvents": Array [],
- }
- `);
+ "timestamp": 0.009,
+ "type": "layout-effects",
+ },
+ ],
+ 10 => Array [],
+ 11 => Array [],
+ 12 => Array [],
+ 13 => Array [],
+ 14 => Array [],
+ 15 => Array [],
+ 16 => Array [],
+ 17 => Array [],
+ 18 => Array [],
+ 19 => Array [],
+ 20 => Array [],
+ 21 => Array [],
+ 22 => Array [],
+ 23 => Array [],
+ 24 => Array [],
+ 25 => Array [],
+ 26 => Array [],
+ 27 => Array [],
+ 28 => Array [],
+ 29 => Array [],
+ 30 => Array [],
+ },
+ "nativeEvents": Array [],
+ "otherUserTimingMarks": Array [],
+ "reactVersion": "17.0.3",
+ "schedulingEvents": Array [
+ Object {
+ "lanes": Array [
+ 9,
+ ],
+ "timestamp": 0.005,
+ "type": "schedule-render",
+ "warning": null,
+ },
+ ],
+ "startTime": 1,
+ "suspenseEvents": Array [],
+ }
+ `);
}
});
@@ -448,107 +557,183 @@ describe(preprocessData, () => {
...createUserTimingData(clearedMarks),
]);
expect(data).toMatchInlineSnapshot(`
+ Object {
+ "batchUIDToMeasuresMap": Map {
+ 0 => Array [
Object {
- "componentMeasures": Array [],
- "duration": 0.013,
- "flamechart": Array [],
- "laneToLabelMap": Map {
- 0 => "Sync",
- 1 => "InputContinuousHydration",
- 2 => "InputContinuous",
- 3 => "DefaultHydration",
- 4 => "Default",
- 5 => "TransitionHydration",
- 6 => "Transition",
- 7 => "Transition",
- 8 => "Transition",
- 9 => "Transition",
- 10 => "Transition",
- 11 => "Transition",
- 12 => "Transition",
- 13 => "Transition",
- 14 => "Transition",
- 15 => "Transition",
- 16 => "Transition",
- 17 => "Transition",
- 18 => "Transition",
- 19 => "Transition",
- 20 => "Transition",
- 21 => "Transition",
- 22 => "Retry",
- 23 => "Retry",
- 24 => "Retry",
- 25 => "Retry",
- 26 => "Retry",
- 27 => "SelectiveHydration",
- 28 => "IdleHydration",
- 29 => "Idle",
- 30 => "Offscreen",
- },
- "measures": Array [
- Object {
- "batchUID": 0,
- "depth": 0,
- "duration": 0.008,
- "lanes": Array [
- 0,
- ],
- "timestamp": 0.005,
- "type": "render-idle",
- },
- Object {
- "batchUID": 0,
- "depth": 0,
- "duration": 0.001,
- "lanes": Array [
- 0,
- ],
- "timestamp": 0.005,
- "type": "render",
- },
- Object {
- "batchUID": 0,
- "depth": 0,
- "duration": 0.005999999999999999,
- "lanes": Array [
- 0,
- ],
- "timestamp": 0.007,
- "type": "commit",
- },
- Object {
- "batchUID": 0,
- "depth": 1,
- "duration": 0.0010000000000000009,
- "lanes": Array [
- 0,
- ],
- "timestamp": 0.011,
- "type": "layout-effects",
- },
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.008,
+ "lanes": Array [
+ 0,
],
- "nativeEvents": Array [],
- "otherUserTimingMarks": Array [
- Object {
- "name": "__v3",
- "timestamp": 0.003,
- },
+ "timestamp": 0.005,
+ "type": "render-idle",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.001,
+ "lanes": Array [
+ 0,
+ ],
+ "timestamp": 0.005,
+ "type": "render",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.005999999999999999,
+ "lanes": Array [
+ 0,
],
- "reactVersion": "17.0.3",
- "schedulingEvents": Array [
- Object {
- "lanes": Array [
- 0,
- ],
- "timestamp": 0.004,
- "type": "schedule-render",
- "warning": null,
- },
+ "timestamp": 0.007,
+ "type": "commit",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 1,
+ "duration": 0.0010000000000000009,
+ "lanes": Array [
+ 0,
],
- "startTime": 4,
- "suspenseEvents": Array [],
- }
- `);
+ "timestamp": 0.011,
+ "type": "layout-effects",
+ },
+ ],
+ },
+ "componentMeasures": Array [],
+ "duration": 0.013,
+ "flamechart": Array [],
+ "laneToLabelMap": Map {
+ 0 => "Sync",
+ 1 => "InputContinuousHydration",
+ 2 => "InputContinuous",
+ 3 => "DefaultHydration",
+ 4 => "Default",
+ 5 => "TransitionHydration",
+ 6 => "Transition",
+ 7 => "Transition",
+ 8 => "Transition",
+ 9 => "Transition",
+ 10 => "Transition",
+ 11 => "Transition",
+ 12 => "Transition",
+ 13 => "Transition",
+ 14 => "Transition",
+ 15 => "Transition",
+ 16 => "Transition",
+ 17 => "Transition",
+ 18 => "Transition",
+ 19 => "Transition",
+ 20 => "Transition",
+ 21 => "Transition",
+ 22 => "Retry",
+ 23 => "Retry",
+ 24 => "Retry",
+ 25 => "Retry",
+ 26 => "Retry",
+ 27 => "SelectiveHydration",
+ 28 => "IdleHydration",
+ 29 => "Idle",
+ 30 => "Offscreen",
+ },
+ "laneToReactMeasureMap": Map {
+ 0 => Array [
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.008,
+ "lanes": Array [
+ 0,
+ ],
+ "timestamp": 0.005,
+ "type": "render-idle",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.001,
+ "lanes": Array [
+ 0,
+ ],
+ "timestamp": 0.005,
+ "type": "render",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.005999999999999999,
+ "lanes": Array [
+ 0,
+ ],
+ "timestamp": 0.007,
+ "type": "commit",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 1,
+ "duration": 0.0010000000000000009,
+ "lanes": Array [
+ 0,
+ ],
+ "timestamp": 0.011,
+ "type": "layout-effects",
+ },
+ ],
+ 1 => Array [],
+ 2 => Array [],
+ 3 => Array [],
+ 4 => Array [],
+ 5 => Array [],
+ 6 => Array [],
+ 7 => Array [],
+ 8 => Array [],
+ 9 => Array [],
+ 10 => Array [],
+ 11 => Array [],
+ 12 => Array [],
+ 13 => Array [],
+ 14 => Array [],
+ 15 => Array [],
+ 16 => Array [],
+ 17 => Array [],
+ 18 => Array [],
+ 19 => Array [],
+ 20 => Array [],
+ 21 => Array [],
+ 22 => Array [],
+ 23 => Array [],
+ 24 => Array [],
+ 25 => Array [],
+ 26 => Array [],
+ 27 => Array [],
+ 28 => Array [],
+ 29 => Array [],
+ 30 => Array [],
+ },
+ "nativeEvents": Array [],
+ "otherUserTimingMarks": Array [
+ Object {
+ "name": "__v3",
+ "timestamp": 0.003,
+ },
+ ],
+ "reactVersion": "17.0.3",
+ "schedulingEvents": Array [
+ Object {
+ "lanes": Array [
+ 0,
+ ],
+ "timestamp": 0.004,
+ "type": "schedule-render",
+ "warning": null,
+ },
+ ],
+ "startTime": 4,
+ "suspenseEvents": Array [],
+ }
+ `);
}
});
@@ -572,189 +757,327 @@ describe(preprocessData, () => {
...createUserTimingData(clearedMarks),
]);
expect(data).toMatchInlineSnapshot(`
+ Object {
+ "batchUIDToMeasuresMap": Map {
+ 0 => Array [
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.009999999999999998,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.005,
+ "type": "render-idle",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.003,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.005,
+ "type": "render",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.006,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.009,
+ "type": "commit",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 1,
+ "duration": 0.0010000000000000009,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.013,
+ "type": "layout-effects",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.0019999999999999983,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.016,
+ "type": "passive-effects",
+ },
+ ],
+ 1 => Array [
+ Object {
+ "batchUID": 1,
+ "depth": 0,
+ "duration": 0.010000000000000002,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.019,
+ "type": "render-idle",
+ },
Object {
- "componentMeasures": Array [
- Object {
- "componentName": "App",
- "duration": 0.001,
- "timestamp": 0.006,
- "warning": null,
- },
- Object {
- "componentName": "App",
- "duration": 0.0010000000000000009,
- "timestamp": 0.02,
- "warning": null,
- },
+ "batchUID": 1,
+ "depth": 0,
+ "duration": 0.002999999999999999,
+ "lanes": Array [
+ 4,
],
- "duration": 0.031,
- "flamechart": Array [],
- "laneToLabelMap": Map {
- 0 => "Sync",
- 1 => "InputContinuousHydration",
- 2 => "InputContinuous",
- 3 => "DefaultHydration",
- 4 => "Default",
- 5 => "TransitionHydration",
- 6 => "Transition",
- 7 => "Transition",
- 8 => "Transition",
- 9 => "Transition",
- 10 => "Transition",
- 11 => "Transition",
- 12 => "Transition",
- 13 => "Transition",
- 14 => "Transition",
- 15 => "Transition",
- 16 => "Transition",
- 17 => "Transition",
- 18 => "Transition",
- 19 => "Transition",
- 20 => "Transition",
- 21 => "Transition",
- 22 => "Retry",
- 23 => "Retry",
- 24 => "Retry",
- 25 => "Retry",
- 26 => "Retry",
- 27 => "SelectiveHydration",
- 28 => "IdleHydration",
- 29 => "Idle",
- 30 => "Offscreen",
- },
- "measures": Array [
- Object {
- "batchUID": 0,
- "depth": 0,
- "duration": 0.009999999999999998,
- "lanes": Array [
- 4,
- ],
- "timestamp": 0.005,
- "type": "render-idle",
- },
- Object {
- "batchUID": 0,
- "depth": 0,
- "duration": 0.003,
- "lanes": Array [
- 4,
- ],
- "timestamp": 0.005,
- "type": "render",
- },
- Object {
- "batchUID": 0,
- "depth": 0,
- "duration": 0.006,
- "lanes": Array [
- 4,
- ],
- "timestamp": 0.009,
- "type": "commit",
- },
- Object {
- "batchUID": 0,
- "depth": 1,
- "duration": 0.0010000000000000009,
- "lanes": Array [
- 4,
- ],
- "timestamp": 0.013,
- "type": "layout-effects",
- },
- Object {
- "batchUID": 0,
- "depth": 0,
- "duration": 0.0019999999999999983,
- "lanes": Array [
- 4,
- ],
- "timestamp": 0.016,
- "type": "passive-effects",
- },
- Object {
- "batchUID": 1,
- "depth": 0,
- "duration": 0.010000000000000002,
- "lanes": Array [
- 4,
- ],
- "timestamp": 0.019,
- "type": "render-idle",
- },
- Object {
- "batchUID": 1,
- "depth": 0,
- "duration": 0.002999999999999999,
- "lanes": Array [
- 4,
- ],
- "timestamp": 0.019,
- "type": "render",
- },
- Object {
- "batchUID": 1,
- "depth": 0,
- "duration": 0.006000000000000002,
- "lanes": Array [
- 4,
- ],
- "timestamp": 0.023,
- "type": "commit",
- },
- Object {
- "batchUID": 1,
- "depth": 1,
- "duration": 0.0010000000000000009,
- "lanes": Array [
- 4,
- ],
- "timestamp": 0.027,
- "type": "layout-effects",
- },
- Object {
- "batchUID": 1,
- "depth": 0,
- "duration": 0.0010000000000000009,
- "lanes": Array [
- 4,
- ],
- "timestamp": 0.03,
- "type": "passive-effects",
- },
+ "timestamp": 0.019,
+ "type": "render",
+ },
+ Object {
+ "batchUID": 1,
+ "depth": 0,
+ "duration": 0.006000000000000002,
+ "lanes": Array [
+ 4,
],
- "nativeEvents": Array [],
- "otherUserTimingMarks": Array [
- Object {
- "name": "__v3",
- "timestamp": 0.003,
- },
+ "timestamp": 0.023,
+ "type": "commit",
+ },
+ Object {
+ "batchUID": 1,
+ "depth": 1,
+ "duration": 0.0010000000000000009,
+ "lanes": Array [
+ 4,
],
- "reactVersion": "17.0.3",
- "schedulingEvents": Array [
- Object {
- "lanes": Array [
- 4,
- ],
- "timestamp": 0.004,
- "type": "schedule-render",
- "warning": null,
- },
- Object {
- "componentName": "App",
- "lanes": Array [
- 4,
- ],
- "timestamp": 0.017,
- "type": "schedule-state-update",
- "warning": null,
- },
+ "timestamp": 0.027,
+ "type": "layout-effects",
+ },
+ Object {
+ "batchUID": 1,
+ "depth": 0,
+ "duration": 0.0010000000000000009,
+ "lanes": Array [
+ 4,
],
- "startTime": 4,
- "suspenseEvents": Array [],
- }
- `);
+ "timestamp": 0.03,
+ "type": "passive-effects",
+ },
+ ],
+ },
+ "componentMeasures": Array [
+ Object {
+ "componentName": "App",
+ "duration": 0.001,
+ "timestamp": 0.006,
+ "warning": null,
+ },
+ Object {
+ "componentName": "App",
+ "duration": 0.0010000000000000009,
+ "timestamp": 0.02,
+ "warning": null,
+ },
+ ],
+ "duration": 0.031,
+ "flamechart": Array [],
+ "laneToLabelMap": Map {
+ 0 => "Sync",
+ 1 => "InputContinuousHydration",
+ 2 => "InputContinuous",
+ 3 => "DefaultHydration",
+ 4 => "Default",
+ 5 => "TransitionHydration",
+ 6 => "Transition",
+ 7 => "Transition",
+ 8 => "Transition",
+ 9 => "Transition",
+ 10 => "Transition",
+ 11 => "Transition",
+ 12 => "Transition",
+ 13 => "Transition",
+ 14 => "Transition",
+ 15 => "Transition",
+ 16 => "Transition",
+ 17 => "Transition",
+ 18 => "Transition",
+ 19 => "Transition",
+ 20 => "Transition",
+ 21 => "Transition",
+ 22 => "Retry",
+ 23 => "Retry",
+ 24 => "Retry",
+ 25 => "Retry",
+ 26 => "Retry",
+ 27 => "SelectiveHydration",
+ 28 => "IdleHydration",
+ 29 => "Idle",
+ 30 => "Offscreen",
+ },
+ "laneToReactMeasureMap": Map {
+ 0 => Array [],
+ 1 => Array [],
+ 2 => Array [],
+ 3 => Array [],
+ 4 => Array [
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.009999999999999998,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.005,
+ "type": "render-idle",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.003,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.005,
+ "type": "render",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.006,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.009,
+ "type": "commit",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 1,
+ "duration": 0.0010000000000000009,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.013,
+ "type": "layout-effects",
+ },
+ Object {
+ "batchUID": 0,
+ "depth": 0,
+ "duration": 0.0019999999999999983,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.016,
+ "type": "passive-effects",
+ },
+ Object {
+ "batchUID": 1,
+ "depth": 0,
+ "duration": 0.010000000000000002,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.019,
+ "type": "render-idle",
+ },
+ Object {
+ "batchUID": 1,
+ "depth": 0,
+ "duration": 0.002999999999999999,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.019,
+ "type": "render",
+ },
+ Object {
+ "batchUID": 1,
+ "depth": 0,
+ "duration": 0.006000000000000002,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.023,
+ "type": "commit",
+ },
+ Object {
+ "batchUID": 1,
+ "depth": 1,
+ "duration": 0.0010000000000000009,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.027,
+ "type": "layout-effects",
+ },
+ Object {
+ "batchUID": 1,
+ "depth": 0,
+ "duration": 0.0010000000000000009,
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.03,
+ "type": "passive-effects",
+ },
+ ],
+ 5 => Array [],
+ 6 => Array [],
+ 7 => Array [],
+ 8 => Array [],
+ 9 => Array [],
+ 10 => Array [],
+ 11 => Array [],
+ 12 => Array [],
+ 13 => Array [],
+ 14 => Array [],
+ 15 => Array [],
+ 16 => Array [],
+ 17 => Array [],
+ 18 => Array [],
+ 19 => Array [],
+ 20 => Array [],
+ 21 => Array [],
+ 22 => Array [],
+ 23 => Array [],
+ 24 => Array [],
+ 25 => Array [],
+ 26 => Array [],
+ 27 => Array [],
+ 28 => Array [],
+ 29 => Array [],
+ 30 => Array [],
+ },
+ "nativeEvents": Array [],
+ "otherUserTimingMarks": Array [
+ Object {
+ "name": "__v3",
+ "timestamp": 0.003,
+ },
+ ],
+ "reactVersion": "17.0.3",
+ "schedulingEvents": Array [
+ Object {
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.004,
+ "type": "schedule-render",
+ "warning": null,
+ },
+ Object {
+ "componentName": "App",
+ "lanes": Array [
+ 4,
+ ],
+ "timestamp": 0.017,
+ "type": "schedule-state-update",
+ "warning": null,
+ },
+ ],
+ "startTime": 4,
+ "suspenseEvents": Array [],
+ }
+ `);
}
});
diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
index 532251ce5a3ee..5adb7f89b7701 100644
--- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
@@ -13,13 +13,14 @@ import {
} from '@elg/speedscope';
import type {TimelineEvent} from '@elg/speedscope';
import type {
- Milliseconds,
BatchUID,
Flamechart,
+ Milliseconds,
NativeEvent,
Phase,
ReactLane,
ReactComponentMeasure,
+ ReactMeasure,
ReactMeasureType,
ReactProfilerData,
SchedulingEvent,
@@ -33,7 +34,7 @@ import {getBatchRange} from '../utils/getBatchRange';
type MeasureStackElement = {|
type: ReactMeasureType,
depth: number,
- index: number,
+ measure: ReactMeasure,
startTime: Milliseconds,
stopTime?: Milliseconds,
|};
@@ -92,17 +93,19 @@ export function getLanesFromTransportDecimalBitmask(
return lanes;
}
-const laneToLabelMap: Map = new Map();
-function updateLaneToLabelMap(laneLabelTuplesString: string): void {
+function updateLaneToLabelMap(
+ profilerData: ReactProfilerData,
+ laneLabelTuplesString: string,
+): void {
// These marks appear multiple times in the data;
// We only need to extact them once.
- if (laneToLabelMap.size === 0) {
+ if (profilerData.laneToLabelMap.size === 0) {
const laneLabelTuples = laneLabelTuplesString.split(',');
for (let laneIndex = 0; laneIndex < laneLabelTuples.length; laneIndex++) {
// The numeric lane value (e.g. 64) isn't important.
// The profiler parses and stores the lane's position within the bitmap,
// (e.g. lane 1 is index 0, lane 16 is index 4).
- laneToLabelMap.set(laneIndex, laneLabelTuples[laneIndex]);
+ profilerData.laneToLabelMap.set(laneIndex, laneLabelTuples[laneIndex]);
}
}
}
@@ -133,18 +136,32 @@ function markWorkStarted(
state: ProcessorState,
) {
const {batchUID, measureStack} = state;
- const index = currentProfilerData.measures.length;
const depth = getDepth(measureStack);
- state.measureStack.push({depth, index, startTime, type});
-
- currentProfilerData.measures.push({
+ const measure: ReactMeasure = {
type,
batchUID,
depth,
lanes,
timestamp: startTime,
duration: 0,
+ };
+
+ state.measureStack.push({depth, measure, startTime, type});
+
+ // This array is pre-initialized when the batchUID is generated.
+ const measures = currentProfilerData.batchUIDToMeasuresMap.get(batchUID);
+ if (measures != null) {
+ measures.push(measure);
+ } else {
+ currentProfilerData.batchUIDToMeasuresMap.set(state.batchUID, [measure]);
+ }
+
+ // This array is pre-initialized before processing starts.
+ lanes.forEach(lane => {
+ ((currentProfilerData.laneToReactMeasureMap.get(
+ lane,
+ ): any): ReactMeasure[]).push(measure);
});
}
@@ -174,8 +191,7 @@ function markWorkCompleted(
);
}
- const {index, startTime} = stack.pop();
- const measure = currentProfilerData.measures[index];
+ const {measure, startTime} = stack.pop();
if (!measure) {
console.error('Could not find matching measure for type "%s".', type);
}
@@ -282,7 +298,7 @@ function processTimelineEvent(
}
} else if (name.startsWith('--react-lane-labels-')) {
const [laneLabelTuplesString] = name.substr(20).split('-');
- updateLaneToLabelMap(laneLabelTuplesString);
+ updateLaneToLabelMap(currentProfilerData, laneLabelTuplesString);
} else if (name.startsWith('--component-render-start-')) {
const [componentName] = name.substr(25).split('-');
@@ -648,12 +664,18 @@ export default function preprocessData(
): ReactProfilerData {
const flamechart = preprocessFlamechart(timeline);
+ const laneToReactMeasureMap = new Map();
+ for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) {
+ laneToReactMeasureMap.set(lane, []);
+ }
+
const profilerData: ReactProfilerData = {
+ batchUIDToMeasuresMap: new Map(),
componentMeasures: [],
duration: 0,
flamechart,
- laneToLabelMap,
- measures: [],
+ laneToLabelMap: new Map(),
+ laneToReactMeasureMap,
nativeEvents: [],
otherUserTimingMarks: [],
reactVersion: null,
@@ -739,7 +761,11 @@ export default function preprocessData(
state.potentialSuspenseEventsOutsideOfTransition.forEach(
([suspenseEvent, lanes]) => {
// HACK This is a bit gross but the numeric lane value might change between render versions.
- if (!lanes.some(lane => laneToLabelMap.get(lane) === 'Transition')) {
+ if (
+ !lanes.some(
+ lane => profilerData.laneToLabelMap.get(lane) === 'Transition',
+ )
+ ) {
suspenseEvent.warning = WARNING_STRINGS.SUSPEND_DURING_UPATE;
}
},
diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-scheduling-profiler/src/types.js
index edd683043a56e..761033f2cdda6 100644
--- a/packages/react-devtools-scheduling-profiler/src/types.js
+++ b/packages/react-devtools-scheduling-profiler/src/types.js
@@ -7,7 +7,7 @@
* @flow
*/
-// Type utilities
+import type {ScrollState} from './view-base/utils/scrollState';
// Source: https://github.com/facebook/flow/issues/4002#issuecomment-323612798
// eslint-disable-next-line no-unused-vars
@@ -123,12 +123,29 @@ export type FlamechartStackLayer = FlamechartStackFrame[];
export type Flamechart = FlamechartStackLayer[];
+export type HorizontalScrollStateChangeCallback = (
+ scrollState: ScrollState,
+) => void;
+
+// Imperative view state that corresponds to profiler data.
+// This state lives outside of React's lifecycle
+// and should be erased/reset whenever new profiler data is loaded.
+export type ViewState = {|
+ horizontalScrollState: ScrollState,
+ onHorizontalScrollStateChange: (
+ callback: HorizontalScrollStateChangeCallback,
+ ) => void,
+ updateHorizontalScrollState: (scrollState: ScrollState) => void,
+ viewToMutableViewStateMap: Map,
+|};
+
export type ReactProfilerData = {|
+ batchUIDToMeasuresMap: Map,
componentMeasures: ReactComponentMeasure[],
duration: number,
flamechart: Flamechart,
laneToLabelMap: Map,
- measures: ReactMeasure[],
+ laneToReactMeasureMap: Map,
nativeEvents: NativeEvent[],
otherUserTimingMarks: UserTimingMark[],
reactVersion: string | null,
diff --git a/packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js b/packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js
index 0d977e0390e79..840a105f8d687 100644
--- a/packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js
+++ b/packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js
@@ -9,37 +9,35 @@
import memoize from 'memoize-one';
-import type {BatchUID, Milliseconds, ReactProfilerData} from '../types';
+import type {
+ BatchUID,
+ Milliseconds,
+ ReactMeasure,
+ ReactProfilerData,
+} from '../types';
function unmemoizedGetBatchRange(
batchUID: BatchUID,
data: ReactProfilerData,
- minStartTime?: ?number,
+ minStartTime?: number = 0,
): [Milliseconds, Milliseconds] {
- const {measures} = data;
-
- let startTime = 0;
- let stopTime = Infinity;
+ const measures = data.batchUIDToMeasuresMap.get(batchUID);
+ if (measures == null || measures.length === 0) {
+ throw Error(`Could not find measures with batch UID "${batchUID}"`);
+ }
- let i = 0;
+ const lastMeasure = ((measures[measures.length - 1]: any): ReactMeasure);
+ const stopTime = lastMeasure.timestamp + lastMeasure.duration;
- // Find the first measure in the current batch.
- for (i; i < measures.length; i++) {
- const measure = measures[i];
- if (measure.batchUID === batchUID) {
- if (minStartTime == null || measure.timestamp >= minStartTime) {
- startTime = measure.timestamp;
- break;
- }
- }
+ if (stopTime < minStartTime) {
+ return [0, 0];
}
- // Find the last measure in the current batch.
- for (i; i < measures.length; i++) {
- const measure = measures[i];
- if (measure.batchUID === batchUID) {
- stopTime = measure.timestamp;
- } else {
+ let startTime = minStartTime;
+ for (let index = 0; index < measures.length; index++) {
+ const measure = measures[index];
+ if (measure.timestamp >= minStartTime) {
+ startTime = measure.timestamp;
break;
}
}
diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js b/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js
index 78d90e2495da9..5519599f12dcc 100644
--- a/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js
+++ b/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js
@@ -18,6 +18,7 @@ import type {
import type {Rect} from './geometry';
import type {ScrollState} from './utils/scrollState';
import type {ViewRefs} from './Surface';
+import type {ViewState} from '../types';
import {Surface} from './Surface';
import {View} from './View';
@@ -30,96 +31,84 @@ import {
zoomState,
} from './utils/scrollState';
import {
- DEFAULT_ZOOM_LEVEL,
MAX_ZOOM_LEVEL,
MIN_ZOOM_LEVEL,
MOVE_WHEEL_DELTA_THRESHOLD,
} from './constants';
-export type HorizontalPanAndZoomViewOnChangeCallback = (
- state: ScrollState,
- view: HorizontalPanAndZoomView,
-) => void;
-
export class HorizontalPanAndZoomView extends View {
+ _contentView: View;
_intrinsicContentWidth: number;
_isPanning = false;
- _scrollState: ScrollState = {offset: 0, length: 0};
- _onStateChange: HorizontalPanAndZoomViewOnChangeCallback = () => {};
+ _viewState: ViewState;
constructor(
surface: Surface,
frame: Rect,
contentView: View,
intrinsicContentWidth: number,
- onStateChange?: HorizontalPanAndZoomViewOnChangeCallback,
+ viewState: ViewState,
) {
super(surface, frame);
- this.addSubview(contentView);
- this._intrinsicContentWidth = intrinsicContentWidth;
- this._setScrollState({
- offset: 0,
- length: intrinsicContentWidth * DEFAULT_ZOOM_LEVEL,
- });
- if (onStateChange) this._onStateChange = onStateChange;
- }
- setFrame(newFrame: Rect) {
- super.setFrame(newFrame);
+ this._contentView = contentView;
+ this._intrinsicContentWidth = intrinsicContentWidth;
+ this._viewState = viewState;
- // Revalidate scrollState
- this._setStateAndInformCallbacksIfChanged(this._scrollState);
- }
+ viewState.onHorizontalScrollStateChange(scrollState => {
+ this.zoomToRange(scrollState.offset, scrollState.length);
+ });
- setScrollState(proposedState: ScrollState) {
- this._setScrollState(proposedState);
+ this.addSubview(contentView);
}
/**
- * Just sets scroll state. Use `_setStateAndInformCallbacksIfChanged` if this
- * view's callbacks should also be called.
+ * Just sets scroll state.
+ * Use `_setStateAndInformCallbacksIfChanged` if this view's callbacks should also be called.
*
* @returns Whether state was changed
* @private
*/
- _setScrollState(proposedState: ScrollState): boolean {
+ setScrollState(proposedState: ScrollState) {
const clampedState = clampState({
state: proposedState,
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
containerLength: this.frame.size.width,
});
- if (areScrollStatesEqual(clampedState, this._scrollState)) {
- return false;
+ if (
+ !areScrollStatesEqual(clampedState, this._viewState.horizontalScrollState)
+ ) {
+ this.setNeedsDisplay();
}
- this._scrollState = clampedState;
- this.setNeedsDisplay();
- return true;
}
/**
- * @private
+ * Zoom to a specific range of the content specified as a range of the
+ * content view's intrinsic content size.
+ *
+ * Does not inform callbacks of state change since this is a public API.
*/
- _setStateAndInformCallbacksIfChanged(proposedState: ScrollState) {
- if (this._setScrollState(proposedState)) {
- this._onStateChange(this._scrollState, this);
- }
+ zoomToRange(rangeStart: number, rangeEnd: number) {
+ const newState = moveStateToRange({
+ state: this._viewState.horizontalScrollState,
+ rangeStart,
+ rangeEnd,
+ contentLength: this._intrinsicContentWidth,
+
+ minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
+ maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
+ containerLength: this.frame.size.width,
+ });
+ this.setScrollState(newState);
}
desiredSize() {
return this._contentView.desiredSize();
}
- /**
- * Reference to the content view. This view is also the only view in
- * `this.subviews`.
- */
- get _contentView() {
- return this.subviews[0];
- }
-
layoutSubviews() {
- const {offset, length} = this._scrollState;
+ const {offset, length} = this._viewState.horizontalScrollState;
const proposedFrame = {
origin: {
x: this.frame.origin.x + offset,
@@ -134,24 +123,22 @@ export class HorizontalPanAndZoomView extends View {
super.layoutSubviews();
}
- /**
- * Zoom to a specific range of the content specified as a range of the
- * content view's intrinsic content size.
- *
- * Does not inform callbacks of state change since this is a public API.
- */
- zoomToRange(rangeStart: number, rangeEnd: number) {
- const newState = moveStateToRange({
- state: this._scrollState,
- rangeStart,
- rangeEnd,
- contentLength: this._intrinsicContentWidth,
-
- minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
- maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
- containerLength: this.frame.size.width,
- });
- this._setScrollState(newState);
+ handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
+ switch (interaction.type) {
+ case 'mousedown':
+ this._handleMouseDown(interaction, viewRefs);
+ break;
+ case 'mousemove':
+ this._handleMouseMove(interaction, viewRefs);
+ break;
+ case 'mouseup':
+ this._handleMouseUp(interaction, viewRefs);
+ break;
+ case 'wheel-plain':
+ case 'wheel-shift':
+ this._handleWheel(interaction);
+ break;
+ }
}
_handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
@@ -183,11 +170,11 @@ export class HorizontalPanAndZoomView extends View {
return;
}
const newState = translateState({
- state: this._scrollState,
+ state: this._viewState.horizontalScrollState,
delta: interaction.payload.event.movementX,
containerLength: this.frame.size.width,
});
- this._setStateAndInformCallbacksIfChanged(newState);
+ this._viewState.updateHorizontalScrollState(newState);
}
_handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
@@ -226,44 +213,26 @@ export class HorizontalPanAndZoomView extends View {
}
const newState = zoomState({
- state: this._scrollState,
+ state: this._viewState.horizontalScrollState,
multiplier: 1 + 0.005 * -deltaY,
- fixedPoint: location.x - this._scrollState.offset,
+ fixedPoint: location.x - this._viewState.horizontalScrollState.offset,
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
containerLength: this.frame.size.width,
});
- this._setStateAndInformCallbacksIfChanged(newState);
+ this._viewState.updateHorizontalScrollState(newState);
} else {
if (absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD) {
return;
}
const newState = translateState({
- state: this._scrollState,
+ state: this._viewState.horizontalScrollState,
delta: -deltaX,
containerLength: this.frame.size.width,
});
- this._setStateAndInformCallbacksIfChanged(newState);
- }
- }
-
- handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
- switch (interaction.type) {
- case 'mousedown':
- this._handleMouseDown(interaction, viewRefs);
- break;
- case 'mousemove':
- this._handleMouseMove(interaction, viewRefs);
- break;
- case 'mouseup':
- this._handleMouseUp(interaction, viewRefs);
- break;
- case 'wheel-plain':
- case 'wheel-shift':
- this._handleWheel(interaction);
- break;
+ this._viewState.updateHorizontalScrollState(newState);
}
}
}
diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js
index 8394bcf105770..1f0f890ce38ef 100644
--- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js
+++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js
@@ -17,6 +17,7 @@ import type {
} from './useCanvasInteraction';
import type {Rect} from './geometry';
import type {ViewRefs} from './Surface';
+import type {ViewState} from '../types';
import {BORDER_SIZE, COLORS} from '../content-views/constants';
import {drawText} from '../content-views/utils/text';
@@ -35,10 +36,10 @@ type ResizingState = $ReadOnly<{|
mouseY: number,
|}>;
-type LayoutState = $ReadOnly<{|
+type LayoutState = {|
/** Resize bar's vertical position relative to resize view's frame.origin.y */
barOffsetY: number,
-|}>;
+|};
const RESIZE_BAR_DOT_RADIUS = 1;
const RESIZE_BAR_DOT_SPACING = 4;
@@ -217,36 +218,33 @@ class ResizeBar extends View {
export class ResizableView extends View {
_canvasRef: {current: HTMLCanvasElement | null};
_layoutState: LayoutState;
+ _mutableViewStateKey: string;
_resizeBar: ResizeBar;
_resizingState: ResizingState | null = null;
_subview: View;
+ _viewState: ViewState;
constructor(
surface: Surface,
frame: Rect,
subview: View,
+ viewState: ViewState,
canvasRef: {current: HTMLCanvasElement | null},
label: string,
) {
super(surface, frame, noopLayout);
this._canvasRef = canvasRef;
-
+ this._layoutState = {barOffsetY: 0};
+ this._mutableViewStateKey = label + ':ResizableView';
this._subview = subview;
this._resizeBar = new ResizeBar(surface, frame, label);
+ this._viewState = viewState;
this.addSubview(this._subview);
this.addSubview(this._resizeBar);
- const subviewDesiredSize = subview.desiredSize();
- this._updateLayoutStateAndResizeBar(
- subviewDesiredSize.maxInitialHeight != null
- ? Math.min(
- subviewDesiredSize.maxInitialHeight,
- subviewDesiredSize.height,
- )
- : subviewDesiredSize.height,
- );
+ this._restoreMutableViewState();
}
desiredSize() {
@@ -274,6 +272,35 @@ export class ResizableView extends View {
super.layoutSubviews();
}
+ _restoreMutableViewState() {
+ if (
+ this._viewState.viewToMutableViewStateMap.has(this._mutableViewStateKey)
+ ) {
+ this._layoutState = ((this._viewState.viewToMutableViewStateMap.get(
+ this._mutableViewStateKey,
+ ): any): LayoutState);
+
+ this._updateLayoutStateAndResizeBar(this._layoutState.barOffsetY);
+ } else {
+ this._viewState.viewToMutableViewStateMap.set(
+ this._mutableViewStateKey,
+ this._layoutState,
+ );
+
+ const subviewDesiredSize = this._subview.desiredSize();
+ this._updateLayoutStateAndResizeBar(
+ subviewDesiredSize.maxInitialHeight != null
+ ? Math.min(
+ subviewDesiredSize.maxInitialHeight,
+ subviewDesiredSize.height,
+ )
+ : subviewDesiredSize.height,
+ );
+ }
+
+ this.setNeedsDisplay();
+ }
+
_shouldRenderResizeBar() {
const subviewDesiredSize = this._subview.desiredSize();
return subviewDesiredSize.hideScrollBarIfLessThanHeight != null
@@ -287,10 +314,7 @@ export class ResizableView extends View {
barOffsetY = 0;
}
- this._layoutState = {
- ...this._layoutState,
- barOffsetY,
- };
+ this._layoutState.barOffsetY = barOffsetY;
this._resizeBar.showLabel = barOffsetY === 0;
}
@@ -341,6 +365,10 @@ export class ResizableView extends View {
}
_handleClick(interaction: ClickInteraction) {
+ if (!this._shouldRenderResizeBar()) {
+ return;
+ }
+
const cursorInView = rectContainsPoint(
interaction.payload.location,
this.frame,
@@ -356,6 +384,10 @@ export class ResizableView extends View {
}
_handleDoubleClick(interaction: DoubleClickInteraction) {
+ if (!this._shouldRenderResizeBar()) {
+ return;
+ }
+
const cursorInView = rectContainsPoint(
interaction.payload.location,
this.frame,
diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js
index 245b53aff7180..3f2f775912166 100644
--- a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js
+++ b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js
@@ -17,6 +17,7 @@ import type {
import type {Rect} from './geometry';
import type {ScrollState} from './utils/scrollState';
import type {ViewRefs} from './Surface';
+import type {ViewState} from '../types';
import {Surface} from './Surface';
import {View} from './View';
@@ -34,13 +35,33 @@ const CARET_WIDTH = 5;
const CARET_HEIGHT = 3;
export class VerticalScrollView extends View {
- _scrollState: ScrollState = {offset: 0, length: 0};
- _isPanning = false;
-
- constructor(surface: Surface, frame: Rect, contentView: View) {
+ _contentView: View;
+ _isPanning: boolean;
+ _mutableViewStateKey: string;
+ _scrollState: ScrollState;
+ _viewState: ViewState;
+
+ constructor(
+ surface: Surface,
+ frame: Rect,
+ contentView: View,
+ viewState: ViewState,
+ label: string,
+ ) {
super(surface, frame);
+
+ this._contentView = contentView;
+ this._isPanning = false;
+ this._mutableViewStateKey = label + ':VerticalScrollView';
+ this._scrollState = {
+ offset: 0,
+ length: 0,
+ };
+ this._viewState = viewState;
+
this.addSubview(contentView);
- this._setScrollState(this._scrollState);
+
+ this._restoreMutableViewState();
}
setFrame(newFrame: Rect) {
@@ -102,14 +123,6 @@ export class VerticalScrollView extends View {
}
}
- /**
- * Reference to the content view. This view is also the only view in
- * `this.subviews`.
- */
- get _contentView() {
- return this.subviews[0];
- }
-
layoutSubviews() {
const {offset} = this._scrollState;
const desiredSize = this._contentView.desiredSize();
@@ -133,6 +146,23 @@ export class VerticalScrollView extends View {
super.layoutSubviews();
}
+ handleInteraction(interaction: Interaction) {
+ switch (interaction.type) {
+ case 'mousedown':
+ this._handleMouseDown(interaction);
+ break;
+ case 'mousemove':
+ this._handleMouseMove(interaction);
+ break;
+ case 'mouseup':
+ this._handleMouseUp(interaction);
+ break;
+ case 'wheel-shift':
+ this._handleWheelShift(interaction);
+ break;
+ }
+ }
+
_handleMouseDown(interaction: MouseDownInteraction) {
if (rectContainsPoint(interaction.payload.location, this.frame)) {
this._isPanning = true;
@@ -184,21 +214,21 @@ export class VerticalScrollView extends View {
this._setScrollState(newState);
}
- handleInteraction(interaction: Interaction) {
- switch (interaction.type) {
- case 'mousedown':
- this._handleMouseDown(interaction);
- break;
- case 'mousemove':
- this._handleMouseMove(interaction);
- break;
- case 'mouseup':
- this._handleMouseUp(interaction);
- break;
- case 'wheel-shift':
- this._handleWheelShift(interaction);
- break;
+ _restoreMutableViewState() {
+ if (
+ this._viewState.viewToMutableViewStateMap.has(this._mutableViewStateKey)
+ ) {
+ this._scrollState = ((this._viewState.viewToMutableViewStateMap.get(
+ this._mutableViewStateKey,
+ ): any): ScrollState);
+ } else {
+ this._viewState.viewToMutableViewStateMap.set(
+ this._mutableViewStateKey,
+ this._scrollState,
+ );
}
+
+ this.setNeedsDisplay();
}
/**
@@ -212,10 +242,11 @@ export class VerticalScrollView extends View {
maxContentLength: height,
containerLength: this.frame.size.height,
});
- if (areScrollStatesEqual(clampedState, this._scrollState)) {
- return;
+ if (!areScrollStatesEqual(clampedState, this._scrollState)) {
+ this._scrollState.offset = clampedState.offset;
+ this._scrollState.length = clampedState.length;
+
+ this.setNeedsDisplay();
}
- this._scrollState = clampedState;
- this.setNeedsDisplay();
}
}
diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/utils/scrollState.js b/packages/react-devtools-scheduling-profiler/src/view-base/utils/scrollState.js
index e7f73a86d33fb..9e9bdb4919f3f 100644
--- a/packages/react-devtools-scheduling-profiler/src/view-base/utils/scrollState.js
+++ b/packages/react-devtools-scheduling-profiler/src/view-base/utils/scrollState.js
@@ -18,10 +18,10 @@ import {clamp} from './clamp';
* |<-------------------length------------------->|
* ```
*/
-export type ScrollState = $ReadOnly<{|
+export type ScrollState = {|
offset: number,
length: number,
-|}>;
+|};
function clampOffset(state: ScrollState, containerLength: number): ScrollState {
return {