Skip to content

Commit

Permalink
Performance improvements for large graphs
Browse files Browse the repository at this point in the history
  • Loading branch information
abrenneke committed May 8, 2024
1 parent f2f289c commit 36e2d0b
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 84 deletions.
6 changes: 6 additions & 0 deletions packages/app/src/components/DraggableNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { type ProcessDataForNode } from '../state/dataFlow';
import { type DraggingWireDef } from '../state/graphBuilder';

interface DraggableNodeProps {
renderSkeleton?: boolean;
heightCache: HeightCache;
node: ChartNode;
connections?: NodeConnection[];
Expand All @@ -32,6 +33,7 @@ interface DraggableNodeProps {
processPage: number | 'latest';
draggingWire?: DraggingWireDef;
isZoomedOut: boolean;
isReallyZoomedOut: boolean;
isPinned: boolean;
onWireEndDrag?: (event: MouseEvent<HTMLElement>, endNodeId: NodeId, endPortId: PortId) => void;
onNodeSelected?: (node: ChartNode, multi: boolean) => void;
Expand Down Expand Up @@ -66,7 +68,9 @@ export const DraggableNode: FC<DraggableNodeProps> = ({
processPage,
draggingWire,
isZoomedOut,
isReallyZoomedOut,
isPinned,
renderSkeleton,
onWireStartDrag,
onWireEndDrag,
onNodeSelected,
Expand Down Expand Up @@ -97,7 +101,9 @@ export const DraggableNode: FC<DraggableNodeProps> = ({
processPage={processPage}
draggingWire={draggingWire}
isZoomedOut={isZoomedOut}
isReallyZoomedOut={isReallyZoomedOut}
isPinned={isPinned}
renderSkeleton={renderSkeleton}
onWireEndDrag={onWireEndDrag}
onWireStartDrag={onWireStartDrag}
onSelectNode={useStableCallback((multi: boolean) => {
Expand Down
101 changes: 81 additions & 20 deletions packages/app/src/components/NodeCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useNodeHeightCache } from '../hooks/useNodeBodyHeight';
import { DraggableNode } from './DraggableNode.js';
import { css } from '@emotion/react';
import { nodeStyles } from './nodeStyles.js';
import { type FC, useMemo, useRef, useState, type MouseEvent, useEffect } from 'react';
import { type FC, useMemo, useRef, useState, type MouseEvent, useEffect, useLayoutEffect } from 'react';
import { ContextMenu, type ContextMenuContext } from './ContextMenu.js';
import { CSSTransition } from 'react-transition-group';
import { WireLayer } from './WireLayer.js';
Expand Down Expand Up @@ -35,7 +35,7 @@ import {
import { useCanvasPositioning } from '../hooks/useCanvasPositioning.js';
import { VisualNode } from './VisualNode.js';
import { useStableCallback } from '../hooks/useStableCallback.js';
import { useThrottleFn } from 'ahooks';
import { useThrottle, useThrottleFn } from 'ahooks';
import { produce } from 'immer';
import { graphMetadataState } from '../state/graph.js';
import { useViewportBounds } from '../hooks/useViewportBounds.js';
Expand Down Expand Up @@ -157,6 +157,8 @@ export interface NodeCanvasProps {

export type PortPositions = Record<string, { x: number; y: number }>;

const shouldShowNodeBasedOnPosition = new WeakMap<ChartNode, boolean>();

export const NodeCanvas: FC<NodeCanvasProps> = ({
nodes,
connections,
Expand Down Expand Up @@ -209,7 +211,13 @@ export const NodeCanvas: FC<NodeCanvasProps> = ({
setContextMenuData,
} = useContextMenu();

const { nodePortPositions, canvasRef, recalculate: recalculatePortPositions } = useNodePortPositions();
const shouldRenderWires = canvasPosition.zoom > 0.15;

const {
nodePortPositions,
canvasRef,
recalculate: recalculatePortPositions,
} = useNodePortPositions({ enabled: shouldRenderWires });

useEffect(() => {
recalculatePortPositions();
Expand Down Expand Up @@ -577,6 +585,61 @@ export const NodeCanvas: FC<NodeCanvasProps> = ({
const selectedProcessPagePerNode = useRecoilValue(selectedProcessPageNodesState);

const isZoomedOut = canvasPosition.zoom < 0.4;
const isReallyZoomedOut = canvasPosition.zoom < 0.2;

const [forceRender, setForceRender] = useState(0);
const movingRerenderTimeout = useRef<number | undefined>();
const lastNodeRecalculateTime = useRef(0);

const isLargeGraph = nodes.length > 100;
const debounceTime = isLargeGraph ? 500 : 50;

useLayoutEffect(() => {
const recalculateVisibleNodes = () => {
let numVisible = 0;

for (const node of nodes) {
const isPinned = pinnedNodes.includes(node.id);

const shouldHide =
(node.visualData.x < viewportBounds.left - (node.visualData.width ?? 300) ||
node.visualData.x > viewportBounds.right + (node.visualData.width ?? 300) ||
node.visualData.y < viewportBounds.top - 500 ||
node.visualData.y > viewportBounds.bottom + 500) &&
!isPinned;

// if (numVisible > 300) {
// shouldHide = true;
// }

shouldShowNodeBasedOnPosition.set(node, !shouldHide);

if (!shouldHide) {
numVisible++;
}
}

setForceRender((prev) => prev + 1);
lastNodeRecalculateTime.current = Date.now();
};

if (movingRerenderTimeout.current) {
window.clearTimeout(movingRerenderTimeout.current);
}

movingRerenderTimeout.current = window.setTimeout(() => {
recalculateVisibleNodes();
}, debounceTime);
}, [
pinnedNodes,
nodes,
viewportBounds.left,
viewportBounds.right,
viewportBounds.top,
viewportBounds.bottom,
forceRender,
debounceTime,
]);

return (
<DndContext onDragStart={onNodeStartDrag} onDragEnd={onNodeDragged}>
Expand All @@ -602,25 +665,19 @@ export const NodeCanvas: FC<NodeCanvasProps> = ({
className="canvas-contents"
style={{
transform: `scale(${canvasPosition.zoom}, ${canvasPosition.zoom}) translate(${canvasPosition.x}px, ${canvasPosition.y}px) translateZ(-1px)`,
willChange: 'transform',
}}
>
<div className="nodes">
{nodesWithConnections.map(({ node, nodeConnections }) => {
const isPinned = pinnedNodes.includes(node.id);

if (
(node.visualData.x < viewportBounds.left - (node.visualData.width ?? 300) ||
node.visualData.x > viewportBounds.right + (node.visualData.width ?? 300) ||
node.visualData.y < viewportBounds.top - 500 ||
node.visualData.y > viewportBounds.bottom + 500) &&
!isPinned
) {
if (!shouldShowNodeBasedOnPosition.get(node)) {
return null;
}

if (draggingNodes.some((n) => n.id === node.id)) {
return null;
}

return (
<DraggableNode
key={node.id}
Expand All @@ -633,6 +690,7 @@ export const NodeCanvas: FC<NodeCanvasProps> = ({
lastRun={lastRunPerNode[node.id]}
isPinned={pinnedNodes.includes(node.id)}
isZoomedOut={isZoomedOut && node.type !== 'comment'}
isReallyZoomedOut={isReallyZoomedOut && node.type !== 'comment'}
processPage={selectedProcessPagePerNode[node.id]!}
draggingWire={draggingWire}
onWireStartDrag={onWireStartDrag}
Expand Down Expand Up @@ -676,6 +734,7 @@ export const NodeCanvas: FC<NodeCanvasProps> = ({
isKnownNodeType={node.type in nodeTypes}
isPinned={pinnedNodes.includes(node.id)}
isZoomedOut={isZoomedOut && node.type !== 'comment'}
isReallyZoomedOut={isReallyZoomedOut && node.type !== 'comment'}
processPage={selectedProcessPagePerNode[node.id]!}
/>
))}
Expand Down Expand Up @@ -715,14 +774,16 @@ export const NodeCanvas: FC<NodeCanvasProps> = ({
/>
)}

<WireLayer
connections={connections}
draggingWire={draggingWire}
highlightedNodes={highlightedNodes}
highlightedPort={hoveringPort}
portPositions={nodePortPositions}
draggingNode={draggingNodes.length > 0}
/>
{shouldRenderWires && (
<WireLayer
connections={connections}
draggingWire={draggingWire}
highlightedNodes={highlightedNodes}
highlightedPort={hoveringPort}
portPositions={nodePortPositions}
draggingNode={draggingNodes.length > 0}
/>
)}
{hoveringPort && hoveringShowPortInfo && (
<PortInfo floatingStyles={floatingStyles} ref={refs.setFloating} port={hoveringPort} />
)}
Expand Down
104 changes: 57 additions & 47 deletions packages/app/src/components/VisualNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,14 @@ export type VisualNodeProps = {
isDragging?: boolean;
isOverlay?: boolean;
isSelected?: boolean;
scale?: number;
isKnownNodeType: boolean;
isPinned: boolean;
lastRun?: ProcessDataForNode[];
processPage: number | 'latest';
draggingWire?: DraggingWireDef;
isZoomedOut: boolean;
isReallyZoomedOut: boolean;
renderSkeleton?: boolean;
onWireStartDrag?: (
event: MouseEvent<HTMLElement>,
startNodeId: NodeId,
Expand Down Expand Up @@ -106,13 +107,14 @@ export const VisualNode = memo(
yDelta = 0,
isDragging,
isOverlay,
scale,
isSelected,
isKnownNodeType,
isPinned,
lastRun,
processPage,
isZoomedOut,
isReallyZoomedOut,
renderSkeleton,
onWireEndDrag,
onWireStartDrag,
onSelectNode,
Expand Down Expand Up @@ -143,7 +145,7 @@ export const VisualNode = memo(

const style: CSSProperties = {
opacity: isDragging ? '0' : '',
transform: `translate(${node.visualData.x + xDelta}px, ${node.visualData.y + yDelta}px) scale(${scale ?? 1})`,
transform: `translate(${node.visualData.x + xDelta}px, ${node.visualData.y + yDelta}px) scale(${1})`,
zIndex: isComment ? -10000 : node.visualData.zIndex ?? 0,
width: node.visualData.width,
height: isComment ? asCommentNode.data.height : undefined,
Expand All @@ -162,12 +164,15 @@ export const VisualNode = memo(
yDelta,
node.visualData.width,
isDragging,
scale,
node.visualData.zIndex,
isComment,
asCommentNode.data.height,
]);

if (renderSkeleton) {
return <div className="node-skeleton" style={style} {...nodeAttributes} />;
}

const nodeRef = (refValue: HTMLDivElement | null) => {
if (typeof ref === 'function') {
ref(refValue);
Expand All @@ -176,8 +181,6 @@ export const VisualNode = memo(
}
};

// const isZoomedOut = !isComment && zoom < 0.4;

const selectedProcessRun =
lastRun && lastRun.length > 0
? lastRun?.at(processPage === 'latest' ? lastRun.length - 1 : processPage)?.data
Expand Down Expand Up @@ -222,6 +225,7 @@ export const VisualNode = memo(
isKnownNodeType={isKnownNodeType}
lastRun={lastRun}
processPage={processPage}
isReallyZoomedOut={isReallyZoomedOut}
/>
) : (
<NormalVisualNodeContent
Expand Down Expand Up @@ -255,6 +259,7 @@ const ZoomedOutVisualNodeContent: FC<{
isKnownNodeType: boolean;
lastRun?: ProcessDataForNode[];
processPage: number | 'latest';
isReallyZoomedOut: boolean;
onSelectNode?: (multi: boolean) => void;
onStartEditing?: () => void;
onWireStartDrag?: (event: MouseEvent<HTMLElement>, startNodeId: NodeId, startPortId: PortId) => void;
Expand All @@ -281,6 +286,7 @@ const ZoomedOutVisualNodeContent: FC<{
isKnownNodeType,
lastRun,
processPage,
isReallyZoomedOut,
onSelectNode,
onStartEditing,
onWireStartDrag,
Expand Down Expand Up @@ -316,48 +322,52 @@ const ZoomedOutVisualNodeContent: FC<{
return (
<>
<div className="node-title">
<div className="grab-area" {...handleAttributes} onClick={handleGrabClick}>
{node.isSplitRun ? <GitForkLine /> : <></>}
<div className="title-text">{node.title}</div>
</div>
<div className="title-controls">
<div className="last-run-status">
{selectedProcessRun?.status ? (
match(selectedProcessRun.status)
.with({ type: 'ok' }, () => (
<div className="success">
<SendIcon />
</div>
))
.with({ type: 'error' }, () => (
<div className="error">
<SendIcon />
</div>
))
.with({ type: 'running' }, () => (
<div className="running">
<LoadingSpinner />
</div>
))
.with({ type: 'interrupted' }, () => (
<div className="interrupted">
<SendIcon />
</div>
))
.with({ type: 'notRan' }, () => (
<div className="not-ran">
<SendIcon />
</div>
))
.exhaustive()
) : (
<></>
)}
{!isReallyZoomedOut && (
<div className="grab-area" {...handleAttributes} onClick={handleGrabClick}>
{node.isSplitRun ? <GitForkLine /> : <></>}
<div className="title-text">{node.title}</div>
</div>
<button className="edit-button" onClick={handleEditClick} onMouseDown={handleEditMouseDown} title="Edit">
<SettingsCogIcon />
</button>
</div>
)}
{!isReallyZoomedOut && (
<div className="title-controls">
<div className="last-run-status">
{selectedProcessRun?.status ? (
match(selectedProcessRun.status)
.with({ type: 'ok' }, () => (
<div className="success">
<SendIcon />
</div>
))
.with({ type: 'error' }, () => (
<div className="error">
<SendIcon />
</div>
))
.with({ type: 'running' }, () => (
<div className="running">
<LoadingSpinner />
</div>
))
.with({ type: 'interrupted' }, () => (
<div className="interrupted">
<SendIcon />
</div>
))
.with({ type: 'notRan' }, () => (
<div className="not-ran">
<SendIcon />
</div>
))
.exhaustive()
) : (
<></>
)}
</div>
<button className="edit-button" onClick={handleEditClick} onMouseDown={handleEditMouseDown} title="Edit">
<SettingsCogIcon />
</button>
</div>
)}
</div>

{isKnownNodeType && (
Expand Down
Loading

0 comments on commit 36e2d0b

Please sign in to comment.