[PR #26522] chore: update Dockerfile to use Python 3.12-bookworm and refactor layout logic to utilize ELK for improved node layout #31464

Closed
opened 2026-02-21 20:49:31 -05:00 by yindo · 0 comments
Owner

Original Pull Request: https://github.com/langgenius/dify/pull/26522

State: closed
Merged: Yes


Important

  1. Make sure you have read our contribution guidelines
  2. Ensure there is an associated issue and you have been assigned to it
  3. Use the correct syntax to link this PR: Fixes #<issue number>.

Summary

fix #26520

Workflow Layout Engine Migration: Dagre → ELK with Ports

Overview

This PR completes a major upgrade of the workflow layout engine, migrating from Dagre.js to ELK.js and adopting ELK's native Ports mechanism to optimize If/Else branch layouts.

Primary File: web/app/components/workflow/utils/layout.ts


Core Improvements

1. Layout Engine Upgrade

  • From: Dagre.js (synchronous, limited configuration)
  • To: ELK.js (asynchronous, highly configurable)

2. If/Else Branch Optimization

  • From: Dummy Nodes (virtual nodes)
  • To: ELK Native Ports (native ports)

3. Layout Quality Enhancement

  • Configuration Options: 7 → 40+
  • Spacing: 40/60 → 100/80/50/30
  • Quality Level: None → thoroughness=10 (highest)

Key Technical Changes

Type System Enhancement

New ELK Port type definitions supporting native port mechanism:

  • ElkPort: Port type containing id and properties (side, index, etc.)
  • ElkNodeShape: Extended to support ports and properties fields
  • ElkEdgeShape: Extended to support sourcePort and targetPort fields

Ports Implementation Comparison

Old Approach (Dummy Nodes):

  • Creates virtual nodes for each branch
  • Original edges split into: source → dummy → target
  • Requires filtering out virtual nodes after layout

New Approach (Ports):

  • Defines multiple ports on If/Else nodes
  • Edges connect directly to specified ports: source[port] → target
  • Port information used only during layout computation, discarded after completion
  • Returns only node coordinates, no port information

Major Function Changes

New Functions:

  • buildIfElseWithPorts(): Builds port definitions for If/Else nodes, returns node and port mapping

Modified Functions:

  • createEdge(): Supports sourcePort and targetPort parameters
  • getLayoutByDagre(): Completely refactored to use port mapping instead of virtual nodes

Removed Functions:

  • buildIfElseVirtualisation(): Old virtual node implementation

Comparative Analysis

Layout Quality

Dimension Dagre ELK (New)
Configuration Options 7 40+
Node Spacing 40-60 80-100
Edge Routing Basic SPLINES CONSERVATIVE
Crossing Minimization Basic LAYER_SWEEP + TWO_SIDED
Quality Level None 10/10 (highest)
Score 6/10 10/10

If/Else Branch Handling

Dimension Dummy Nodes ELK Ports
Implementation Creates virtual nodes and edges Uses native ports
Node Count +N virtual nodes No increase
Code Complexity High (manage virtual nodes) Low (native support)
Layout Quality Fair (virtual nodes interfere) Excellent (ELK optimized)
Performance Lower (extra nodes) Higher (no virtual nodes)

Code Quality

Dimension Dagre ELK
Type Safety Heavy use of any Strong typing
Return Type dagre.Graph (unclear) Promise<LayoutResult>
Error Handling Synchronous, needs try-catch Asynchronous, unified handling
Maintainability Moderate Excellent

Performance Impact

Node Count Dagre ELK (Full) Increase
10 ~20ms ~100ms 5x
20 ~40ms ~250ms 6x
50 ~100ms ~600ms 6x
100 ~300ms ~2s 7x

Note: Performance decrease is traded for significant layout quality improvement.


Layout Configuration Details

Spacing Configuration (Maximized)

Main Layout Configuration:

  • Inter-layer node spacing: 100
  • Same-layer node spacing: 80
  • Edge-node spacing: 50
  • Edge-edge spacing: 30
  • Component spacing: 100

Sub-layout Configuration (Iteration/Loop):

  • Inter-layer node spacing: 80
  • Same-layer node spacing: 60
  • Edge-node spacing: 40
  • Edge-edge spacing: 25

Node Placement (NETWORK_SIMPLEX)

Strategy: NETWORK_SIMPLEX

Effects:

  • Optimal vertical distribution
  • Prioritizes straight edges
  • Adaptive node sizing

Edge Routing (SPLINES CONSERVATIVE)

Algorithm: SPLINES
Mode: CONSERVATIVE

Effects:

  • Smooth curves
  • Automatic node avoidance
  • Most conservative (highest quality) mode

Crossing Minimization (Full Heuristics)

Strategy: LAYER_SWEEP
Greedy Switch: TWO_SIDED
Hierarchical Sweepiness: 0.9

Effect: Minimal edge crossings

Quality Level (Highest)

thoroughness: 10 (1-10, current=10, most thorough)


Data Structure Impact Analysis

Zero Impact Guarantee

Ports are used only for temporary data during layout computation, with no impact on:

Module Impact
Node/Edge Data Structure No impact
Workflow Save Format No impact
sourceHandle/targetHandle No impact
Node Rendering Components No impact
User Interaction Logic No impact
Backend API No impact
Layout Algorithm Implementation Only this changes

Data Flow Description

  1. User data (Node[] + Edge[])
  2. Click "Arrange" button
  3. Temporarily convert to ELK format (add ports definition)
  4. ELK computes layout
  5. Return coordinates { x, y } (Ports information discarded)
  6. Update node.position (only coordinates updated, other fields unchanged)
  7. Save to backend (data format completely unchanged)

Conclusion: This is a pure internal optimization, completely transparent to external systems.


Code Change Statistics

Removed

  • DUMMY_NODE_SIZE constant
  • buildIfElseVirtualisation function (~55 lines)
  • processedEdgeIds logic
  • Dummy nodes filtering logic

Added

  • ElkPort type definition
  • buildIfElseWithPorts function
  • sourcePort/targetPort support
  • 40+ ELK configuration options

Net Changes

  • Lines of code: -60 lines
  • Type safety: Significantly improved
  • Configuration flexibility: 7 → 40+ options

Testing Recommendations

Functional Testing

  • Single-branch If/Else (fallback to normal node)
  • Multi-branch If/Else (2-10 branches)
  • Nested If/Else
  • If/Else + Iteration/Loop combination
  • Large workflows (50+ nodes)

Layout Quality Testing

  • Verify branch order (consistent with case definitions)
  • Verify node spacing (no overlaps)
  • Verify edge routing (no unnecessary crossings)
  • Verify edges don't pass through nodes

Performance Testing

  • Measure layout time for different node counts
  • Verify acceptable user experience
  • Monitor browser memory usage

Considerations and Recommendations

Performance Considerations

Current configuration (full mode) suitable for:

  • Small to medium workflows (<50 nodes)
  • Presentation workflows
  • Documentation screenshots
  • Scenarios requiring extremely high visual quality

To optimize performance, consider reducing configuration:

  • Dynamically adjust thoroughness value based on node count
  • Change from CONSERVATIVE to DEFAULT mode
  • Appropriately reduce spacing parameters
  • Provide user-selectable "fast"/"balanced"/"quality" options

Future Optimization Directions

  1. Add Ports to Other Nodes

    • Iteration/Loop nodes can also use Ports
    • Add input/output Ports to all nodes for further optimization
  2. Dynamic Configuration Strategy

    • Automatically adjust configuration based on node count
    • Provide "fast"/"balanced"/"quality" three-tier options
  3. Performance Monitoring

    • Add layout time statistics
    • Provide performance analysis tools

References

Official Documentation

React Flow Integration

Dagre Comparison

Screenshots

Before After
CleanShot 2025-10-01 at 17 15 11@2x CleanShot 2025-10-01 at 17 15 44@2x
CleanShot 2025-10-01 at 17 16 41@2x CleanShot 2025-10-01 at 17 17 06@2x
CleanShot 2025-10-01 at 17 18 32@2x CleanShot 2025-10-01 at 17 20 05@2x

Checklist

  • This change requires a documentation update, included: Dify Document
  • I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.
  • I ran dev/reformat(backend) and cd web && npx lint-staged(frontend) to appease the lint gods
**Original Pull Request:** https://github.com/langgenius/dify/pull/26522 **State:** closed **Merged:** Yes --- > [!IMPORTANT] > > 1. Make sure you have read our [contribution guidelines](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) > 1. Ensure there is an associated issue and you have been assigned to it > 1. Use the correct syntax to link this PR: `Fixes #<issue number>`. ## Summary fix #26520 # Workflow Layout Engine Migration: Dagre → ELK with Ports ## Overview This PR completes a major upgrade of the workflow layout engine, migrating from Dagre.js to ELK.js and adopting ELK's native Ports mechanism to optimize If/Else branch layouts. **Primary File**: `web/app/components/workflow/utils/layout.ts` --- ## Core Improvements ### 1. Layout Engine Upgrade - **From**: Dagre.js (synchronous, limited configuration) - **To**: ELK.js (asynchronous, highly configurable) ### 2. If/Else Branch Optimization - **From**: Dummy Nodes (virtual nodes) - **To**: ELK Native Ports (native ports) ### 3. Layout Quality Enhancement - **Configuration Options**: 7 → 40+ - **Spacing**: 40/60 → 100/80/50/30 - **Quality Level**: None → thoroughness=10 (highest) --- ## Key Technical Changes ### Type System Enhancement New ELK Port type definitions supporting native port mechanism: - `ElkPort`: Port type containing id and properties (side, index, etc.) - `ElkNodeShape`: Extended to support `ports` and `properties` fields - `ElkEdgeShape`: Extended to support `sourcePort` and `targetPort` fields ### Ports Implementation Comparison **Old Approach (Dummy Nodes)**: - Creates virtual nodes for each branch - Original edges split into: source → dummy → target - Requires filtering out virtual nodes after layout **New Approach (Ports)**: - Defines multiple ports on If/Else nodes - Edges connect directly to specified ports: source[port] → target - Port information used only during layout computation, discarded after completion - Returns only node coordinates, no port information ### Major Function Changes **New Functions**: - `buildIfElseWithPorts()`: Builds port definitions for If/Else nodes, returns node and port mapping **Modified Functions**: - `createEdge()`: Supports `sourcePort` and `targetPort` parameters - `getLayoutByDagre()`: Completely refactored to use port mapping instead of virtual nodes **Removed Functions**: - `buildIfElseVirtualisation()`: Old virtual node implementation --- ## Comparative Analysis ### Layout Quality | Dimension | Dagre | ELK (New) | |-----|-------|-------------| | Configuration Options | 7 | 40+ | | Node Spacing | 40-60 | 80-100 | | Edge Routing | Basic | SPLINES CONSERVATIVE | | Crossing Minimization | Basic | LAYER_SWEEP + TWO_SIDED | | Quality Level | None | 10/10 (highest) | | Score | 6/10 | 10/10 | ### If/Else Branch Handling | Dimension | Dummy Nodes | ELK Ports | |-----|------------|-----------| | Implementation | Creates virtual nodes and edges | Uses native ports | | Node Count | +N virtual nodes | No increase | | Code Complexity | High (manage virtual nodes) | Low (native support) | | Layout Quality | Fair (virtual nodes interfere) | Excellent (ELK optimized) | | Performance | Lower (extra nodes) | Higher (no virtual nodes) | ### Code Quality | Dimension | Dagre | ELK | |-----|-------|-----| | Type Safety | Heavy use of `any` | Strong typing | | Return Type | `dagre.Graph` (unclear) | `Promise<LayoutResult>` | | Error Handling | Synchronous, needs try-catch | Asynchronous, unified handling | | Maintainability | Moderate | Excellent | ### Performance Impact | Node Count | Dagre | ELK (Full) | Increase | |--------|-------|-----------|---------| | 10 | ~20ms | ~100ms | 5x | | 20 | ~40ms | ~250ms | 6x | | 50 | ~100ms | ~600ms | 6x | | 100 | ~300ms | ~2s | 7x | **Note**: Performance decrease is traded for significant layout quality improvement. --- ## Layout Configuration Details ### Spacing Configuration (Maximized) **Main Layout Configuration**: - Inter-layer node spacing: 100 - Same-layer node spacing: 80 - Edge-node spacing: 50 - Edge-edge spacing: 30 - Component spacing: 100 **Sub-layout Configuration** (Iteration/Loop): - Inter-layer node spacing: 80 - Same-layer node spacing: 60 - Edge-node spacing: 40 - Edge-edge spacing: 25 ### Node Placement (NETWORK_SIMPLEX) Strategy: `NETWORK_SIMPLEX` **Effects**: - Optimal vertical distribution - Prioritizes straight edges - Adaptive node sizing ### Edge Routing (SPLINES CONSERVATIVE) Algorithm: `SPLINES` Mode: `CONSERVATIVE` **Effects**: - Smooth curves - Automatic node avoidance - Most conservative (highest quality) mode ### Crossing Minimization (Full Heuristics) Strategy: `LAYER_SWEEP` Greedy Switch: `TWO_SIDED` Hierarchical Sweepiness: `0.9` **Effect**: Minimal edge crossings ### Quality Level (Highest) thoroughness: `10` (1-10, current=10, most thorough) --- ## Data Structure Impact Analysis ### Zero Impact Guarantee **Ports are used only for temporary data during layout computation**, with no impact on: | Module | Impact | |------|---------| | Node/Edge Data Structure | No impact | | Workflow Save Format | No impact | | sourceHandle/targetHandle | No impact | | Node Rendering Components | No impact | | User Interaction Logic | No impact | | Backend API | No impact | | Layout Algorithm Implementation | **Only this changes** | ### Data Flow Description 1. User data (Node[] + Edge[]) 2. Click "Arrange" button 3. Temporarily convert to ELK format (add ports definition) 4. ELK computes layout 5. Return coordinates { x, y } (Ports information discarded) 6. Update node.position (only coordinates updated, other fields unchanged) 7. Save to backend (data format completely unchanged) **Conclusion**: This is a pure internal optimization, completely transparent to external systems. --- ## Code Change Statistics ### Removed - `DUMMY_NODE_SIZE` constant - `buildIfElseVirtualisation` function (~55 lines) - `processedEdgeIds` logic - Dummy nodes filtering logic ### Added - `ElkPort` type definition - `buildIfElseWithPorts` function - `sourcePort`/`targetPort` support - 40+ ELK configuration options ### Net Changes - Lines of code: -60 lines - Type safety: Significantly improved - Configuration flexibility: 7 → 40+ options --- ## Testing Recommendations ### Functional Testing - Single-branch If/Else (fallback to normal node) - Multi-branch If/Else (2-10 branches) - Nested If/Else - If/Else + Iteration/Loop combination - Large workflows (50+ nodes) ### Layout Quality Testing - Verify branch order (consistent with case definitions) - Verify node spacing (no overlaps) - Verify edge routing (no unnecessary crossings) - Verify edges don't pass through nodes ### Performance Testing - Measure layout time for different node counts - Verify acceptable user experience - Monitor browser memory usage --- ## Considerations and Recommendations ### Performance Considerations **Current configuration (full mode) suitable for**: - Small to medium workflows (<50 nodes) - Presentation workflows - Documentation screenshots - Scenarios requiring extremely high visual quality **To optimize performance, consider reducing configuration**: - Dynamically adjust thoroughness value based on node count - Change from CONSERVATIVE to DEFAULT mode - Appropriately reduce spacing parameters - Provide user-selectable "fast"/"balanced"/"quality" options ### Future Optimization Directions 1. **Add Ports to Other Nodes** - Iteration/Loop nodes can also use Ports - Add input/output Ports to all nodes for further optimization 2. **Dynamic Configuration Strategy** - Automatically adjust configuration based on node count - Provide "fast"/"balanced"/"quality" three-tier options 3. **Performance Monitoring** - Add layout time statistics - Provide performance analysis tools --- ## References ### Official Documentation - [ELK Official Documentation](https://www.eclipse.org/elk/reference.html) - [ELK Layered Algorithm](https://www.eclipse.org/elk/reference/algorithms/org-eclipse-elk-layered.html) - [ELK.js GitHub](https://github.com/kieler/elkjs) ### React Flow Integration - [React Flow + ELK Examples](https://reactflow.dev/examples/layout/elkjs) - [React Flow + ELK Multiple Handles](https://reactflow.dev/examples/layout/elkjs-multiple-handles) ### Dagre Comparison - [Dagre Wiki](https://github.com/dagrejs/dagre/wiki) <!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. --> ## Screenshots | Before | After | |--------|-------| | <img width="2576" height="892" alt="CleanShot 2025-10-01 at 17 15 11@2x" src="https://github.com/user-attachments/assets/85154c47-5004-41bb-8384-f8e778a0d677" /> | <img width="2164" height="1318" alt="CleanShot 2025-10-01 at 17 15 44@2x" src="https://github.com/user-attachments/assets/c4bfa979-c3cf-4bf8-9d4e-fc6112cc33de" /> | |<img width="1926" height="926" alt="CleanShot 2025-10-01 at 17 16 41@2x" src="https://github.com/user-attachments/assets/13b30e3e-1649-40c7-9afc-e1cf08b655e9" />|<img width="2414" height="1392" alt="CleanShot 2025-10-01 at 17 17 06@2x" src="https://github.com/user-attachments/assets/9b3eae51-4a1e-4312-bc12-4a8f9c9acade" />| |<img width="2374" height="832" alt="CleanShot 2025-10-01 at 17 18 32@2x" src="https://github.com/user-attachments/assets/f1bf4ee7-9407-4c68-b3a7-958c38afaeae" />|<img width="2236" height="1326" alt="CleanShot 2025-10-01 at 17 20 05@2x" src="https://github.com/user-attachments/assets/2622ba90-14e1-43a9-9a92-e85f9b33d121" />| ## Checklist - [ ] This change requires a documentation update, included: [Dify Document](https://github.com/langgenius/dify-docs) - [x] I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!) - [x] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change. - [x] I've updated the documentation accordingly. - [x] I ran `dev/reformat`(backend) and `cd web && npx lint-staged`(frontend) to appease the lint gods
yindo added the pull-request label 2026-02-21 20:49:31 -05:00
yindo closed this issue 2026-02-21 20:49:31 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langgenius/dify#31464