Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spike: Very selective canvas rerender #6086

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions editor/src/components/canvas/canvas-component-entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useDispatch } from '../editor/store/dispatch-context'
import {
CanvasStateContext,
EditorStateContext,
LowPriorityStateContext,
Substores,
useEditorState,
} from '../editor/store/store-hook'
Expand All @@ -23,15 +24,19 @@ import type {
UiJsxCanvasPropsWithErrorCallback,
} from './ui-jsx-canvas'
import { DomWalkerInvalidatePathsCtxAtom, UiJsxCanvas, pickUiJsxCanvasProps } from './ui-jsx-canvas'
import { PerformanceOptimizedCanvasContextProvider } from './ui-jsx-canvas-renderer/ui-jsx-canvas-selective-context-provider'

interface CanvasComponentEntryProps {}

export const CanvasComponentEntry = React.memo((props: CanvasComponentEntryProps) => {
const canvasStore = React.useContext(CanvasStateContext)
const lowPrioritystore = React.useContext(LowPriorityStateContext) // TODO before merge: use the low priority provider!!

return (
<EditorStateContext.Provider value={canvasStore == null ? null : canvasStore}>
<CanvasComponentEntryInner {...props} />
<EditorStateContext.Provider value={lowPrioritystore == null ? null : lowPrioritystore}>
<PerformanceOptimizedCanvasContextProvider targetPath={null}>
<CanvasComponentEntryInner {...props} />
</PerformanceOptimizedCanvasContextProvider>
</EditorStateContext.Provider>
)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type ComponentRendererComponent = React.ComponentType<
> & {
topLevelElementName: string | null
propertyControls?: PropertyControls
utopiaType: 'UTOPIA_COMPONENT_RENDERER_COMPONENT'
utopiaType: 'UTOPIA_COMPONENT_WRAPPER_COMPONENT' | 'UTOPIA_COMPONENT_RENDERER_COMPONENT'
filePath: string
originalName: string | null
}
Expand All @@ -25,6 +25,9 @@ export function isComponentRendererComponent(
return (
component != null &&
typeof component === 'function' &&
(component as ComponentRendererComponent).utopiaType === 'UTOPIA_COMPONENT_RENDERER_COMPONENT'
((component as ComponentRendererComponent).utopiaType ===
'UTOPIA_COMPONENT_WRAPPER_COMPONENT' ||
(component as ComponentRendererComponent).utopiaType ===
'UTOPIA_COMPONENT_RENDERER_COMPONENT')
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ import { mapArrayToDictionary } from '../../../core/shared/array-utils'
import { assertNever } from '../../../core/shared/utils'
import { addFakeSpyEntry } from './ui-jsx-canvas-spy-wrapper'
import type { FilePathMappings } from '../../../core/model/project-file-utils'
import {
CanvasStateMetaContext,
PerformanceOptimizedCanvasContextProvider,
} from './ui-jsx-canvas-selective-context-provider'
import { enableWhyDidYouRenderOnComponent } from '../../../utils/react-memoize.test-utils'

function tryToGetInstancePathFromAllProps(props: any): ElementPath | null {
const { [UTOPIA_INSTANCE_PATH]: instancePathAny, [UTOPIA_PATH_KEY]: pathsString } = props

return tryToGetInstancePath(instancePathAny, pathsString)
}

function tryToGetInstancePath(
maybePath: ElementPath | null,
Expand All @@ -65,12 +76,33 @@ function tryToGetInstancePath(
}
}

let componentRenderNumber: { [componentName: string]: { rerenderNumber: number } } = {}

export function createComponentRendererComponent(params: {
topLevelElementName: string | null
filePath: string
mutableContextRef: React.MutableRefObject<MutableUtopiaCtxRefData>
}): ComponentRendererComponent {
const Component = (...functionArguments: Array<any>) => {
if (componentRenderNumber[params.topLevelElementName ?? ''] == null) {
componentRenderNumber[params.topLevelElementName ?? ''] = { rerenderNumber: 0 }
} else {
componentRenderNumber[params.topLevelElementName ?? ''].rerenderNumber++
}

if (componentRenderNumber[params.topLevelElementName ?? ''].rerenderNumber > 1000) {
throw new Error(
`ComponentRendererComponent rerendered more than 1000 times: ${params.topLevelElementName}`,
)
}
// throw an error if this component is accidentally used outside of a PerformanceSensitiveCanvsaContextProvider
const canvasStateMeta = React.useContext(CanvasStateMetaContext)
if (canvasStateMeta == null) {
throw new Error(
`ComponentRendererComponent used outside of a PerformanceSensitiveCanvsaContextProvider`,
)
}

// Attempt to determine which function argument is the "regular" props object/value.
// Default it to the first if one is not identified by looking for some of our special keys.
let regularPropsArgumentIndex: number = functionArguments.findIndex((functionArgument) => {
Expand Down Expand Up @@ -99,7 +131,9 @@ export function createComponentRendererComponent(params: {

const mutableContext = params.mutableContextRef.current[params.filePath].mutableContext

const instancePath: ElementPath | null = tryToGetInstancePath(instancePathAny, pathsString)
const instancePath: ElementPath | null = tryToGetInstancePathFromAllProps(
functionArguments[regularPropsArgumentIndex],
)

function shouldUpdate() {
return (
Expand Down Expand Up @@ -366,7 +400,31 @@ export function createComponentRendererComponent(params: {
Component.utopiaType = 'UTOPIA_COMPONENT_RENDERER_COMPONENT' as const
Component.filePath = params.filePath
Component.originalName = params.topLevelElementName
return Component
if (params.topLevelElementName === 'storyboard') {
enableWhyDidYouRenderOnComponent(Component)
}

const WrapperComponentToGetStateContext = (...args: any[]) => {
const elementPath = tryToGetInstancePathFromAllProps(args[0])

return (
// {/* TODO how to pass down rest of params */}
// <PerformanceOptimizedCanvasContextProvider targetPath={elementPath}>
<Component {...args[0]} />
// </PerformanceOptimizedCanvasContextProvider>
)
}

WrapperComponentToGetStateContext.displayName = `ComponentRendererWrapper(${params.topLevelElementName})`
WrapperComponentToGetStateContext.topLevelElementName = params.topLevelElementName
WrapperComponentToGetStateContext.utopiaType = 'UTOPIA_COMPONENT_WRAPPER_COMPONENT' as const
WrapperComponentToGetStateContext.filePath = params.filePath
WrapperComponentToGetStateContext.originalName = params.topLevelElementName
if (params.topLevelElementName === 'storyboard') {
enableWhyDidYouRenderOnComponent(WrapperComponentToGetStateContext)
}

return WrapperComponentToGetStateContext
}

function isRenderProp(prop: any): prop is { props: { [UTOPIA_PATH_KEY]: string } } {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type React from 'react'
import React from 'react'
import { emptySet } from '../../../core/shared/set-utils'
import type { MapLike } from 'typescript'
import { atomWithPubSub } from '../../../core/shared/atom-with-pub-sub'
Expand Down Expand Up @@ -59,13 +59,10 @@ const EmptyResolve = (importOrigin: string, toImport: string): Either<string, st
return left(`Error while resolving ${toImport}, the resolver is missing`)
}

export const UtopiaProjectCtxAtom = atomWithPubSub<UtopiaProjectCtxProps>({
key: 'UtopiaProjectCtxAtom',
defaultValue: {
projectContents: {},
openStoryboardFilePathKILLME: null,
resolve: EmptyResolve,
},
export const UtopiaProjectCtxAtom = React.createContext<UtopiaProjectCtxProps>({
projectContents: {},
openStoryboardFilePathKILLME: null,
resolve: EmptyResolve,
})

interface SceneLevelContextProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export function useGetCodeAndHighlightBounds(
code: string
highlightBounds: HighlightBoundsForUids | null
} {
const projectContext = usePubSubAtomReadOnly(UtopiaProjectCtxAtom, shouldUpdateCallback)
const projectContext = React.useContext(UtopiaProjectCtxAtom)
if (filePath == null) {
return emptyHighlightBoundsResult
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React from 'react'
import type { ElementPath } from 'utopia-shared/src/types'
import {
CanvasStateContext,
EditorStateContext,
LowPriorityStateContext,
Substores,
useEditorState,
} from '../../editor/store/store-hook'
import { ElementsToRerenderGLOBAL } from '../ui-jsx-canvas'
import { UtopiaProjectCtxAtom } from './ui-jsx-canvas-contexts'
import { useKeepShallowReferenceEquality } from '../../../utils/react-performance'
import { optionalMap } from '../../../core/shared/optional-utils'
import { toString } from '../../../core/shared/element-path'
import { enableWhyDidYouRenderOnComponent } from '../../../utils/react-memoize.test-utils'
import { left } from '../../../core/shared/either'
import type { ResolveFn } from '../../custom-code/code-file'

type CanvasStateMeta = {
providedStoreType: 'canvas' | 'low-priority'
rerenderExpected: boolean
}

export const CanvasStateMetaContext = React.createContext<CanvasStateMeta | null>(null)

export const PerformanceOptimizedCanvasContextProvider: React.FunctionComponent<
React.PropsWithChildren<{ targetPath: ElementPath | null }>
> = (props) => {
const realtimeCanvasStore = React.useContext(CanvasStateContext)
const maybeOldLowPriorityStore = React.useContext(LowPriorityStateContext)

const shouldUseRealtimeStore =
ElementsToRerenderGLOBAL.current === 'rerender-all-elements' ||
(props.targetPath != null && ElementsToRerenderGLOBAL.current.includes(props.targetPath))

const storeToUse = shouldUseRealtimeStore ? realtimeCanvasStore : maybeOldLowPriorityStore

const canvasStateMeta: CanvasStateMeta = React.useMemo(() => {
return {
providedStoreType: shouldUseRealtimeStore ? 'canvas' : 'low-priority',
rerenderExpected: shouldUseRealtimeStore,
}
}, [shouldUseRealtimeStore])

// console.log(
// 'CanvasStateMetaContext.Provider',
// optionalMap(toString, props.targetPath),
// canvasStateMeta,
// )

return (
<CanvasStateMetaContext.Provider value={canvasStateMeta}>
<EditorStateContext.Provider value={storeToUse}>
<CanvasContextProviderInner>{props.children}</CanvasContextProviderInner>
</EditorStateContext.Provider>
</CanvasStateMetaContext.Provider>
)
}
PerformanceOptimizedCanvasContextProvider.displayName = 'PerformanceOptimizedCanvasContextProvider'

const CanvasContextProviderInner: React.FunctionComponent<React.PropsWithChildren<unknown>> = (
props,
) => {
const utopiaProjectContextValue = useCreateUtopiaProjectCtx()
const previousValue = React.useRef(utopiaProjectContextValue)

// if (previousValue.current !== utopiaProjectContextValue) {
// console.log(
// 'LOST CanvasContextProviderInner utopiaProjectContextValue LOST',
// utopiaProjectContextValue,
// )
// } else {
// console.log('PRESERVED UtopiaProjectCtxAtom', utopiaProjectContextValue)
// }
previousValue.current = utopiaProjectContextValue

return (
<UtopiaProjectCtxAtom.Provider value={utopiaProjectContextValue}>
{props.children}
</UtopiaProjectCtxAtom.Provider>
)
}
CanvasContextProviderInner.displayName = 'CanvasContextProviderInner'
// enableWhyDidYouRenderOnComponent(CanvasContextProviderInner)

function useCreateUtopiaProjectCtx() {
const projectContents = useEditorState(
Substores.projectContents,
(store) => store.editor.projectContents,
'useCreateUtopiaProjectCtx projectContents',
)

const uiFilePath = useEditorState(
Substores.canvas,
(store) => store.editor.canvas.openFile?.filename ?? null,
'useCreateUtopiaProjectCtx uiFilePath',
)

const curriedResolveFn = useEditorState(
Substores.restOfEditor,
(store) => store.editor.codeResultCache.curriedResolveFn,
'useCreateUtopiaProjectCtx curriedResolveFn',
)

const resolve: ResolveFn = React.useMemo(() => {
return curriedResolveFn(projectContents)
}, [curriedResolveFn, projectContents])

const utopiaProjectContextValue = useKeepShallowReferenceEquality({
projectContents: projectContents,
openStoryboardFilePathKILLME: uiFilePath,
resolve: resolve,
})

return utopiaProjectContextValue
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react'
import { usePubSubAtomReadOnly } from '../../../core/shared/atom-with-pub-sub'
import type { TopLevelElement } from '../../../core/shared/element-template'
import type { Imports } from '../../../core/shared/project-file-types'
Expand All @@ -14,7 +15,7 @@ export function useGetTopLevelElementsAndImports(
topLevelElements: TopLevelElement[]
imports: Imports
} {
const projectContext = usePubSubAtomReadOnly(UtopiaProjectCtxAtom, shouldUpdateCallback) // TODO MAYBE create a usePubSubAtomSelector
const projectContext = React.useContext(UtopiaProjectCtxAtom)
if (filePath == null) {
return emptyResult
} else {
Expand Down
7 changes: 4 additions & 3 deletions editor/src/components/canvas/ui-jsx-canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,9 +318,10 @@ export const UiJsxCanvas = React.memo<UiJsxCanvasPropsWithErrorCallback>((props)
let evaluatedFileNames = React.useRef<Array<string>>([]) // evaluated (i.e. not using a cached evaluation) this render
evaluatedFileNames.current = [uiFilePath]

if (!IS_TEST_ENVIRONMENT) {
listenForReactRouterErrors(console)
}
// TODO undo before merge
// if (!IS_TEST_ENVIRONMENT) {
// listenForReactRouterErrors(console)
// }

React.useEffect(() => {
if (clearErrors != null) {
Expand Down
6 changes: 4 additions & 2 deletions editor/src/templates/editor-entry-point-imports.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// import * as React from 'react'
// const whyDidYouRender = require('@welldone-software/why-did-you-render')
// import whyDidYouRender from '@welldone-software/why-did-you-render'
// whyDidYouRender(React, {
// trackAllPureComponents: true,
// trackAllPureComponents: false,
// logOnDifferentValues: true,
// logOwnerReasons: true,
// })

import '../vite-shims'
Expand Down
Loading