fix: prevent subgraph node position corruption during graph transitions#10828
fix: prevent subgraph node position corruption during graph transitions#10828
Conversation
ResizeObserver was converting DOM positions back to canvas coordinates using clientPosToCanvasPos, which depends on the current canvas scale/offset. During graph transitions (e.g. entering a subgraph from a draft-loaded workflow), the canvas viewport was stale (still had the parent graph's zoom level), causing all subgraph node positions to be permanently corrupted. The corruption accumulated across page refreshes as drafts saved the corrupted positions. Use the layout store's existing position (initialized from LiteGraph) instead of reverse-converting DOM screen coordinates. Only fall back to getBoundingClientRect conversion for nodes not yet in the layout store. Fixes subgraph node positions drifting apart or compressing together when entering a subgraph after draft workflow reload.
…t reload Verifies that entering a subgraph, reloading the page (draft auto-loads), and auto-entering the subgraph via hash navigation does not corrupt internal node positions.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds a Playwright spec that verifies subgraph internal node coordinates persist across draft-driven reloads; changes Vue node resize-tracking to prefer stored layout positions for x/y and updates its tests; replaces canvas click calls with explicit pointer events; adds a helper to wait for draft persistence. Changes
Sequence Diagram(s)(omitted) Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
🎨 Storybook: ✅ Built — View Storybook |
🎭 Playwright: ✅ 912 passed, 0 failed · 3 flaky📊 Browser Reports
|
📦 Bundle: 5.11 MB gzip 🟢 -95 BDetailsSummary
Category Glance App Entry Points — 22.3 kB (baseline 22.3 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.2 MB (baseline 1.2 MB) • 🔴 +112 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 76.6 kB (baseline 76.6 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed / 2 unchanged Panels & Settings — 484 kB (baseline 484 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 12 unchanged User & Accounts — 17.1 kB (baseline 17.1 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 5 added / 5 removed / 2 unchanged Editors & Dialogs — 109 kB (baseline 109 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 2 added / 2 removed UI Components — 60.3 kB (baseline 60.3 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 2.97 MB (baseline 2.97 MB) • ⚪ 0 BStores, services, APIs, and repositories
Status: 13 added / 13 removed / 4 unchanged Utilities & Hooks — 338 kB (baseline 338 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 13 added / 13 removed / 13 unchanged Vendor & Third-Party — 9.8 MB (baseline 9.8 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 8.44 MB (baseline 8.44 MB) • ⚪ 0 BBundles that do not match a named category
Status: 55 added / 55 removed / 79 unchanged ⚡ Performance Report
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-04-02T20:53:59.148Z",
"gitSha": "97e6d7a590992e80c52fa7a4dffb7d6b05d934dd",
"branch": "fix/subgraph-node-position-corruption",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2007.1170000000222,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.108000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 395.76900000000006,
"heapDeltaBytes": -4420720,
"heapUsedBytes": 43215800,
"domNodes": 22,
"jsHeapTotalBytes": 25165824,
"scriptDurationMs": 20.357,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-idle",
"durationMs": 2034.7220000000448,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.549,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 342.638,
"heapDeltaBytes": 20662844,
"heapUsedBytes": 63868364,
"domNodes": 18,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 16.181999999999995,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 1998.9520000000311,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.408000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 360.96999999999997,
"heapDeltaBytes": 20148464,
"heapUsedBytes": 63117628,
"domNodes": 22,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 19.668999999999993,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1793.4590000000128,
"styleRecalcs": 75,
"styleRecalcDurationMs": 39.641,
"layouts": 12,
"layoutDurationMs": 3.639,
"taskDurationMs": 760.241,
"heapDeltaBytes": 16019228,
"heapUsedBytes": 58764536,
"domNodes": 59,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 137.04,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2016.2900000000263,
"styleRecalcs": 85,
"styleRecalcDurationMs": 47.34,
"layouts": 12,
"layoutDurationMs": 3.7799999999999994,
"taskDurationMs": 982.8090000000001,
"heapDeltaBytes": 15911652,
"heapUsedBytes": 58953992,
"domNodes": 67,
"jsHeapTotalBytes": 23592960,
"scriptDurationMs": 135.91,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2017.898999999943,
"styleRecalcs": 84,
"styleRecalcDurationMs": 44.498999999999995,
"layouts": 12,
"layoutDurationMs": 3.417,
"taskDurationMs": 968.215,
"heapDeltaBytes": 16342236,
"heapUsedBytes": 58819912,
"domNodes": 67,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 137.05100000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1731.4830000000256,
"styleRecalcs": 32,
"styleRecalcDurationMs": 18.254999999999995,
"layouts": 6,
"layoutDurationMs": 0.773,
"taskDurationMs": 296.71100000000007,
"heapDeltaBytes": 24676680,
"heapUsedBytes": 67472748,
"domNodes": 78,
"jsHeapTotalBytes": 20971520,
"scriptDurationMs": 25.115999999999993,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1758.3889999999656,
"styleRecalcs": 33,
"styleRecalcDurationMs": 19.047,
"layouts": 6,
"layoutDurationMs": 0.6040000000000002,
"taskDurationMs": 320.065,
"heapDeltaBytes": 15432116,
"heapUsedBytes": 67117448,
"domNodes": 81,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 25.273000000000003,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1722.5960000000669,
"styleRecalcs": 30,
"styleRecalcDurationMs": 18.648999999999997,
"layouts": 6,
"layoutDurationMs": 0.783,
"taskDurationMs": 307.893,
"heapDeltaBytes": 24699628,
"heapUsedBytes": 67487272,
"domNodes": 79,
"jsHeapTotalBytes": 20447232,
"scriptDurationMs": 26.038999999999998,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 542.412999999982,
"styleRecalcs": 13,
"styleRecalcDurationMs": 10.004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 338.824,
"heapDeltaBytes": 6246748,
"heapUsedBytes": 49009124,
"domNodes": 22,
"jsHeapTotalBytes": 13893632,
"scriptDurationMs": 69.133,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 538.9700000000062,
"styleRecalcs": 13,
"styleRecalcDurationMs": 8.752999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 339.886,
"heapDeltaBytes": 6428732,
"heapUsedBytes": 49149324,
"domNodes": 21,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 66.44900000000001,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.669999999999998,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "dom-widget-clipping",
"durationMs": 561.6679999999405,
"styleRecalcs": 13,
"styleRecalcDurationMs": 9.862,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 354.59,
"heapDeltaBytes": 6172352,
"heapUsedBytes": 49000568,
"domNodes": 22,
"jsHeapTotalBytes": 13631488,
"scriptDurationMs": 70.15599999999999,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "large-graph-idle",
"durationMs": 2046.96899999999,
"styleRecalcs": 11,
"styleRecalcDurationMs": 12.072,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 586.6210000000001,
"heapDeltaBytes": 4964184,
"heapUsedBytes": 55929080,
"domNodes": -254,
"jsHeapTotalBytes": 16183296,
"scriptDurationMs": 109.16899999999998,
"eventListeners": -121,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-idle",
"durationMs": 2057.643999999982,
"styleRecalcs": 11,
"styleRecalcDurationMs": 11.333,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 543.498,
"heapDeltaBytes": 5218980,
"heapUsedBytes": 56264024,
"domNodes": -259,
"jsHeapTotalBytes": 15921152,
"scriptDurationMs": 102.08300000000001,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2033.2689999999047,
"styleRecalcs": 11,
"styleRecalcDurationMs": 11.501999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 543.1729999999999,
"heapDeltaBytes": 17720492,
"heapUsedBytes": 69694488,
"domNodes": -255,
"jsHeapTotalBytes": 14405632,
"scriptDurationMs": 97.621,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-pan",
"durationMs": 2185.9329999999773,
"styleRecalcs": 69,
"styleRecalcDurationMs": 16.65,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1092.678,
"heapDeltaBytes": 17475700,
"heapUsedBytes": 71313748,
"domNodes": -263,
"jsHeapTotalBytes": 18747392,
"scriptDurationMs": 401.49,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2129.542000000015,
"styleRecalcs": 70,
"styleRecalcDurationMs": 17.634000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1087.8609999999999,
"heapDeltaBytes": 19280496,
"heapUsedBytes": 71239420,
"domNodes": -258,
"jsHeapTotalBytes": 18485248,
"scriptDurationMs": 404.58299999999997,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2195.363000000043,
"styleRecalcs": 71,
"styleRecalcDurationMs": 18.788,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1213.6670000000001,
"heapDeltaBytes": 20719104,
"heapUsedBytes": 72580268,
"domNodes": -257,
"jsHeapTotalBytes": 19009536,
"scriptDurationMs": 467.697,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3145.339999999976,
"styleRecalcs": 66,
"styleRecalcDurationMs": 16.503999999999998,
"layouts": 60,
"layoutDurationMs": 7.454000000000001,
"taskDurationMs": 1318.711,
"heapDeltaBytes": -4401228,
"heapUsedBytes": 59684100,
"domNodes": -263,
"jsHeapTotalBytes": 17346560,
"scriptDurationMs": 487.713,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3211.3800000000197,
"styleRecalcs": 66,
"styleRecalcDurationMs": 17.768000000000004,
"layouts": 60,
"layoutDurationMs": 7.645,
"taskDurationMs": 1405.152,
"heapDeltaBytes": 9511724,
"heapUsedBytes": 64521792,
"domNodes": -262,
"jsHeapTotalBytes": 18280448,
"scriptDurationMs": 563.308,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-zoom",
"durationMs": 3198.7589999999955,
"styleRecalcs": 66,
"styleRecalcDurationMs": 17.631999999999998,
"layouts": 60,
"layoutDurationMs": 7.543,
"taskDurationMs": 1337.153,
"heapDeltaBytes": 10760956,
"heapUsedBytes": 66641512,
"domNodes": -264,
"jsHeapTotalBytes": 16240640,
"scriptDurationMs": 490.83,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 2049.9829999999974,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.875,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 539.4300000000001,
"heapDeltaBytes": 3990200,
"heapUsedBytes": 56494980,
"domNodes": -260,
"jsHeapTotalBytes": 16183296,
"scriptDurationMs": 98.258,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 2040.3929999999946,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.728,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 547.2680000000001,
"heapDeltaBytes": 4565496,
"heapUsedBytes": 56819296,
"domNodes": -260,
"jsHeapTotalBytes": 15921152,
"scriptDurationMs": 101.19300000000001,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2019.9620000000778,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.955000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 522.062,
"heapDeltaBytes": 16793844,
"heapUsedBytes": 70661556,
"domNodes": -261,
"jsHeapTotalBytes": 16183296,
"scriptDurationMs": 90.532,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 555.5669999999964,
"styleRecalcs": 48,
"styleRecalcDurationMs": 11.96,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 358.302,
"heapDeltaBytes": 6760256,
"heapUsedBytes": 49718112,
"domNodes": 22,
"jsHeapTotalBytes": 13369344,
"scriptDurationMs": 123.88499999999999,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 556.7020000000298,
"styleRecalcs": 48,
"styleRecalcDurationMs": 12.327,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 361.606,
"heapDeltaBytes": 6427348,
"heapUsedBytes": 49680588,
"domNodes": 22,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 124.16099999999999,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.669999999999998,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 576.7609999999195,
"styleRecalcs": 48,
"styleRecalcDurationMs": 11.678,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 360.90299999999996,
"heapDeltaBytes": 6543960,
"heapUsedBytes": 49583144,
"domNodes": 21,
"jsHeapTotalBytes": 13369344,
"scriptDurationMs": 126.54099999999998,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2016.4219999999773,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.48,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 357.236,
"heapDeltaBytes": 11235616,
"heapUsedBytes": 63427788,
"domNodes": 22,
"jsHeapTotalBytes": 25952256,
"scriptDurationMs": 20.319999999999997,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2024.815999999987,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.181000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 343.486,
"heapDeltaBytes": 20139272,
"heapUsedBytes": 63271356,
"domNodes": 23,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 19.551000000000005,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 1996.7599999999948,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.067,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 343.36899999999997,
"heapDeltaBytes": 19894652,
"heapUsedBytes": 62989088,
"domNodes": 20,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 18.322000000000003,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1983.3830000000034,
"styleRecalcs": 83,
"styleRecalcDurationMs": 50.367999999999995,
"layouts": 16,
"layoutDurationMs": 3.9880000000000004,
"taskDurationMs": 905.3209999999999,
"heapDeltaBytes": 11902664,
"heapUsedBytes": 54961688,
"domNodes": 71,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 102.304,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 2027.3150000000442,
"styleRecalcs": 88,
"styleRecalcDurationMs": 50.127,
"layouts": 16,
"layoutDurationMs": 4.868,
"taskDurationMs": 956.2300000000001,
"heapDeltaBytes": 11913452,
"heapUsedBytes": 54948032,
"domNodes": 74,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 105.818,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 2000.0609999999597,
"styleRecalcs": 88,
"styleRecalcDurationMs": 46.631,
"layouts": 16,
"layoutDurationMs": 4.3149999999999995,
"taskDurationMs": 927.5730000000001,
"heapDeltaBytes": 11869472,
"heapUsedBytes": 54617292,
"domNodes": 73,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 107.723,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8230.79899999999,
"styleRecalcs": 252,
"styleRecalcDurationMs": 45.608000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3922.6330000000003,
"heapDeltaBytes": 30588656,
"heapUsedBytes": 81614436,
"domNodes": -255,
"jsHeapTotalBytes": 24776704,
"scriptDurationMs": 1428.107,
"eventListeners": -109,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8175.782999999967,
"styleRecalcs": 250,
"styleRecalcDurationMs": 43.976,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3706.33,
"heapDeltaBytes": 25993236,
"heapUsedBytes": 76670236,
"domNodes": -257,
"jsHeapTotalBytes": 21106688,
"scriptDurationMs": 1257.706,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8219.03199999997,
"styleRecalcs": 252,
"styleRecalcDurationMs": 46.033,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3757.2699999999995,
"heapDeltaBytes": 21747552,
"heapUsedBytes": 72173172,
"domNodes": -256,
"jsHeapTotalBytes": 21368832,
"scriptDurationMs": 1259.5,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 12359.369000000015,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12346.63,
"heapDeltaBytes": -28184716,
"heapUsedBytes": 166078764,
"domNodes": -8331,
"jsHeapTotalBytes": 27090944,
"scriptDurationMs": 582.427,
"eventListeners": -16464,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 12481.79799999997,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12470.949,
"heapDeltaBytes": -34392600,
"heapUsedBytes": 166322188,
"domNodes": -8333,
"jsHeapTotalBytes": 24207360,
"scriptDurationMs": 585.538,
"eventListeners": -16464,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 12366.416999999956,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12353.012,
"heapDeltaBytes": -31038508,
"heapUsedBytes": 166120076,
"domNodes": -8331,
"jsHeapTotalBytes": 27090944,
"scriptDurationMs": 602.3320000000001,
"eventListeners": -16462,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.780000000000047,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 14231.569999999976,
"styleRecalcs": 65,
"styleRecalcDurationMs": 13.881000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14208.297999999999,
"heapDeltaBytes": -6586648,
"heapUsedBytes": 186909028,
"domNodes": -8331,
"jsHeapTotalBytes": 26742784,
"scriptDurationMs": 846.461,
"eventListeners": -16462,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 14345.949999999959,
"styleRecalcs": 68,
"styleRecalcDurationMs": 14.037999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14324.529000000002,
"heapDeltaBytes": -31645052,
"heapUsedBytes": 160542976,
"domNodes": -8331,
"jsHeapTotalBytes": -2269184,
"scriptDurationMs": 870.709,
"eventListeners": -16456,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 14135.51700000005,
"styleRecalcs": 65,
"styleRecalcDurationMs": 13.74900000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14113.362000000001,
"heapDeltaBytes": -22291132,
"heapUsedBytes": 173305580,
"domNodes": -8333,
"jsHeapTotalBytes": 24645632,
"scriptDurationMs": 850.7130000000001,
"eventListeners": -16460,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 471.78199999996195,
"styleRecalcs": 23,
"styleRecalcDurationMs": 24.657,
"layouts": 5,
"layoutDurationMs": 1.5469999999999997,
"taskDurationMs": 133.22299999999998,
"heapDeltaBytes": 4779332,
"heapUsedBytes": 49875480,
"domNodes": 192,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 28.220999999999997,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "workflow-execution",
"durationMs": 459.68599999991966,
"styleRecalcs": 21,
"styleRecalcDurationMs": 28.880000000000003,
"layouts": 6,
"layoutDurationMs": 1.6489999999999998,
"taskDurationMs": 137.07599999999996,
"heapDeltaBytes": 4555392,
"heapUsedBytes": 48163324,
"domNodes": 159,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 33.717,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "workflow-execution",
"durationMs": 444.753999999989,
"styleRecalcs": 16,
"styleRecalcDurationMs": 21.679000000000002,
"layouts": 5,
"layoutDurationMs": 1.2389999999999999,
"taskDurationMs": 118.15900000000002,
"heapDeltaBytes": 4516504,
"heapUsedBytes": 48401148,
"domNodes": 156,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 26.390000000000004,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
}
]
} |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
browser_tests/tests/subgraph/subgraphDraftPositions.spec.ts (1)
35-46: Draft persistence poll could be more specific.The poll checks for any draft key starting with
'Comfy.Workflow.Draft.v2:'. If test isolation is imperfect (e.g., stale localStorage from a prior run), this could pass prematurely before the current workflow's draft is written. Consider polling for the draft content to contain expected node IDs, or verifying key count increased.That said, if the fixture guarantees a clean browser context, this is acceptable.
🔧 Optional: more robust draft detection
// Wait for the debounced draft persistence to flush to localStorage await expect .poll( () => - comfyPage.page.evaluate(() => - Object.keys(localStorage).some((k) => - k.startsWith('Comfy.Workflow.Draft.v2:') - ) - ), + comfyPage.page.evaluate(() => { + const draftKeys = Object.keys(localStorage).filter((k) => + k.startsWith('Comfy.Workflow.Draft.v2:') + ) + // Verify at least one draft exists and contains subgraph data + return draftKeys.some((k) => { + const raw = localStorage.getItem(k) + return raw && raw.includes('subgraphs') + }) + }), { timeout: 3000 } ) .toBe(true)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@browser_tests/tests/subgraph/subgraphDraftPositions.spec.ts` around lines 35 - 46, The poll currently checks any localStorage key starting with 'Comfy.Workflow.Draft.v2:' which can pass due to stale data; update the poll (the comfyPage.page.evaluate call) to locate the specific draft key for the current workflow (or read all matching keys and verify the stored draft JSON contains the expected node IDs or that the matching-key count increased), and assert that the draft value includes those expected node IDs (or matches the current workflowId) before returning true so the test only proceeds once the correct draft was persisted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@browser_tests/tests/subgraph/subgraphDraftPositions.spec.ts`:
- Around line 35-46: The poll currently checks any localStorage key starting
with 'Comfy.Workflow.Draft.v2:' which can pass due to stale data; update the
poll (the comfyPage.page.evaluate call) to locate the specific draft key for the
current workflow (or read all matching keys and verify the stored draft JSON
contains the expected node IDs or that the matching-key count increased), and
assert that the draft value includes those expected node IDs (or matches the
current workflowId) before returning true so the test only proceeds once the
correct draft was persisted.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 380ae82c-e80e-48f5-94ec-4b64b57935cc
📒 Files selected for processing (1)
browser_tests/tests/subgraph/subgraphDraftPositions.spec.ts
Use page.reload() with explicit draft persistence polling instead of comfyPage.setup(), which triggered a full navigation that bypassed the draft auto-load flow.
8b9d2d0 to
1c429d2
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
browser_tests/tests/subgraph/subgraphDraftPositions.spec.ts (1)
39-50: Make draft-flush waiting assert a real write, not just key presence.This poll can pass on a pre-existing
Comfy.Workflow.Draft.v2:*key, so reload may happen before this test’s latest state is actually persisted. Consider waiting for the draft payload to change.Proposed hardening
+ const draftSnapshotBefore = await comfyPage.page.evaluate(() => { + const key = Object.keys(localStorage).find((k) => + k.startsWith('Comfy.Workflow.Draft.v2:') + ) + return key + ? { key, value: localStorage.getItem(key) } + : { key: null, value: null } + }) + // Wait for the debounced draft persistence to flush to localStorage await expect .poll( () => - comfyPage.page.evaluate(() => - Object.keys(localStorage).some((k) => - k.startsWith('Comfy.Workflow.Draft.v2:') - ) - ), + comfyPage.page.evaluate((before) => { + const key = + before.key ?? + Object.keys(localStorage).find((k) => + k.startsWith('Comfy.Workflow.Draft.v2:') + ) + if (!key) return false + const value = localStorage.getItem(key) + return value !== null && value !== before.value + }, draftSnapshotBefore), { timeout: 3000 } ) .toBe(true)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@browser_tests/tests/subgraph/subgraphDraftPositions.spec.ts` around lines 39 - 50, The current poll only checks for existence of a Comfy.Workflow.Draft.v2:* key and can succeed on a stale pre-existing key; update the wait to assert a real write by first snapshotting the current draft payload via comfyPage.page.evaluate (reading value(s) for keys that startWith('Comfy.Workflow.Draft.v2:')) and then poll until the payload for that key(s) changes (or contains expected content from the test) instead of just toBe(true); modify the poll caller around comfyPage.page.evaluate and the localStorage check so it compares previousValue !== currentValue (or verifies the expected serialized draft fields) before proceeding.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@browser_tests/tests/subgraph/subgraphDraftPositions.spec.ts`:
- Around line 39-50: The current poll only checks for existence of a
Comfy.Workflow.Draft.v2:* key and can succeed on a stale pre-existing key;
update the wait to assert a real write by first snapshotting the current draft
payload via comfyPage.page.evaluate (reading value(s) for keys that
startWith('Comfy.Workflow.Draft.v2:')) and then poll until the payload for that
key(s) changes (or contains expected content from the test) instead of just
toBe(true); modify the poll caller around comfyPage.page.evaluate and the
localStorage check so it compares previousValue !== currentValue (or verifies
the expected serialized draft fields) before proceeding.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7df9fbe3-840d-4874-8eb9-adcb1a7e5e56
📒 Files selected for processing (1)
browser_tests/tests/subgraph/subgraphDraftPositions.spec.ts
| k.startsWith('Comfy.Workflow.Draft.v2:') | ||
| ) | ||
| ), | ||
| { timeout: 3000 } |
There was a problem hiding this comment.
I would keep the default 5s here.
It's rare that you want tighter timing constraints for things like this.
There was a problem hiding this comment.
Good call, changed to default 5s
| await expect | ||
| .poll( | ||
| () => | ||
| comfyPage.page.evaluate(() => |
There was a problem hiding this comment.
Optional: this seems like it would make sense to put into the page object, if you can find a good name.
There was a problem hiding this comment.
Extracted to WorkflowHelper.waitForDraftPersisted().
…overlay canvas.click() gets intercepted by DOM widget textarea after the position fix places nodes at correct locations. dispatchEvent bypasses the pointer event interception check.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
browser_tests/fixtures/helpers/SubgraphHelper.ts (1)
487-496: Extract repeated synthetic canvas click dispatch into a private helper.
packAllInteriorNodesduplicates the same pointerdown/pointerup sequence twice. A small helper keeps this deterministic event payload in one place and avoids drift later.Proposed refactor
export class SubgraphHelper { constructor(private readonly comfyPage: ComfyPage) {} + + private async dispatchPrimaryCanvasPointerClick(): Promise<void> { + const eventInit = { + bubbles: true, + cancelable: true, + button: 0 + } + await this.comfyPage.canvas.dispatchEvent('pointerdown', eventInit) + await this.comfyPage.canvas.dispatchEvent('pointerup', eventInit) + } @@ async packAllInteriorNodes(hostNodeId: string): Promise<void> { @@ - await this.comfyPage.canvas.dispatchEvent('pointerdown', { - bubbles: true, - cancelable: true, - button: 0 - }) - await this.comfyPage.canvas.dispatchEvent('pointerup', { - bubbles: true, - cancelable: true, - button: 0 - }) + await this.dispatchPrimaryCanvasPointerClick() @@ - await this.comfyPage.canvas.dispatchEvent('pointerdown', { - bubbles: true, - cancelable: true, - button: 0 - }) - await this.comfyPage.canvas.dispatchEvent('pointerup', { - bubbles: true, - cancelable: true, - button: 0 - }) + await this.dispatchPrimaryCanvasPointerClick()As per coding guidelines, “Watch out for Code Smells and refactor to avoid them.”
Also applies to: 505-514
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@browser_tests/fixtures/helpers/SubgraphHelper.ts` around lines 487 - 496, packAllInteriorNodes currently repeats the same synthetic pointerdown/pointerup sequence (this.comfyPage.canvas.dispatchEvent with bubbles/cancelable/button) in two places (around the shown block and again at lines 505-514); extract that logic into a private helper method (e.g., private dispatchCanvasClick or private clickCanvas) on SubgraphHelper that accepts the target element (this.comfyPage.canvas) and dispatches pointerdown then pointerup with the deterministic payload, then replace both duplicated blocks in packAllInteriorNodes with calls to that helper to centralize the event payload and avoid duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@browser_tests/fixtures/helpers/SubgraphHelper.ts`:
- Around line 487-496: packAllInteriorNodes currently repeats the same synthetic
pointerdown/pointerup sequence (this.comfyPage.canvas.dispatchEvent with
bubbles/cancelable/button) in two places (around the shown block and again at
lines 505-514); extract that logic into a private helper method (e.g., private
dispatchCanvasClick or private clickCanvas) on SubgraphHelper that accepts the
target element (this.comfyPage.canvas) and dispatches pointerdown then pointerup
with the deterministic payload, then replace both duplicated blocks in
packAllInteriorNodes with calls to that helper to centralize the event payload
and avoid duplication.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 309f14f1-4433-4444-8f90-0e51496c75f0
📒 Files selected for processing (1)
browser_tests/fixtures/helpers/SubgraphHelper.ts
Move draft localStorage polling into WorkflowHelper for reuse. Use default 5s timeout instead of 3s per review feedback.
|
Nice fix! We have a complementary one-file fix in #10810 that addresses a related but separate issue — Zero file overlap between our PRs (yours touches If you want to bundle them, cherry-pick commands: git fetch https://github.com/artokun/ComfyUI_frontend.git fix/viewport-template-override
git cherry-pick df7c4c5ad # fix: use public nodes getter, remove redundant variable
git cherry-pick 650834d0d # fix: don't override loadGraphData viewport on cache missOr we can rebase #10810 onto your branch as the base. Either way works — happy to help land both fixes together. |
|
Caution Docstrings generation - FAILED No docstrings were generated. |
|
Thanks so much for looking into this and offering to bundle them together — really appreciate the collaboration! 😍 Since both PRs have zero file overlap and address the same viewport transition problem from different angles (yours fixes I'd prefer to keep them as separate PRs if that's okay — keeping the scope narrow makes it easier to reason about each fix in isolation, and safer to revert independently if either one causes unexpected issues down the road. Looking forward to seeing #10810 land as well — together they should make viewport transitions much more robust! |
Summary
Fix subgraph internal node positions being permanently corrupted when entering a subgraph after a draft workflow reload. The corruption accumulated across page refreshes, causing nodes to progressively drift apart or compress together.
Changes
ResizeObservercallback (useVueNodeResizeTracking.ts), node positions are now read from the Layout Store (source of truth, initialized from LiteGraph) instead of reverse-converting DOM screen coordinates viagetBoundingClientRect()+clientPosToCanvasPos(). The fallback to DOM-based conversion is retained only for nodes not yet present in the Layout Store.Root Cause
ResizeObserverwas usinggetBoundingClientRect()to get DOM element positions, then converting them to canvas coordinates viaclientPosToCanvasPos(). This conversion depends on the currentcanvas.ds.scaleandcanvas.ds.offset.During graph transitions (e.g., entering a subgraph from a draft-loaded workflow), the canvas viewport was stale — it still had the parent graph's zoom level because
fitView()hadn't run yet (it's scheduled viarequestAnimationFrame). TheResizeObservercallback fired beforefitView, converting DOM positions using the wrong scale/offset, and writing the corrupted positions to the Layout Store. TheuseLayoutSyncwriteback then permanently overwrote the LiteGraph node positions.The corruption accumulated across sessions:
ResizeObserverwrites corrupted positionsThis is the same class of bug that PR #9121 fixed for slot positions — the DOM→canvas coordinate conversion is inherently fragile during viewport transitions. This PR applies the same principle to node positions.
Why This Only Affects
main(No Backport Needed)This bug requires two features that only exist on
main, not oncore/1.41orcore/1.42:subgraphNavigationStore's watcher toflush: 'sync'and addedrequestAnimationFrame(fitView)on viewport cache miss. This creates the timing window whereResizeObserverfires beforefitViewcorrects the canvas scale.On 1.41/1.42,
restoreViewportdoes nothing on cache miss (nofitViewscheduling), and the watcher uses default async flush — so theResizeObservernever runs with a stale viewport.Review Focus
nodeLayout.position(already in the Layout Store frominitializeFromLiteGraph) instead of computing position fromgetBoundingClientRect(). This eliminates the dependency on canvas scale/offset being up-to-date duringResizeObservercallbacks.getBoundingClientRect→clientPosToCanvasPos) is retained for nodes not yet in the Layout Store (e.g., first render of a newly created node). At that point the canvas transform is stable, so the conversion is safe.E2E Test Fixes
subgraphDraftPositions.spec.ts: replacedcomfyPage.setup({ clearStorage: false })withpage.reload()+ explicit draft persistence polling. Thesetup()method performs a full navigation viagoto()which bypassed the draft auto-load flow.SubgraphHelper.packAllInteriorNodes: replacedcanvas.click()withdispatchEvent('pointerdown'/'pointerup'). The position fix places subgraph nodes at their correct locations, which now overlap with DOM widget textareas that intercept pointer events.Test Plan
useVueNodeResizeTracking.test.ts)subgraphDraftPositions.spec.ts— draft reload preserves subgraph node positionsScreenshots
Before

After

┆Issue is synchronized with this Notion page by Unito