From f0438f935e97601cf6024f96ed0893af19037c6d Mon Sep 17 00:00:00 2001 From: Balazs Bajorics <2226774+balazsbajorics@users.noreply.github.com> Date: Tue, 16 Jul 2024 15:05:46 +0200 Subject: [PATCH] checkpoint (doesn't work yet) --- .../canvas/canvas-component-entry.tsx | 9 +- .../component-renderer-component.tsx | 7 +- .../ui-jsx-canvas-component-renderer.tsx | 62 +++++++++- .../ui-jsx-canvas-contexts.tsx | 13 +- .../ui-jsx-canvas-execution-scope.tsx | 2 +- ...-jsx-canvas-selective-context-provider.tsx | 116 ++++++++++++++++++ .../ui-jsx-canvas-top-level-elements.tsx | 3 +- .../src/components/canvas/ui-jsx-canvas.tsx | 7 +- .../templates/editor-entry-point-imports.tsx | 6 +- 9 files changed, 204 insertions(+), 21 deletions(-) create mode 100644 editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-selective-context-provider.tsx diff --git a/editor/src/components/canvas/canvas-component-entry.tsx b/editor/src/components/canvas/canvas-component-entry.tsx index a6e78a6188cb..5117bb76972c 100644 --- a/editor/src/components/canvas/canvas-component-entry.tsx +++ b/editor/src/components/canvas/canvas-component-entry.tsx @@ -12,6 +12,7 @@ import { useDispatch } from '../editor/store/dispatch-context' import { CanvasStateContext, EditorStateContext, + LowPriorityStateContext, Substores, useEditorState, } from '../editor/store/store-hook' @@ -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 ( - - + + + + ) }) diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/component-renderer-component.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/component-renderer-component.tsx index 330d83128b4d..0403cc359687 100644 --- a/editor/src/components/canvas/ui-jsx-canvas-renderer/component-renderer-component.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/component-renderer-component.tsx @@ -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 } @@ -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') ) } diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-component-renderer.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-component-renderer.tsx index 0607baa26b0c..d864a1f9781e 100644 --- a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-component-renderer.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-component-renderer.tsx @@ -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, @@ -65,12 +76,33 @@ function tryToGetInstancePath( } } +let componentRenderNumber: { [componentName: string]: { rerenderNumber: number } } = {} + export function createComponentRendererComponent(params: { topLevelElementName: string | null filePath: string mutableContextRef: React.MutableRefObject }): ComponentRendererComponent { const Component = (...functionArguments: Array) => { + 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) => { @@ -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 ( @@ -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 */} + // + + // + ) + } + + 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 } } { diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-contexts.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-contexts.tsx index f223928216b2..ec90bb2a4252 100644 --- a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-contexts.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-contexts.tsx @@ -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' @@ -59,13 +59,10 @@ const EmptyResolve = (importOrigin: string, toImport: string): Either({ - key: 'UtopiaProjectCtxAtom', - defaultValue: { - projectContents: {}, - openStoryboardFilePathKILLME: null, - resolve: EmptyResolve, - }, +export const UtopiaProjectCtxAtom = React.createContext({ + projectContents: {}, + openStoryboardFilePathKILLME: null, + resolve: EmptyResolve, }) interface SceneLevelContextProps { diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx index 28c442e395c1..c032c612ae66 100644 --- a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-execution-scope.tsx @@ -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 { diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-selective-context-provider.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-selective-context-provider.tsx new file mode 100644 index 000000000000..bcbc3bef7ef1 --- /dev/null +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-selective-context-provider.tsx @@ -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(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 ( + + + {props.children} + + + ) +} +PerformanceOptimizedCanvasContextProvider.displayName = 'PerformanceOptimizedCanvasContextProvider' + +const CanvasContextProviderInner: React.FunctionComponent> = ( + 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 ( + + {props.children} + + ) +} +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 +} diff --git a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-top-level-elements.tsx b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-top-level-elements.tsx index 7673aa0caa4d..4c0b19f5ca56 100644 --- a/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-top-level-elements.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-top-level-elements.tsx @@ -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' @@ -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 { diff --git a/editor/src/components/canvas/ui-jsx-canvas.tsx b/editor/src/components/canvas/ui-jsx-canvas.tsx index 92bf28f445e1..35093274d4b3 100644 --- a/editor/src/components/canvas/ui-jsx-canvas.tsx +++ b/editor/src/components/canvas/ui-jsx-canvas.tsx @@ -318,9 +318,10 @@ export const UiJsxCanvas = React.memo((props) let evaluatedFileNames = React.useRef>([]) // 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) { diff --git a/editor/src/templates/editor-entry-point-imports.tsx b/editor/src/templates/editor-entry-point-imports.tsx index 928b3f60e1a0..2d4dd08e5e6e 100644 --- a/editor/src/templates/editor-entry-point-imports.tsx +++ b/editor/src/templates/editor-entry-point-imports.tsx @@ -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'