Skip to content

fix: pass the DOM height convention explicitly, and retire ambient LayoutSource - #14657

Closed
DrJKL wants to merge 2 commits into
feature/ecs-migrationfrom
drjkl/layout-source-at-instantiation
Closed

fix: pass the DOM height convention explicitly, and retire ambient LayoutSource#14657
DrJKL wants to merge 2 commits into
feature/ecs-migrationfrom
drjkl/layout-source-at-instantiation

Conversation

@DrJKL

@DrJKL DrJKL commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Stacked on #14480. Six commits: one bug fix, five removals. +355/−1113.

The layout store carried a process-global currentSource, written by one subsystem and read by another for a purpose neither named. This PR fixes the bug that came out of it, then deletes the mechanism and everything that turned out to be holding onto it.

The bug

batchUpdateNodeBounds read the ambient LayoutSource as an out-of-band flag meaning "these bounds were measured off the DOM and include the title height", overwrote the source to Vue for the operation it emitted, then restored the original on exit. The only producer of LayoutSource.DOM was the shared ResizeObserver, which set it and never cleared it.

  1. The shared ResizeObserver sets DOM and leaves it set.
  2. startDrag sets Vue once, at pointerdown.
  3. endDrag calls batchUpdateNodeBounds at pointerup with nothing setting a source in between.
  4. The heights it passes are read back out of the store, so they are already title-less.
  5. shouldNormalizeHeights is true, and the title height comes off a second time.

Snap-on-drop shrinks the snapped nodes by NODE_TITLE_HEIGHT. The window is narrower than "any observer during any drag" — a Canvas-sourced write clears DOM — so it needs the observer to fire with nothing else attributing after it. Invisible at every call site individually; it exists only in the gap between them.

The regression test was written against the old code first and fails on it: expected 170 to be 200.

Changes

  • batchUpdateNodeBounds(rootGraphId, updates, { source, boundsIncludeTitleHeight }) — the measurement convention travels with the call. Splits the two jobs ambient source was doing: who made the change, and what convention the caller measured in.
  • useLayoutMutations(source) — required, not optional. An optional parameter reintroduces the ambient default through another door. An audit of all thirteen setSource call sites found zero that varied source per call: eleven fixed for the caller's lifetime, one fake-varying, one not attribution at all.
  • The store's current source is gonecurrentSource, setSource, getCurrentSource, getCurrentActor, setActor, ACTOR_CONFIG.DEFAULT_SOURCE. Operations may omit actor; the store stamps its own session actor, so no caller needs to know it.
  • LayoutSource is Canvas | Vue. DOM died with the explicit flag. External was never emitted by anything — it existed only as the store's initial value, which made LGraphNode.vue's guard branch reachable only before the first setSource in the process.
  • clearGraph deleted — zero production callers. LGraph.clear sweeps layout per-entity through unregisterAllGraphLayout; the prefix-wipe had been dead since. Takes ClearGraphOperation and its dispatch case with it.
  • The node customRef's write path deleted — zero production writers across twenty consumers, all of which only read. It was building four operations by hand from a diff of old and new layout. getNodeLayoutRef returns a ComputedRef now.
  • useLayoutSync deleted — orphaned when GraphCanvas.vue moved to notifyLayoutChanges, and kept off knip's radar by its own test file. 354 lines.
  • useNodeZIndex's layoutSource option and per-call source override deleted — no production caller passed either, and the tests covering them asserted the mock.

Review focus

  • getNodeLayoutRef changes primitive, customRefcomputed. customRef's contract requires a set, so making the ref read-only meant changing it. This is the one part of the removal that is not semantically inert: reads are now cached between store notifications, where the old ref re-read Yjs on every access. Safe because ynodes.observeDeep fires the node's trigger for every mutation path including remote applyUpdate — the same trigger the old ref used — but it deserves a look.
  • actor is now optional on LayoutOperation. No production caller passes one. The only submitters are fix(layout): enforce per-instance geometry ownership #14480's tests asserting the operation's actor becomes the Yjs transaction origin, so the store fills it with ??= rather than overwriting, which preserves that contract.
  • canvasLayoutMutations() became a module const. It read like an accessor and mutated a singleton. Import-time evaluation is inert — useLayoutMutations only builds closures.
  • Two docs corrected. docs/adr/0003 and docs/architecture/node-data-store.md both described a root-graph teardown calling layoutStore.clearGraph(graphId). That was already false before this PR.

Emitted source is deliberately unchanged

batchUpdateNodeBounds still stamps every operation Vue regardless of caller, so paste and arrange keep reporting Vue even though they mean Canvas. Correcting that turns on LGraphNode.vue's CSS-var rewrite for those paths — a visible behaviour change that wants its own review, so it is not in this PR. It got considerably cheaper here: the microtask-vs-rAF scheduling half of that risk lived in useLayoutSync, which this PR deletes.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 506ba690-7d86-471b-8725-ebb28d7137ec

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

🎭 Playwright: ✅ 1811 passed, 0 failed · 3 flaky

📊 Browser Reports
  • chromium: View Report (✅ 1790 / ❌ 0 / ⚠️ 3 / ⏭️ 5)
  • chromium-2x: View Report (✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • chromium-0.5x: View Report (✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • mobile-chrome: View Report (✅ 18 / ❌ 0 / ⚠️ 0 / ⏭️ 0)

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 08/13/2026, 06:46:54 PM UTC

Links

📦 Bundle Size

⏳ Size data collection in progress…

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 45.4 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 59.6 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 50.3 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 63.5 MB heap
large-graph-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 62.5 MB heap
large-graph-pan: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 1ms TBT · 61.8 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 60.3 MB heap
legacy-node-drag: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 56.3 MB heap
minimap-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 63.7 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 61.4 MB heap
subgraph-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 72.6 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 53.9 MB heap
subgraph-transition-enter: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 130ms TBT · 84.0 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 64.9 MB heap
vue-large-graph-idle: · 55.4 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 158.0 MB heap
vue-large-graph-pan: · 54.6 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 410ms TBT · 155.4 MB heap
workflow-execution: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 49.9 MB heap

ℹ️ No baseline found — significance unavailable.

Absolute values
Metric Value
canvas-idle: avg frame time 17ms
canvas-idle: p95 frame time 17ms
canvas-idle: layout duration 0ms
canvas-idle: style recalc duration 6ms
canvas-idle: layout count 0
canvas-idle: style recalc count 7
canvas-idle: task duration 540ms
canvas-idle: script duration 26ms
canvas-idle: TBT 0ms
canvas-idle: heap used 45.4 MB
canvas-idle: DOM nodes -283
canvas-idle: event listeners -157
canvas-mouse-sweep: avg frame time 17ms
canvas-mouse-sweep: p95 frame time 17ms
canvas-mouse-sweep: layout duration 3ms
canvas-mouse-sweep: style recalc duration 39ms
canvas-mouse-sweep: layout count 12
canvas-mouse-sweep: style recalc count 79
canvas-mouse-sweep: task duration 1191ms
canvas-mouse-sweep: script duration 165ms
canvas-mouse-sweep: TBT 0ms
canvas-mouse-sweep: heap used 59.6 MB
canvas-mouse-sweep: DOM nodes -283
canvas-mouse-sweep: event listeners -171
canvas-zoom-sweep: avg frame time 17ms
canvas-zoom-sweep: p95 frame time 17ms
canvas-zoom-sweep: layout duration 1ms
canvas-zoom-sweep: style recalc duration 17ms
canvas-zoom-sweep: layout count 6
canvas-zoom-sweep: style recalc count 32
canvas-zoom-sweep: task duration 392ms
canvas-zoom-sweep: script duration 30ms
canvas-zoom-sweep: TBT 0ms
canvas-zoom-sweep: heap used 50.3 MB
canvas-zoom-sweep: DOM nodes 78
canvas-zoom-sweep: event listeners 19
dom-widget-clipping: avg frame time 17ms
dom-widget-clipping: p95 frame time 17ms
dom-widget-clipping: layout duration 0ms
dom-widget-clipping: style recalc duration 6ms
dom-widget-clipping: layout count 0
dom-widget-clipping: style recalc count 10
dom-widget-clipping: task duration 424ms
dom-widget-clipping: script duration 36ms
dom-widget-clipping: TBT 0ms
dom-widget-clipping: heap used 63.5 MB
dom-widget-clipping: DOM nodes 16
dom-widget-clipping: event listeners 2
large-graph-idle: avg frame time 17ms
large-graph-idle: p95 frame time 17ms
large-graph-idle: layout duration 0ms
large-graph-idle: style recalc duration 4ms
large-graph-idle: layout count 0
large-graph-idle: style recalc count 5
large-graph-idle: task duration 802ms
large-graph-idle: script duration 308ms
large-graph-idle: TBT 0ms
large-graph-idle: heap used 62.5 MB
large-graph-idle: DOM nodes -284
large-graph-idle: event listeners -168
large-graph-pan: avg frame time 17ms
large-graph-pan: p95 frame time 17ms
large-graph-pan: layout duration 0ms
large-graph-pan: style recalc duration 11ms
large-graph-pan: layout count 0
large-graph-pan: style recalc count 67
large-graph-pan: task duration 2900ms
large-graph-pan: script duration 1357ms
large-graph-pan: TBT 1ms
large-graph-pan: heap used 61.8 MB
large-graph-pan: DOM nodes -283
large-graph-pan: event listeners -151
large-graph-zoom: avg frame time 17ms
large-graph-zoom: p95 frame time 17ms
large-graph-zoom: layout duration 7ms
large-graph-zoom: style recalc duration 12ms
large-graph-zoom: layout count 60
large-graph-zoom: style recalc count 68
large-graph-zoom: task duration 3006ms
large-graph-zoom: script duration 1426ms
large-graph-zoom: TBT 0ms
large-graph-zoom: heap used 60.3 MB
large-graph-zoom: DOM nodes -120
large-graph-zoom: event listeners -71
legacy-node-drag: avg frame time 17ms
legacy-node-drag: p95 frame time 17ms
legacy-node-drag: layout duration 0ms
legacy-node-drag: style recalc duration 4ms
legacy-node-drag: layout count 0
legacy-node-drag: style recalc count 42
legacy-node-drag: task duration 2807ms
legacy-node-drag: script duration 754ms
legacy-node-drag: TBT 0ms
legacy-node-drag: heap used 56.3 MB
legacy-node-drag: DOM nodes -289
legacy-node-drag: event listeners -109
minimap-idle: avg frame time 17ms
minimap-idle: p95 frame time 17ms
minimap-idle: layout duration 0ms
minimap-idle: style recalc duration 3ms
minimap-idle: layout count 0
minimap-idle: style recalc count 4
minimap-idle: task duration 787ms
minimap-idle: script duration 296ms
minimap-idle: TBT 0ms
minimap-idle: heap used 63.7 MB
minimap-idle: DOM nodes -286
minimap-idle: event listeners -184
subgraph-dom-widget-clipping: avg frame time 17ms
subgraph-dom-widget-clipping: p95 frame time 17ms
subgraph-dom-widget-clipping: layout duration 0ms
subgraph-dom-widget-clipping: style recalc duration 10ms
subgraph-dom-widget-clipping: layout count 0
subgraph-dom-widget-clipping: style recalc count 47
subgraph-dom-widget-clipping: task duration 537ms
subgraph-dom-widget-clipping: script duration 144ms
subgraph-dom-widget-clipping: TBT 0ms
subgraph-dom-widget-clipping: heap used 61.4 MB
subgraph-dom-widget-clipping: DOM nodes 19
subgraph-dom-widget-clipping: event listeners 8
subgraph-idle: avg frame time 17ms
subgraph-idle: p95 frame time 17ms
subgraph-idle: layout duration 0ms
subgraph-idle: style recalc duration 9ms
subgraph-idle: layout count 0
subgraph-idle: style recalc count 10
subgraph-idle: task duration 458ms
subgraph-idle: script duration 19ms
subgraph-idle: TBT 0ms
subgraph-idle: heap used 72.6 MB
subgraph-idle: DOM nodes 19
subgraph-idle: event listeners 4
subgraph-mouse-sweep: avg frame time 17ms
subgraph-mouse-sweep: p95 frame time 17ms
subgraph-mouse-sweep: layout duration 4ms
subgraph-mouse-sweep: style recalc duration 35ms
subgraph-mouse-sweep: layout count 16
subgraph-mouse-sweep: style recalc count 77
subgraph-mouse-sweep: task duration 1091ms
subgraph-mouse-sweep: script duration 117ms
subgraph-mouse-sweep: TBT 0ms
subgraph-mouse-sweep: heap used 53.9 MB
subgraph-mouse-sweep: DOM nodes -296
subgraph-mouse-sweep: event listeners -203
subgraph-transition-enter: avg frame time 17ms
subgraph-transition-enter: p95 frame time 17ms
subgraph-transition-enter: layout duration 12ms
subgraph-transition-enter: style recalc duration 30ms
subgraph-transition-enter: layout count 15
subgraph-transition-enter: style recalc count 19
subgraph-transition-enter: task duration 913ms
subgraph-transition-enter: script duration 47ms
subgraph-transition-enter: TBT 130ms
subgraph-transition-enter: heap used 84.0 MB
subgraph-transition-enter: DOM nodes 13671
subgraph-transition-enter: event listeners 2383
viewport-pan-sweep: avg frame time 17ms
viewport-pan-sweep: p95 frame time 17ms
viewport-pan-sweep: layout duration 0ms
viewport-pan-sweep: style recalc duration 34ms
viewport-pan-sweep: layout count 0
viewport-pan-sweep: style recalc count 247
viewport-pan-sweep: task duration 10135ms
viewport-pan-sweep: script duration 4711ms
viewport-pan-sweep: TBT 0ms
viewport-pan-sweep: heap used 64.9 MB
viewport-pan-sweep: DOM nodes -280
viewport-pan-sweep: event listeners -154
vue-large-graph-idle: avg frame time 18ms
vue-large-graph-idle: p95 frame time 17ms
vue-large-graph-idle: layout duration 0ms
vue-large-graph-idle: style recalc duration 0ms
vue-large-graph-idle: layout count 0
vue-large-graph-idle: style recalc count 0
vue-large-graph-idle: task duration 17634ms
vue-large-graph-idle: script duration 1220ms
vue-large-graph-idle: TBT 0ms
vue-large-graph-idle: heap used 158.0 MB
vue-large-graph-idle: DOM nodes -6563
vue-large-graph-idle: event listeners -7469
vue-large-graph-pan: avg frame time 18ms
vue-large-graph-pan: p95 frame time 17ms
vue-large-graph-pan: layout duration 0ms
vue-large-graph-pan: style recalc duration 19ms
vue-large-graph-pan: layout count 0
vue-large-graph-pan: style recalc count 153
vue-large-graph-pan: task duration 21673ms
vue-large-graph-pan: script duration 1755ms
vue-large-graph-pan: TBT 410ms
vue-large-graph-pan: heap used 155.4 MB
vue-large-graph-pan: DOM nodes -6548
vue-large-graph-pan: event listeners -7465
workflow-execution: avg frame time 17ms
workflow-execution: p95 frame time 17ms
workflow-execution: layout duration 1ms
workflow-execution: style recalc duration 22ms
workflow-execution: layout count 3
workflow-execution: style recalc count 16
workflow-execution: task duration 137ms
workflow-execution: script duration 10ms
workflow-execution: TBT 0ms
workflow-execution: heap used 49.9 MB
workflow-execution: DOM nodes 125
workflow-execution: event listeners 97
Raw data
{
  "timestamp": "2026-08-13T18:58:25.477Z",
  "gitSha": "b48c06093cc08e353eee5167f98170d489fa1c83",
  "branch": "drjkl/layout-source-at-instantiation",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2062.7079999999864,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 5.727000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 573.8449999999999,
      "heapDeltaBytes": -20854904,
      "heapUsedBytes": 47923204,
      "domNodes": -283,
      "jsHeapTotalBytes": 18845696,
      "scriptDurationMs": 26.262999999999998,
      "eventListeners": -157,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "canvas-idle",
      "durationMs": 2054.0090000000646,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 7.0619999999999985,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 505.6860000000001,
      "heapDeltaBytes": -21344788,
      "heapUsedBytes": 47212928,
      "domNodes": -282,
      "jsHeapTotalBytes": 19107840,
      "scriptDurationMs": 26.061999999999994,
      "eventListeners": -157,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2109.730000000013,
      "styleRecalcs": 78,
      "styleRecalcDurationMs": 38.212999999999994,
      "layouts": 12,
      "layoutDurationMs": 3.0429999999999997,
      "taskDurationMs": 1182.315,
      "heapDeltaBytes": -1018428,
      "heapUsedBytes": 67939604,
      "domNodes": -283,
      "jsHeapTotalBytes": 21073920,
      "scriptDurationMs": 166.125,
      "eventListeners": -155,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2199.6010000000297,
      "styleRecalcs": 80,
      "styleRecalcDurationMs": 40.003,
      "layouts": 12,
      "layoutDurationMs": 3.7399999999999998,
      "taskDurationMs": 1200.376,
      "heapDeltaBytes": -11575964,
      "heapUsedBytes": 57046168,
      "domNodes": -282,
      "jsHeapTotalBytes": 20549632,
      "scriptDurationMs": 164.73399999999998,
      "eventListeners": -187,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1737.1550000000298,
      "styleRecalcs": 31,
      "styleRecalcDurationMs": 17.015,
      "layouts": 6,
      "layoutDurationMs": 0.6769999999999999,
      "taskDurationMs": 384.311,
      "heapDeltaBytes": -15815012,
      "heapUsedBytes": 52985628,
      "domNodes": 77,
      "jsHeapTotalBytes": 21495808,
      "scriptDurationMs": 28.053,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1740.0179999999636,
      "styleRecalcs": 32,
      "styleRecalcDurationMs": 16.692999999999998,
      "layouts": 6,
      "layoutDurationMs": 0.5680000000000001,
      "taskDurationMs": 399.244,
      "heapDeltaBytes": -16075884,
      "heapUsedBytes": 52520208,
      "domNodes": 78,
      "jsHeapTotalBytes": 21233664,
      "scriptDurationMs": 31.170999999999996,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 746.8910000000051,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 6.113,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 431.30500000000006,
      "heapDeltaBytes": -2132128,
      "heapUsedBytes": 66531544,
      "domNodes": 16,
      "jsHeapTotalBytes": 21495808,
      "scriptDurationMs": 35.501999999999995,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 748.4090000000378,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 6.841999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 416.8419999999999,
      "heapDeltaBytes": -2219348,
      "heapUsedBytes": 66534456,
      "domNodes": 16,
      "jsHeapTotalBytes": 21757952,
      "scriptDurationMs": 36.492999999999995,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2063.1889999999657,
      "styleRecalcs": 4,
      "styleRecalcDurationMs": 3.2900000000000014,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 808.8629999999999,
      "heapDeltaBytes": -4609700,
      "heapUsedBytes": 61084060,
      "domNodes": -284,
      "jsHeapTotalBytes": 2195456,
      "scriptDurationMs": 314.336,
      "eventListeners": -183,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2035.9849999999824,
      "styleRecalcs": 5,
      "styleRecalcDurationMs": 4.130000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 795.0500000000001,
      "heapDeltaBytes": 4624324,
      "heapUsedBytes": 69968152,
      "domNodes": -283,
      "jsHeapTotalBytes": 1933312,
      "scriptDurationMs": 301.35900000000004,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 3644.4290000000024,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 10.986,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 2909.226,
      "heapDeltaBytes": -3343520,
      "heapUsedBytes": 60802948,
      "domNodes": -283,
      "jsHeapTotalBytes": 3244032,
      "scriptDurationMs": 1368.6560000000002,
      "eventListeners": -151,
      "totalBlockingTimeMs": 1,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 3686.2539999999626,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 11.601000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 2889.808,
      "heapDeltaBytes": 4335964,
      "heapUsedBytes": 68894448,
      "domNodes": -283,
      "jsHeapTotalBytes": 1863680,
      "scriptDurationMs": 1346.1450000000002,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 5031.67400000001,
      "styleRecalcs": 67,
      "styleRecalcDurationMs": 11.724000000000002,
      "layouts": 60,
      "layoutDurationMs": 6.995000000000001,
      "taskDurationMs": 3046.2949999999996,
      "heapDeltaBytes": -14527932,
      "heapUsedBytes": 58424556,
      "domNodes": -267,
      "jsHeapTotalBytes": 4030464,
      "scriptDurationMs": 1435.237,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 4965.813000000026,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 13.024999999999999,
      "layouts": 60,
      "layoutDurationMs": 7.486,
      "taskDurationMs": 2966.66,
      "heapDeltaBytes": -3682460,
      "heapUsedBytes": 68133832,
      "domNodes": 28,
      "jsHeapTotalBytes": 6815744,
      "scriptDurationMs": 1416.3809999999999,
      "eventListeners": 12,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "legacy-node-drag",
      "durationMs": 4164.250999999978,
      "styleRecalcs": 42,
      "styleRecalcDurationMs": 4.860999999999997,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 2821.8379999999997,
      "heapDeltaBytes": -12682148,
      "heapUsedBytes": 58573976,
      "domNodes": -288,
      "jsHeapTotalBytes": 9273344,
      "scriptDurationMs": 751.795,
      "eventListeners": -110,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "legacy-node-drag",
      "durationMs": 4187.378000000081,
      "styleRecalcs": 41,
      "styleRecalcDurationMs": 4.067999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 2791.857,
      "heapDeltaBytes": -12100720,
      "heapUsedBytes": 59566036,
      "domNodes": -290,
      "jsHeapTotalBytes": 8224768,
      "scriptDurationMs": 757.1999999999999,
      "eventListeners": -108,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "minimap-idle",
      "durationMs": 2026.4339999999947,
      "styleRecalcs": 4,
      "styleRecalcDurationMs": 3.4470000000000005,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 792.3769999999998,
      "heapDeltaBytes": 2252608,
      "heapUsedBytes": 67215316,
      "domNodes": -285,
      "jsHeapTotalBytes": 2719744,
      "scriptDurationMs": 298.139,
      "eventListeners": -183,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "minimap-idle",
      "durationMs": 2010.6129999999212,
      "styleRecalcs": 3,
      "styleRecalcDurationMs": 2.7600000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 782.525,
      "heapDeltaBytes": -178392,
      "heapUsedBytes": 66279864,
      "domNodes": -286,
      "jsHeapTotalBytes": 2457600,
      "scriptDurationMs": 293.195,
      "eventListeners": -185,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 779.8480000000154,
      "styleRecalcs": 47,
      "styleRecalcDurationMs": 10.264,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 562.994,
      "heapDeltaBytes": -4958784,
      "heapUsedBytes": 64319164,
      "domNodes": 20,
      "jsHeapTotalBytes": 22282240,
      "scriptDurationMs": 154.07800000000003,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 696.8339999999671,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 10.194999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 510.66900000000004,
      "heapDeltaBytes": -5021748,
      "heapUsedBytes": 64467392,
      "domNodes": 18,
      "jsHeapTotalBytes": 22020096,
      "scriptDurationMs": 133.483,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2005.5550000000153,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 7.388000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 457.9040000000001,
      "heapDeltaBytes": 6701984,
      "heapUsedBytes": 76072412,
      "domNodes": 18,
      "jsHeapTotalBytes": 20447232,
      "scriptDurationMs": 18.345,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2021.6649999999845,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.924,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 458.186,
      "heapDeltaBytes": 6897332,
      "heapUsedBytes": 76101704,
      "domNodes": 20,
      "jsHeapTotalBytes": 20447232,
      "scriptDurationMs": 18.733,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1855.634000000009,
      "styleRecalcs": 77,
      "styleRecalcDurationMs": 35.296,
      "layouts": 16,
      "layoutDurationMs": 4.215,
      "taskDurationMs": 1086.882,
      "heapDeltaBytes": -12751976,
      "heapUsedBytes": 56750012,
      "domNodes": -296,
      "jsHeapTotalBytes": 19632128,
      "scriptDurationMs": 116.748,
      "eventListeners": -203,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1843.6619999999948,
      "styleRecalcs": 77,
      "styleRecalcDurationMs": 35.611999999999995,
      "layouts": 16,
      "layoutDurationMs": 3.9879999999999995,
      "taskDurationMs": 1094.5,
      "heapDeltaBytes": -12853740,
      "heapUsedBytes": 56323060,
      "domNodes": -296,
      "jsHeapTotalBytes": 19632128,
      "scriptDurationMs": 117.76,
      "eventListeners": -203,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-transition-enter",
      "durationMs": 1367.4099999999498,
      "styleRecalcs": 19,
      "styleRecalcDurationMs": 30.128000000000004,
      "layouts": 15,
      "layoutDurationMs": 11.996,
      "taskDurationMs": 912.9299999999998,
      "heapDeltaBytes": -1487476,
      "heapUsedBytes": 88042072,
      "domNodes": 13671,
      "jsHeapTotalBytes": 10223616,
      "scriptDurationMs": 46.95999999999999,
      "eventListeners": 2383,
      "totalBlockingTimeMs": 130,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 13578.40600000003,
      "styleRecalcs": 247,
      "styleRecalcDurationMs": 34.07,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 10116.137999999999,
      "heapDeltaBytes": 8779040,
      "heapUsedBytes": 72200640,
      "domNodes": -280,
      "jsHeapTotalBytes": 4747264,
      "scriptDurationMs": 4701.871,
      "eventListeners": -155,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 13758.201999999983,
      "styleRecalcs": 247,
      "styleRecalcDurationMs": 33.592,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 10154.128999999999,
      "heapDeltaBytes": -36212,
      "heapUsedBytes": 63938744,
      "domNodes": -279,
      "jsHeapTotalBytes": 4030464,
      "scriptDurationMs": 4720.203,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 17870.762000000013,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 17849.296000000002,
      "heapDeltaBytes": -26813556,
      "heapUsedBytes": 166251176,
      "domNodes": -6548,
      "jsHeapTotalBytes": 5300224,
      "scriptDurationMs": 1213.5900000000001,
      "eventListeners": -7471,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 17436.444999999934,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 17417.88,
      "heapDeltaBytes": -24963356,
      "heapUsedBytes": 165070244,
      "domNodes": -6577,
      "jsHeapTotalBytes": 2678784,
      "scriptDurationMs": 1226.99,
      "eventListeners": -7467,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 18.333333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 21677.874000000033,
      "styleRecalcs": 150,
      "styleRecalcDurationMs": 18.915000000000013,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 21659.370000000003,
      "heapDeltaBytes": -50648512,
      "heapUsedBytes": 156075420,
      "domNodes": -6548,
      "jsHeapTotalBytes": 5824512,
      "scriptDurationMs": 1767.355,
      "eventListeners": -7465,
      "totalBlockingTimeMs": 484,
      "frameDurationMs": 18.333333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 21705.441000000064,
      "styleRecalcs": 155,
      "styleRecalcDurationMs": 18.785000000000053,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 21686.701,
      "heapDeltaBytes": -35138512,
      "heapUsedBytes": 169857784,
      "domNodes": -6548,
      "jsHeapTotalBytes": 3465216,
      "scriptDurationMs": 1743.562,
      "eventListeners": -7465,
      "totalBlockingTimeMs": 335,
      "frameDurationMs": 18.330000000000048,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "workflow-execution",
      "durationMs": 469.05500000002576,
      "styleRecalcs": 17,
      "styleRecalcDurationMs": 21.079,
      "layouts": 3,
      "layoutDurationMs": 0.694,
      "taskDurationMs": 141.25799999999998,
      "heapDeltaBytes": -14026256,
      "heapUsedBytes": 52303404,
      "domNodes": 126,
      "jsHeapTotalBytes": 10223616,
      "scriptDurationMs": 10.697,
      "eventListeners": 97,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "workflow-execution",
      "durationMs": 463.40199999997367,
      "styleRecalcs": 15,
      "styleRecalcDurationMs": 23.494999999999997,
      "layouts": 3,
      "layoutDurationMs": 0.67,
      "taskDurationMs": 131.774,
      "heapDeltaBytes": -14118272,
      "heapUsedBytes": 52387840,
      "domNodes": 123,
      "jsHeapTotalBytes": 9699328,
      "scriptDurationMs": 10.123,
      "eventListeners": 97,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.799999999999272
    }
  ]
}

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.83673% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/renderer/core/layout/store/layoutStore.ts 93.54% 2 Missing ⚠️
src/renderer/core/canvas/canvasStore.ts 0.00% 1 Missing ⚠️
...erer/extensions/vueNodes/components/LGraphNode.vue 50.00% 1 Missing ⚠️
@@                       Coverage Diff                        @@
##             drjkl/split-layout-attachment   #14657   +/-   ##
================================================================
  Coverage                                 ?   81.09%           
================================================================
  Files                                    ?     1898           
  Lines                                    ?   109884           
  Branches                                 ?    34676           
================================================================
  Hits                                     ?    89110           
  Misses                                   ?    20383           
  Partials                                 ?      391           
Flag Coverage Δ
unit 72.94% <95.74%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/composables/graph/useArrangeNodes.ts 88.23% <100.00%> (ø)
src/lib/litegraph/src/LGraphCanvas.ts 64.98% <ø> (ø)
src/renderer/core/layout/constants.ts 100.00% <ø> (ø)
...er/core/layout/operations/graphLayoutAttachment.ts 98.43% <100.00%> (ø)
...renderer/core/layout/operations/layoutMutations.ts 91.42% <100.00%> (ø)
...enderer/core/layout/store/__mocks__/layoutStore.ts 26.31% <ø> (ø)
src/renderer/core/layout/types.ts 100.00% <100.00%> (ø)
...r/extensions/vueNodes/composables/useNodeZIndex.ts 100.00% <100.00%> (ø)
...s/vueNodes/composables/useVueNodeResizeTracking.ts 88.00% <100.00%> (ø)
...renderer/extensions/vueNodes/layout/useNodeDrag.ts 91.37% <100.00%> (ø)
... and 4 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@DrJKL DrJKL added the refactor label Aug 4, 2026
@DrJKL
DrJKL changed the base branch from drjkl/layout-registration-at-attach to drjkl/layout-crdt-safety August 4, 2026 17:17
@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch from ef7dae4 to 0c4bb12 Compare August 4, 2026 17:20
@DrJKL
DrJKL force-pushed the drjkl/layout-source-at-instantiation branch from 4c47c5e to acfe008 Compare August 4, 2026 17:20
@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch from 0c4bb12 to 65ffdc3 Compare August 4, 2026 18:14
@DrJKL
DrJKL force-pushed the drjkl/layout-source-at-instantiation branch from acfe008 to 51aba3c Compare August 4, 2026 18:14
@DrJKL DrJKL changed the title fix: pass the DOM height convention explicitly, not through ambient LayoutSource fix: pass the DOM height convention explicitly, and retire ambient LayoutSource Aug 4, 2026
@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch from 65ffdc3 to b7ca00a Compare August 5, 2026 04:05
@DrJKL
DrJKL force-pushed the drjkl/layout-source-at-instantiation branch from c852026 to 8f44f95 Compare August 5, 2026 04:05
@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch from c95b8eb to 65f0318 Compare August 5, 2026 20:56
@DrJKL
DrJKL force-pushed the drjkl/layout-source-at-instantiation branch from 8f44f95 to a639061 Compare August 5, 2026 20:56
@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch 2 times, most recently from 329456c to b481407 Compare August 6, 2026 22:29
@DrJKL
DrJKL force-pushed the drjkl/layout-source-at-instantiation branch from a639061 to 5ed6da7 Compare August 6, 2026 22:55
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

🔧 Auto-fixes Applied

This PR has been automatically updated to fix linting and formatting issues.

⚠️ Important: Your local branch is now behind. Run git pull before making additional changes to avoid conflicts.

Changes made:

  • ESLint auto-fixes
  • Oxfmt formatting

@DrJKL
DrJKL force-pushed the drjkl/layout-crdt-safety branch from b481407 to 086008e Compare August 6, 2026 23:39
@DrJKL
DrJKL force-pushed the drjkl/layout-source-at-instantiation branch from 9c0a0b1 to c66c1ba Compare August 6, 2026 23:40
@DrJKL
DrJKL changed the base branch from drjkl/layout-crdt-safety to drjkl/split-layout-attachment August 11, 2026 00:26
@DrJKL
DrJKL force-pushed the drjkl/layout-source-at-instantiation branch from c66c1ba to b8d374d Compare August 11, 2026 00:49
@christian-byrne

Copy link
Copy Markdown
Contributor

Heads up on getNodeLayoutRef while you have it open here.

The rewrite in this PR (customRefComputedRef over a private revision ref) changes the mechanism but keeps the lifetime: nodeTriggers is still Map<key, () => void> and cleanupNodeRef still deletes it, so when one holder unmounts, any other holder of the same cached ref keeps tracking a revision the store has forgotten and silently stops updating. That is #14385.

I checked it rather than inferred it — porting the regression test to your stack tip reproduces it identically to main (expected 100 to be 500).

Two things worth knowing if you fix it inline:

  • The Map<key, Set<() => void>> the issue suggests does not work. The ref is cached per key, so one key has exactly one ref and one trigger; the Set holds a single element and cleanup still drops it. The defect is lifetime, not fan-out.
  • Counting inside getNodeLayoutRef does not work either — the transient .value call sites never release, so the count never returns to zero. It needs an explicit retain/release.

I put the port up as #15093, based on this branch so the diff is one commit. Fold it into your rewrite and close mine if that is cheaper — I mainly wanted it tracked somewhere it cannot get lost between the stack and main. main version: #15092

@DrJKL
DrJKL force-pushed the drjkl/split-layout-attachment branch 2 times, most recently from b3850f1 to 9f63eac Compare August 12, 2026 21:51
@DrJKL
DrJKL force-pushed the drjkl/layout-source-at-instantiation branch from b8d374d to 9c1b0ba Compare August 12, 2026 21:52
@DrJKL
DrJKL force-pushed the drjkl/split-layout-attachment branch from 9f63eac to 827386f Compare August 13, 2026 01:15
@DrJKL
DrJKL force-pushed the drjkl/layout-source-at-instantiation branch from 9c1b0ba to 201a2a7 Compare August 13, 2026 01:15
@DrJKL
DrJKL force-pushed the drjkl/split-layout-attachment branch from 827386f to 66f9af2 Compare August 13, 2026 02:36
ampagent and others added 2 commits August 13, 2026 18:39
Retargeted squash of the layout-source stack onto the pruned attachment
rework: useLayoutMutations(source) bakes the source when instantiated,
operations carry their source explicitly, the store stamps the session
actor on operations that do not carry one, and the ambient
setSource/setActor/getCurrentSource/getCurrentActor knobs are removed.
DOM height convention is passed explicitly through batchUpdateNodeBounds
options instead of sniffed from an ambient DOM source.
@DrJKL
DrJKL force-pushed the drjkl/split-layout-attachment branch from 66f9af2 to a43634e Compare August 13, 2026 18:44
@DrJKL
DrJKL force-pushed the drjkl/layout-source-at-instantiation branch from 201a2a7 to 458b00c Compare August 13, 2026 18:45
Base automatically changed from drjkl/split-layout-attachment to feature/ecs-migration August 14, 2026 02:06
@DrJKL

DrJKL commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #15017, which includes the explicit source attribution and DOM height convention fix along with the finalized attachment architecture. No unique changes remain in this PR.

@DrJKL DrJKL closed this Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants