From 64aab1a6dfac5db72e725ac6ed88e7fd6824ee01 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Fri, 14 Nov 2025 20:45:43 +0100 Subject: [PATCH] grealy improved memoization --- src/App/App.tsx | 27 +++-- src/App/Plant/DigitalWatch/DigitalWatch.tsx | 4 +- src/App/SideBar/ShowAST.tsx | 14 ++- src/App/SideBar/SideBar.tsx | 30 +++--- src/App/hooks/useSimulator.ts | 82 +++++++------- src/hooks/useCustomMemo.ts | 19 ++++ src/statecharts/detect_connections.ts | 113 +++++++++++++++++--- src/statecharts/parser.ts | 47 ++------ 8 files changed, 217 insertions(+), 119 deletions(-) create mode 100644 src/hooks/useCustomMemo.ts diff --git a/src/App/App.tsx b/src/App/App.tsx index 9855a60..727e22c 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -3,7 +3,7 @@ import "./App.css"; import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { detectConnections } from "@/statecharts/detect_connections"; +import { connectionsEqual, detectConnections, reducedConcreteSyntaxEqual } from "@/statecharts/detect_connections"; import { parseStatechart } from "../statecharts/parser"; import { BottomPanel } from "./BottomPanel/BottomPanel"; import { defaultSideBarState, SideBar, SideBarState } from "./SideBar/SideBar"; @@ -18,6 +18,7 @@ import { plants } from "./plants"; import { emptyState } from "@/statecharts/concrete_syntax"; import { ModalOverlay } from "./Overlays/ModalOverlay"; import { FindReplace } from "./BottomPanel/FindReplace"; +import { useCustomMemo } from "@/hooks/useCustomMemo"; export type EditHistory = { current: VisualEditorState, @@ -59,7 +60,17 @@ export function App() { // parse concrete syntax always: const conns = useMemo(() => editorState && detectConnections(editorState), [editorState]); - const parsed = useMemo(() => editorState && conns && parseStatechart(editorState, conns), [editorState, conns]); + const parsed = useCustomMemo(() => editorState && conns && parseStatechart(editorState, conns), + [editorState, conns] as const, + // only parse again if anything changed to the connectedness / insideness... + // parsing is fast, BUT re-rendering everything that depends on the AST is slow, and it's difficult to check if the AST changed because AST objects have recursive structure. + ([prevState, prevConns], [nextState, nextConns]) => { + if ((prevState === null) !== (nextState === null)) return false; + if ((prevConns === null) !== (nextConns === null)) return false; + // the following check is much cheaper than re-rendering everything that depends on + return connectionsEqual(prevConns!, nextConns!) + && reducedConcreteSyntaxEqual(prevState!, nextState!); + }); const ast = parsed && parsed[0]; const [appState, setAppState] = useState(defaultAppState); @@ -118,14 +129,6 @@ export function App() { const simulator = useSimulator(ast, plant, plantConns, scrollDownSidebar); - // console.log('render app', {ast, plant, appState}); - // useDetectChange(ast, 'ast'); - // useDetectChange(plant, 'plant'); - // useDetectChange(scrollDownSidebar, 'scrollDownSidebar'); - // useDetectChange(appState, 'appState'); - // useDetectChange(simulator.time, 'simulator.time'); - // useDetectChange(simulator.trace, 'simulator.trace'); - const setters = makeAllSetters(setAppState, Object.keys(appState) as (keyof AppState)[]); const syntaxErrors = parsed && parsed[1] || []; @@ -141,7 +144,9 @@ export function App() { const highlightActive = (currentBigStep && currentBigStep.state.sc.mode) || new Set(); const highlightTransitions = currentBigStep && currentBigStep.state.sc.firedTransitions || []; - const plantState = currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1]; + const plantState = useMemo(() => + currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1], + [currentBigStep, plant]); return
; -}, objectsEqual); +}, jsonDeepEqual); export const digitalWatchPlant = makeStatechartPlant({ ast: dwatchAbstractSyntax, diff --git a/src/App/SideBar/ShowAST.tsx b/src/App/SideBar/ShowAST.tsx index 37e7cb9..0520b84 100644 --- a/src/App/SideBar/ShowAST.tsx +++ b/src/App/SideBar/ShowAST.tsx @@ -5,6 +5,7 @@ import { ConcreteState, stateDescription, Transition, UnstableState } from "../. import { Action, EventTrigger, Expression } from "../../statecharts/label_ast"; import { KeyInfoHidden, KeyInfoVisible } from "../TopPanel/KeyInfo"; import { useShortcuts } from '@/hooks/useShortcuts'; +import { arraysEqual, jsonDeepEqual } from '@/util/util'; export function ShowTransition(props: {transition: Transition}) { return <>➝ {stateDescription(props.transition.tgt)}; @@ -50,7 +51,7 @@ export const ShowAST = memo(function ShowASTx(props: {root: ConcreteState | Unst }); -export function ShowInputEvents({inputEvents, onRaise, disabled}: {inputEvents: EventTrigger[], onRaise: (e: string, p: any) => void, disabled: boolean}) { +export const ShowInputEvents = memo(function ShowInputEvents({inputEvents, onRaise, disabled}: {inputEvents: EventTrigger[], onRaise: (e: string, p: any) => void, disabled: boolean}) { const raiseHandlers = inputEvents.map(({event}) => { return () => { // @ts-ignore @@ -105,17 +106,22 @@ export function ShowInputEvents({inputEvents, onRaise, disabled}: {inputEvents:  
; }) -} +}, (prevProps, nextProps) => { + console.log('onRaise changed:', prevProps.onRaise === nextProps.onRaise, prevProps.onRaise, nextProps.onRaise); + return prevProps.onRaise === nextProps.onRaise + && prevProps.disabled === nextProps.disabled + && jsonDeepEqual(prevProps.inputEvents, nextProps.inputEvents); +}); export function ShowInternalEvents(props: {internalEvents: EventTrigger[]}) { return [...props.internalEvents].map(({event, paramName}) => { - return <>
{event}{paramName===undefined?<>:<>({paramName})}
; + return
{event}{paramName===undefined?<>:<>({paramName})}
; }); } export function ShowOutputEvents(props: {outputEvents: Set}) { return [...props.outputEvents].map(eventName => { - return <>
{eventName}
; + return
{eventName}
; }); } diff --git a/src/App/SideBar/SideBar.tsx b/src/App/SideBar/SideBar.tsx index 488bd38..a6e6048 100644 --- a/src/App/SideBar/SideBar.tsx +++ b/src/App/SideBar/SideBar.tsx @@ -5,7 +5,7 @@ import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined'; import VisibilityIcon from '@mui/icons-material/Visibility'; import { Conns } from '@/statecharts/timed_reactive'; -import { Dispatch, Ref, SetStateAction, useEffect, useRef, useState } from 'react'; +import { Dispatch, memo, Ref, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; import { Statechart } from '@/statecharts/abstract_syntax'; import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from './ShowAST'; import { Plant } from '../Plant/Plant'; @@ -17,6 +17,7 @@ import { plants, UniversalPlantState } from '../plants'; import { TimeMode } from '@/statecharts/time'; import { PersistentDetails } from '../Components/PersistentDetails'; import "./SideBar.css"; +import { objectsEqual } from '@/util/util'; type SavedTraces = [string, BigStepCause[]][]; @@ -76,20 +77,20 @@ type SideBarProps = SideBarState & { time: TimeMode, } & Setters; -export function SideBar({showExecutionTrace, showConnections, plantName, showPlantTrace, showProperties, activeProperty, autoConnect, autoScroll, plantConns, properties, savedTraces, refRightSideBar, ast, plant, setSavedTraces, trace, setTrace, setProperties, setShowPlantTrace, setActiveProperty, setPlantConns, setPlantName, setAutoConnect, setShowProperties, setAutoScroll, time, plantState, onReplayTrace, onRaise, setTime, setShowConnections, setShowExecutionTrace, showPlant, setShowPlant, showOutputEvents, setShowOutputEvents, setShowInternalEvents, showInternalEvents, setShowInputEvents, setShowStateTree, showInputEvents, showStateTree}: SideBarProps) { +export const SideBar = memo(function SideBar({showExecutionTrace, showConnections, plantName, showPlantTrace, showProperties, activeProperty, autoConnect, autoScroll, plantConns, properties, savedTraces, refRightSideBar, ast, plant, setSavedTraces, trace, setTrace, setProperties, setShowPlantTrace, setActiveProperty, setPlantConns, setPlantName, setAutoConnect, setShowProperties, setAutoScroll, time, plantState, onReplayTrace, onRaise, setTime, setShowConnections, setShowExecutionTrace, showPlant, setShowPlant, showOutputEvents, setShowOutputEvents, setShowInternalEvents, showInternalEvents, setShowInputEvents, setShowStateTree, showInputEvents, showStateTree}: SideBarProps) { const [propertyResults, setPropertyResults] = useState(null); const speed = time.kind === "paused" ? 0 : time.scale; - const onSaveTrace = () => { + const onSaveTrace = useCallback(() => { if (trace) { setSavedTraces(savedTraces => [ ...savedTraces, ["untitled", trace.trace.map((item) => item.cause)] as [string, BigStepCause[]], ]); } - } + }, [trace, setSavedTraces]); // if some properties change, re-evaluate them: useEffect(() => { @@ -115,6 +116,9 @@ export function SideBar({showExecutionTrace, showConnections, plantName, showPla } }, [ast, plant, autoConnect]); + const raiseDebugEvent = useCallback((e,p) => onRaise("debug."+e,p), [onRaise]); + const raiseUIEvent = useCallback(e => onRaise("plant.ui."+e.name, e.param), [onRaise]); + return <>
input events {ast && onRaise("debug."+e,p)} + onRaise={raiseDebugEvent} disabled={trace===null || trace.trace[trace.idx].kind === "error"} />} @@ -153,14 +157,14 @@ export function SideBar({showExecutionTrace, showConnections, plantName, showPla disabled={trace!==null} value={plantName} onChange={e => setPlantName(() => e.target.value)}> - {plants.map(([plantName, p]) => - + {plants.map(([plantName]) => + )}
{/* Render plant */} { onRaise("plant.ui."+e.name, e.param)} + raiseUIEvent={raiseUIEvent} />} {/* Connections */} @@ -251,7 +255,9 @@ export function SideBar({showExecutionTrace, showConnections, plantName, showPla
} ; -} +}, (prevProps, nextProps) => { + return objectsEqual(prevProps, nextProps); +}); function autoDetectConns(ast: Statechart, plant: Plant, setPlantConns: Dispatch>) { for (const {event: a} of plant.uiEvents) { @@ -289,7 +295,7 @@ function ConnEditor(ast: Statechart, plant: Plant, plantConns: Conns, return <> {/* SC output events can go to Plant */} - {[...ast.outputEvents].map(e =>
+ {[...ast.outputEvents].map(e =>
, plantConns: Conns,
)]} {/* Plant UI events typically go to the Plant */} - {plant.uiEvents.map(e =>
+ {plant.uiEvents.map(e =>