Skip to content

Commit

Permalink
Scheduling Profiler: Misc UX and performance improvements (#22043)
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Vaughn authored Aug 9, 2021
1 parent ecd73e1 commit 5634ed1
Show file tree
Hide file tree
Showing 12 changed files with 1,076 additions and 623 deletions.
126 changes: 78 additions & 48 deletions packages/react-devtools-scheduling-profiler/src/CanvasPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -54,29 +53,37 @@ 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';

const CONTEXT_MENU_ID = 'canvas';

type Props = {|
profilerData: ReactProfilerData,
viewState: ViewState,
|};

function CanvasPage({profilerData}: Props) {
function CanvasPage({profilerData, viewState}: Props) {
return (
<div
className={styles.CanvasPage}
style={{backgroundColor: COLORS.BACKGROUND}}>
<AutoSizer>
{({height, width}: {height: number, width: number}) => (
<AutoSizedCanvas data={profilerData} height={height} width={width} />
<AutoSizedCanvas
data={profilerData}
height={height}
viewState={viewState}
width={width}
/>
)}
</AutoSizer>
</div>
Expand All @@ -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<HTMLCanvasElement | null>(null);

const [isContextMenuShown, setIsContextMenuShown] = useState<boolean>(false);
Expand All @@ -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<HorizontalPanAndZoomView[]>(
[],
);

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 {
Expand All @@ -169,6 +193,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
surface,
defaultFrame,
view,
viewState,
label,
);
}

Expand All @@ -177,31 +203,30 @@ 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(
surface,
defaultFrame,
data.duration,
);
const axisMarkersViewWrapper = createViewHelper(axisMarkersView);
const axisMarkersViewWrapper = createViewHelper(axisMarkersView, 'time');

let userTimingMarksViewWrapper = null;
if (data.otherUserTimingMarks.length > 0) {
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -256,7 +287,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
reactMeasuresViewRef.current = reactMeasuresView;
const reactMeasuresViewWrapper = createViewHelper(
reactMeasuresView,
'react',
'react scheduling',
true,
true,
);
Expand All @@ -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(
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -617,11 +651,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
{measure !== null && (
<ContextMenuItem
onClick={() =>
zoomToBatch(
contextData.data,
measure,
syncedHorizontalPanAndZoomViewsRef.current,
)
zoomToBatch(contextData.data, measure, viewState, width)
}
title="Zoom to batch">
Zoom to batch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import type {DataResource} from './createDataResourceFromImportedFile';
import type {ViewState} from './types';

import * as React from 'react';
import {
Expand All @@ -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);

Expand Down Expand Up @@ -66,6 +69,7 @@ export function SchedulingProfiler(_: {||}) {
dataResource={schedulingProfilerData}
key={key}
onFileSelect={importSchedulingProfilerData}
viewState={viewState}
/>
</Suspense>
) : (
Expand Down Expand Up @@ -130,15 +134,17 @@ 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) {
return (
<CouldNotLoadProfile error={dataOrError} onFileSelect={onFileSelect} />
);
}
return <CanvasPage profilerData={dataOrError} />;
return <CanvasPage profilerData={dataOrError} viewState={viewState} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<Context>(
Expand All @@ -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<ViewState>(() => {
const horizontalScrollStateChangeCallbacks: Set<HorizontalScrollStateChangeCallback> = 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,
],
);

Expand Down
Loading

0 comments on commit 5634ed1

Please sign in to comment.