Skip to content

feat(workflow): resolve canvas re-rendering and logs perf issues#3844

Open
adithyaakrishna wants to merge 6 commits intosimstudioai:stagingfrom
adithyaakrishna:feat/workflow
Open

feat(workflow): resolve canvas re-rendering and logs perf issues#3844
adithyaakrishna wants to merge 6 commits intosimstudioai:stagingfrom
adithyaakrishna:feat/workflow

Conversation

@adithyaakrishna
Copy link
Copy Markdown
Contributor

Summary

Fixes severe UI lag and re-rendering issues on the workflow page during parallel loop execution. The canvas, output panel, input tab, and terminal logs all suffered from cascading re-renders when many iterations ran simultaneously.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: Perf optimization + code reorg

Testing

  • Run a workflow with a Parallel containing a Loop of 100+ iterations
  • Verify the canvas remains responsive during execution
  • Verify the Output panel does not flicker between iteration results
  • Verify terminal logs auto-scroll to the bottom as new iterations complete
  • Verify scrolling up in logs stays in place while new entries arrive
  • Verify fast scrolling in logs does not show white flash
  • Verify sandbox mode still works (isSandbox prop propagation)

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@cursor
Copy link
Copy Markdown

cursor bot commented Mar 30, 2026

PR Summary

Medium Risk
Moderate risk due to substantial refactors in workflow canvas interaction logic and terminal virtualization/scroll behavior, which could introduce subtle UX regressions despite being largely performance-focused.

Overview
Addresses workflow-page performance issues by reducing cascading React re-renders across the terminal logs, output panel, and workflow canvas during high-volume executions (e.g., nested parallel/loop runs).

Terminal/output changes: StructuredOutput now avoids resetting expanded state when data identity changes but JSON content is unchanged (prevents flicker), the output header toolbar is extracted into a memoized component, terminal log virtualization is reworked to use an external “signal store” (useSyncExternalStore) plus higher overscan, stable row data refs, and auto-scroll only when near bottom.

Workflow canvas changes: introduces new hooks (useBlockOperations, useCanvasKeyboard, useAutoConnectEdge, useNodeDerivation, useLockNotifications) and shared constants (workflow-constants.ts) to centralize canvas behaviors (drop/add/paste/duplicate, auto-connect edges, selection syncing, lock notifications) while trimming block props/state plumbing (remove isActive/isPending props; derive pending/run status from execution store selectors).

Written by Cursor Bugbot for commit 0ec73b0. This will update automatically on new commits. Configure here.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 30, 2026 3:46pm

Request Review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 30, 2026

Greptile Summary

This PR addresses severe UI lag and cascading re-renders on the workflow canvas during parallel loop execution through three complementary strategies: (1) store-level granularity — replacing the full useLastRunPath map subscription with per-block useBlockRunStatus and useIsBlockPending selectors; (2) node data isolation — removing isActive/isPending from ReactFlow node data and derivedNodes deps; and (3) terminal rendering overhaul — a useSyncExternalStore-backed signal store to decouple virtual-list rows from React's render cycle.

The large workflow.tsx is decomposed into focused hooks (useNodeDerivation, useBlockOperations, useCanvasKeyboard, useLockNotifications, useAutoConnectEdge) and a workflow-constants.ts module. ReactFlow nodeTypes/edgeTypes are now module-level constants. Drag tracking state is converted from useState to useRef.

Key findings:

  • The blocksStructureHash optimisation always returns '' on first render, and having blocks in derivedNodes deps alongside the hash means the hash does not actually prevent recomputation when the blocks reference changes.
  • VirtualEntryNodeRow reads onSelectEntry/onToggleNode directly from the signal store; these callbacks don't trigger subscriber notifications, so stale closures could be used if callbacks become non-stable in the future. Safe today.

Confidence Score: 5/5

Safe to merge — all remaining findings are P2 quality/optimisation notes that do not affect correctness or produce observable regressions.

The core performance objectives are implemented correctly. Both flagged issues are pre-existing behaviour or safe-in-practice design choices; neither blocks merge.

use-node-derivation.ts (blocksStructureHash dep optimisation); terminal.tsx (VirtualEntryNodeRow callback subscription)

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-derivation.ts New hook extracting node derivation from workflow.tsx; hash optimisation is partially ineffective because blocks remains in derivedNodes dep array alongside blocksStructureHash, and the hash returns '' on first render.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx Substantial rewrite using useSyncExternalStore + signal store; auto-scroll and auto-select logic corrected; stale-callback risk in VirtualEntryNodeRow is low in practice but structurally present.
apps/sim/stores/execution/store.ts Adds useIsBlockPending and useBlockRunStatus for per-block subscriptions, correctly replacing the full-map useLastRunPath subscriber.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx ~900 lines extracted to dedicated hooks; drag-state converted from useState to useRef to prevent re-renders; remaining logic is clean.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow-constants.ts Extracts ReactFlow nodeTypes, edgeTypes and view constants to module level, ensuring stable object references ReactFlow requires for memoisation.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts Replaces full lastRunPath map subscription with per-block useBlockRunStatus; drops isPending prop in favour of useIsBlockPending store selector.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/output-panel.tsx Extracts toolbar into memoised OutputPanelToolbar sub-component, moving outputOptionsOpen state out of the main panel.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-operations.ts Large extraction of block CRUD, copy/paste, context-menu operations from workflow.tsx; clean separation with no observable logic changes.

Sequence Diagram

sequenceDiagram
    participant ES as ExecutionStore
    participant UIBP as useIsBlockPending(blockId)
    participant UBRS as useBlockRunStatus(blockId)
    participant BV as useBlockVisual
    participant WB as WorkflowBlock (memo)
    participant SS as SignalStore (useSyncExternalStore)
    participant VER as VirtualEntryNodeRow (memo)

    Note over ES: Block becomes active during parallel loop
    ES->>UIBP: activeBlockIds updated
    UIBP->>WB: re-render only this block
    ES->>UBRS: lastRunPath updated for blockId
    UBRS->>BV: runPathStatus changed
    BV->>WB: ring styles updated

    Note over ES: New console entry arrives
    ES->>SS: signalStore.update(selectedId, expandedNodes, ...)
    SS->>VER: useSyncExternalStore notifies (if selectedId/expandedNodes changed)
    VER->>VER: re-render only affected rows

    Note over ES: Other blocks unchanged
    ES--xWB: NO re-render (per-block selector)
    ES--xVER: NO re-render (unchanged selectedId/expandedNodes)
Loading

Reviews (2): Last reviewed commit: "chore: fix review changes" | Re-trigger Greptile

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 30, 2026

@adithyaakrishna is attempting to deploy a commit to the Sim Team on Vercel.

A member of the Team first needs to authorize it.

@adithyaakrishna
Copy link
Copy Markdown
Contributor Author

@greptile review

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

if (prevDataJsonRef.current !== newJson) {
prevDataJsonRef.current = newJson
setExpandedPaths(computeInitialPaths(data, isError))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unguarded JSON.stringify can throw on non-serializable data

Medium Severity

The new deep-equality check calls JSON.stringify(data) without a try-catch. If data contains circular references, BigInt values, or other non-JSON-serializable structures, this will throw an unhandled error and crash the structured output component. The previous code only compared by reference (prevDataRef.current !== data), which can never throw.

Additional Locations (1)
Fix in Cursor Fix in Web

dataRef.current = { rows }

const signalStoreRef = useRef(createRowSignalStore())
signalStoreRef.current.update(selectedEntryId, expandedNodes, onSelectEntry, onToggleNode)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signal store mutated during React render phase

Low Severity

signalStoreRef.current.update() is called during the render of TerminalLogsPane, not inside a useEffect. When the update detects changes, it synchronously fires all useSyncExternalStore listeners. This is a side effect during render, violating React's requirement that rendering be pure. It can cause issues in React Strict Mode (double invocation) and is fragile in concurrent rendering scenarios.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant