Skip to content

perf(graphical-editor): remove edit hot-path deep clones + debounce flow write-back (DOPE-490) - #948

Merged
JoaoGSP merged 4 commits into
developmentfrom
fix/dope-490-edit-hot-path-clones
Jul 21, 2026
Merged

perf(graphical-editor): remove edit hot-path deep clones + debounce flow write-back (DOPE-490)#948
JoaoGSP merged 4 commits into
developmentfrom
fix/dope-490-edit-hot-path-clones

Conversation

@JoaoGSP

@JoaoGSP JoaoGSP commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary

DOPE-490 — Perf 4/5 of the DOPE-446 editor-performance series.

Every graphical edit deep-cloned the ENTIRE flow twice: a 5× JSON round-trip undo snapshot plus an undebounced structuredClone write-back into project.data.pous (live measurement: heap sawtooth 181↔389 MB, ~150–200 MB transient garbage per edit burst).

  • New store/slices/shared/flow-writeback.ts — per-POU debounced (200 ms) flow → pou.body.value write-back. Persists the raw flow by reference: the store is immer-managed (frozen, copy-on-write), so the project copy and the live flow safely share structure. Zod validation + the DOPE-477 raw-object policy live here now.
  • Flush-on-saveexecuteSaveProject/executeSaveFile flush pending write-backs before serializing, so a save landing inside the debounce window persists the fresh body (key acceptance criterion). Compile is covered via its auto-save.
  • Reference snapshotsuse-pou-snapshot.ts and snapshotActions.undo/redo no longer JSON-clone; they flush the affected POU first so a history entry can never pair a stale body with a fresh flow. Project open cancels stale timers.
  • Bug fix along the way — the ladder editor's local flow-less captureSnapshot (from 📝 Add docstrings to DOPE-21-FEAT-highlist-st #277) meant undo of rung add/reorder never restored the canvas; it now uses the shared captureAndPush (includes flows).

Validation

  • 12 new unit tests (flow-writeback.test.ts); full jest suite passes (5216 tests).
  • Live byte-stability gate (web + Electron): save-without-edit → zero diff; element edit + save inside the debounce window → fresh body persisted, only that POU diffs; undo/redo exact across rung add / element add / rung reorder with zero-diff saves after undo.
  • Heap (web build, shared code): 38-rung duplication burst ~1.1 MB/edit; element adds at 40 rungs ~5 MB/edit peak — ≪ 20 MB criterion, sawtooth gone.

Mirror of https://github.com/Autonomy-Logic/openplc-web/pull/610

🤖 Generated with Claude Code

https://claude.ai/code/session_017RGT8nUsyY26HXFSuTBFLz

Summary by CodeRabbit

  • Bug Fixes
    • Improved persistence of Ladder and FBD editor changes with debounced write-back to reduce delays and prevent lost edits.
    • Ensured project/file saves and undo/redo snapshots always include the latest graphical flow updates.
    • Prevented pending graphical edits from carrying over when opening a different project, and avoided persistence while the debugger UI is visible.
    • Improved handling of rapid consecutive edits by coalescing changes into a single update.
  • Tests
    • Added coverage for deferred, flushed, canceled, schema-validated, and debugger-specific flow persistence scenarios.

JoaoGSP and others added 2 commits July 21, 2026 15:16
…low write-back (DOPE-490)

Every graphical edit deep-cloned the whole flow twice (JSON round-trip
undo snapshot + structuredClone write-back), producing 150-200MB of
transient garbage per edit burst on large projects.

- new store/slices/shared/flow-writeback.ts: per-POU debounced (200ms)
  flow -> pou.body write-back; persists the raw flow by reference
  (immer copy-on-write makes sharing safe); flush/cancel entry points
- save paths flush pending write-backs before serializing, so a save
  landing inside the debounce window persists the fresh body
- undo/redo + snapshot capture hold plain references (no JSON clones)
  and flush the POU first so history never pairs a stale body with a
  fresh flow; project open cancels stale timers
- ladder editor's local flow-less captureSnapshot replaced with the
  shared hook: undo of rung add/reorder now restores the canvas

Mirror of the openplc-web PR for DOPE-490.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RGT8nUsyY26HXFSuTBFLz
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1fb914c4-3379-493f-9afd-112eda11d76d

📥 Commits

Reviewing files that changed from the base of the PR and between 5580cb9 and 5ebab22.

📒 Files selected for processing (1)
  • src/frontend/store/slices/shared/flow-writeback.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/frontend/store/slices/shared/flow-writeback.ts

Walkthrough

Graphical Ladder and FBD edits now use centralized debounced write-back scheduling. Saves, snapshots, undo/redo, and project loading coordinate with pending flow writes to keep persisted and historical state synchronized.

Changes

Graphical flow persistence and history

Layer / File(s) Summary
Centralized flow write-back scheduler
src/frontend/store/slices/shared/flow-writeback.ts, src/frontend/store/__tests__/flow-writeback.test.ts
Adds debounced, validated Ladder/FBD persistence with flush, cancellation, coalescing, transient-flag removal, and scheduler coverage.
Ladder and FBD editor integration
src/frontend/components/_features/[workspace]/editor/graphical/{FBD,ladder}/index.tsx
Editors schedule flow writes centrally; Ladder rung creation and reordering capture history through captureAndPush.
Snapshot and undo/redo synchronization
src/frontend/hooks/use-pou-snapshot.ts, src/frontend/store/slices/shared/slice.ts
Snapshots and undo/redo flush pending writes and store reference-based POU state; project loading cancels pending timers.
Save-time write-back synchronization
src/frontend/services/save-actions.ts
Project and single-file saves flush pending flow writes before serialization.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LadderOrFBDEditor
  participant FlowWriteBackScheduler
  participant OpenPLCStore
  participant SaveOrHistoryAction
  LadderOrFBDEditor->>FlowWriteBackScheduler: schedule updated flow
  FlowWriteBackScheduler->>OpenPLCStore: persist validated flow body after debounce
  SaveOrHistoryAction->>FlowWriteBackScheduler: flush pending writes
  FlowWriteBackScheduler->>OpenPLCStore: apply pending write-back
  SaveOrHistoryAction->>OpenPLCStore: read synchronized state
Loading

Possibly related PRs

Poem

I’m a rabbit who queues every flow,
Then waits just a moment before writes can go.
Rungs hop to history, FBDs settle bright,
Saves flush the burrows before taking flight.
Undo and redo find state neat—
A scheduler’s carrot, timed just right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: removing hot-path deep clones and adding debounced flow write-back.
Description check ✅ Passed The description covers the key changes and validation, though it omits most of the template's DOD checklist details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dope-490-edit-hot-path-clones

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RGT8nUsyY26HXFSuTBFLz

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/frontend/components/_features/[workspace]/editor/graphical/FBD/index.tsx (1)

82-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use openPLCStoreBase.getState for deferred/out-of-React state access, per store guidelines.

All three sites pass useOpenPLCStore.getState into scheduler functions (scheduleFlowWriteBack/flushFlowWriteBacks) whose callbacks run later outside the React render/hook lifecycle (inside a setTimeout). The store guideline for this repo calls for openPLCStoreBase.getState() for direct state access outside React; save-actions.ts already follows this correctly for the same kind of deferred access. Verified that auto-zustand-selectors-hook's createSelectorHooks returns the same underlying store object (zustand's create already copies getState onto the bound hook), so useOpenPLCStore.getState and openPLCStoreBase.getState are the same function today — this is a consistency/guideline-compliance issue rather than a live bug, but worth aligning so the two access patterns don't silently diverge if the store wrapper ever changes.

  • src/frontend/components/_features/[workspace]/editor/graphical/FBD/index.tsx#L82-L91: pass openPLCStoreBase.getState to scheduleFlowWriteBack instead of useOpenPLCStore.getState.
  • src/frontend/components/_features/[workspace]/editor/graphical/ladder/index.tsx#L138-L147: same change for its scheduleFlowWriteBack call.
  • src/frontend/hooks/use-pou-snapshot.ts#L24-L42: same change for the flushFlowWriteBacks call.

As per coding guidelines, "direct state access outside React should use openPLCStoreBase.getState()."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/components/_features/`[workspace]/editor/graphical/FBD/index.tsx
around lines 82 - 91, Replace usePLCStore.getState with
openPLCStoreBase.getState when calling scheduleFlowWriteBack in
src/frontend/components/_features/[workspace]/editor/graphical/FBD/index.tsx
lines 82-91 and
src/frontend/components/_features/[workspace]/editor/graphical/ladder/index.tsx
lines 138-147. Make the same replacement for the flushFlowWriteBacks call in
src/frontend/hooks/use-pou-snapshot.ts lines 24-42, preserving the existing
deferred write-back behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/store/slices/shared/flow-writeback.ts`:
- Around line 48-55: The flow write-back path around schema.safeParse, including
flushFlowWriteBacks, must propagate validation failures instead of returning
silently. Return an explicit failure result when validation fails, have
flushFlowWriteBacks abort or surface that failure to the save flow, and only
update the POU and clear updated flags after all flows validate and write back
successfully.

---

Nitpick comments:
In
`@src/frontend/components/_features/`[workspace]/editor/graphical/FBD/index.tsx:
- Around line 82-91: Replace usePLCStore.getState with openPLCStoreBase.getState
when calling scheduleFlowWriteBack in
src/frontend/components/_features/[workspace]/editor/graphical/FBD/index.tsx
lines 82-91 and
src/frontend/components/_features/[workspace]/editor/graphical/ladder/index.tsx
lines 138-147. Make the same replacement for the flushFlowWriteBacks call in
src/frontend/hooks/use-pou-snapshot.ts lines 24-42, preserving the existing
deferred write-back behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 27090057-6111-4a3e-801e-172b26da8592

📥 Commits

Reviewing files that changed from the base of the PR and between c577fa7 and 5580cb9.

📒 Files selected for processing (7)
  • src/frontend/components/_features/[workspace]/editor/graphical/FBD/index.tsx
  • src/frontend/components/_features/[workspace]/editor/graphical/ladder/index.tsx
  • src/frontend/hooks/use-pou-snapshot.ts
  • src/frontend/services/save-actions.ts
  • src/frontend/store/__tests__/flow-writeback.test.ts
  • src/frontend/store/slices/shared/flow-writeback.ts
  • src/frontend/store/slices/shared/slice.ts

Comment on lines +48 to +55
const schema = language === 'ld' ? zodLadderFlowSchema : zodFBDFlowSchema
if (!schema.safeParse(flow).success) return

const { updated: _updated, ...flowBody } = flow
state.projectActions.updatePou({ name: pouName, content: { language, value: flowBody } })

const flowActions = language === 'ld' ? state.ladderFlowActions : state.fbdFlowActions
flowActions.setFlowUpdated({ editorName: pouName, updated: false })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Propagate validation failures so save cannot discard graphical edits.

When validation fails, this returns without updating the POU, but flushFlowWriteBacks reports no failure. Save then serializes the stale body and clears every flow’s updated flag after success, silently losing the graphical edit. Return a failure result from write-back/flush and abort or surface the save until the flow is valid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/store/slices/shared/flow-writeback.ts` around lines 48 - 55, The
flow write-back path around schema.safeParse, including flushFlowWriteBacks,
must propagate validation failures instead of returning silently. Return an
explicit failure result when validation fails, have flushFlowWriteBacks abort or
surface that failure to the save flow, and only update the POU and clear updated
flags after all flows validate and write back successfully.

@JoaoGSP
JoaoGSP merged commit 417158d into development Jul 21, 2026
12 checks passed
@JoaoGSP
JoaoGSP deleted the fix/dope-490-edit-hot-path-clones branch July 21, 2026 19:01
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