Skip to content

Conversation

@cstuncsik
Copy link
Contributor

Summary

Fixes performance bottleneck in workflow runners by optimizing the hasPath method to avoid unnecessary node type loading during expression evaluation.

Related Linear tickets, Github issues, and Community forum posts

PAY-3158

Review / Merge checklist

  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with release/backport (if the PR is an urgent fix that needs to be backported)

@n8n-assistant n8n-assistant bot added core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team labels Jul 23, 2025
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

cubic analysis

2 issues found across 1 file • Review in cubic

React with 👍 or 👎 to teach cubic. You can also tag @cubic-dev-ai to give feedback, ask questions, or re-run the review.

// Collect connection types from source connections
for (const nodeConnections of Object.values(this.connectionsBySourceNode)) {
for (const connectionType of Object.keys(nodeConnections)) {
connectionTypes.add(connectionType as NodeConnectionType);
Copy link
Contributor

Choose a reason for hiding this comment

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

Rule violated: Prefer Typeguards over Type casting

  Directly casting `connectionType` to `NodeConnectionType` violates the "Prefer Typeguards over Type casting" rule. Replace the `as` assertion with a type guard or collect keys into a `Set<string>` and validate before use.
Prompt for AI agents
Address the following comment on packages/workflow/src/workflow.ts at line 1041:

<comment>Directly casting `connectionType` to `NodeConnectionType` violates the &quot;Prefer Typeguards over Type casting&quot; rule. Replace the `as` assertion with a type guard or collect keys into a `Set&lt;string&gt;` and validate before use.</comment>

<file context>
@@ -1032,16 +1032,24 @@ export class Workflow {
 
 			visited.add(nodeName);
 
-			// Check all connection types for this node
-			const allConnectionTypes = [
-				NodeConnectionTypes.Main,
-				NodeConnectionTypes.AiTool,
-				NodeConnectionTypes.AiMemory,
-				NodeConnectionTypes.AiDocument,
</file context>


for (const connectionType of allConnectionTypes) {
// Get connection types that actually exist in this workflow
const connectionTypes = new Set<NodeConnectionType>();
Copy link
Contributor

Choose a reason for hiding this comment

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

connectionTypes is recomputed on every BFS iteration, iterating over the entire connection map each time and causing avoidable O(N²) overhead; compute it once outside the while-loop instead.

Prompt for AI agents
Address the following comment on packages/workflow/src/workflow.ts at line 1036:

<comment>`connectionTypes` is recomputed on every BFS iteration, iterating over the entire connection map each time and causing avoidable O(N²) overhead; compute it once outside the while-loop instead.</comment>

<file context>
@@ -1032,16 +1032,24 @@ export class Workflow {
 
 			visited.add(nodeName);
 
-			// Check all connection types for this node
-			const allConnectionTypes = [
-				NodeConnectionTypes.Main,
-				NodeConnectionTypes.AiTool,
-				NodeConnectionTypes.AiMemory,
-				NodeConnectionTypes.AiDocument,
</file context>

@cstuncsik cstuncsik requested a review from ivov July 23, 2025 13:08
@codecov
Copy link

codecov bot commented Jul 23, 2025

Codecov Report

❌ Patch coverage is 94.73684% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/workflow/src/workflow.ts 94.11% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment on lines 775 to 788
for (const connectionType of nonMainConnectionTypes) {
const connections = nodeConnections[connectionType] || [];
for (const connectionGroup of connections) {
for (const connection of connectionGroup || []) {
if (connection?.node) {
const returnNode = this.getNode(connection.node);
if (!returnNode) {
throw new ApplicationError(`Node "${connection.node}" not found`);
}
return this.getParentMainInputNode(returnNode);
}
}
}
}
Copy link
Contributor Author

@cstuncsik cstuncsik Jul 23, 2025

Choose a reason for hiding this comment

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

I prefer the more declarative (functional programming) to the iterative way and if it doesn't decrease performance that much I'd go with this

Suggested change
for (const connectionType of nonMainConnectionTypes) {
const connections = nodeConnections[connectionType] || [];
for (const connectionGroup of connections) {
for (const connection of connectionGroup || []) {
if (connection?.node) {
const returnNode = this.getNode(connection.node);
if (!returnNode) {
throw new ApplicationError(`Node "${connection.node}" not found`);
}
return this.getParentMainInputNode(returnNode);
}
}
}
}
const parentNode = nonMainConnectionTypes
.flatMap((connectionType) => nodeConnections[connectionType] || [])
.flatMap((connectionGroup) => connectionGroup || [])
.map((connection) => {
if (connection?.node) {
const returnNode = this.getNode(connection.node);
if (!returnNode) {
throw new ApplicationError(`Node "${connection.node}" not found`);
}
return this.getParentMainInputNode(returnNode);
}
})
.find(Boolean);
return parentNode || node;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did a quick benchmark, and the iterative will perform better on complex graphs

  1. Simple Chain (5 nodes): Declarative was 63.6% faster - this is likely due to JIT optimization on simple cases
  2. Complex Graph (20 nodes): Declarative was 11.6% slower
  3. Deep Nesting (10 levels): Declarative was 18.6% slower
  4. Single Node (baseline): Declarative was 38.3% slower

Average performance ratio: 1.013 (declarative slightly slower overall)

@cstuncsik cstuncsik changed the title fix(core): Optimize hasPath method to use existing connection types fix(core): Optimize ensureValidPath method to use existing connection types Jul 23, 2025
@cstuncsik cstuncsik changed the title fix(core): Optimize ensureValidPath method to use existing connection types fix(core): Optimize connection type lookups Jul 23, 2025
if (node) {
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
const outputs = NodeHelpers.getNodeOutputs(this, node, nodeType.description);
if (!node) return node;
Copy link
Member

Choose a reason for hiding this comment

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

Either the argument type should reflect this is nullish, or this check is unnecessary?

Comment on lines -767 to -768
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
const outputs = NodeHelpers.getNodeOutputs(this, node, nodeType.description);
Copy link
Member

Choose a reason for hiding this comment

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

Is it safe not to check outputs anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since I'm not an expert on this part, I discussed it with claude code and it said yes.
I can share the reasoning with you if you're interested

Copy link
Member

Choose a reason for hiding this comment

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

Nobody is an expert on this ancient stuff :)

Since this is the core of the PR, can you please verify? I trust your judgment, not Claude's.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you think we still don't have enough tests to catch if something is wrong with this?

...Object.values(this.connectionsBySourceNode),
...Object.values(this.connectionsByDestinationNode),
].flatMap((nodeConnections) =>
Object.keys(nodeConnections).filter((key): key is NodeConnectionType => !!key),
Copy link
Member

Choose a reason for hiding this comment

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

Is this filter a no-op? Should it be like this?

].flatMap((nodeConnections) => Object.keys(nodeConnections) as NodeConnectionType[]),

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was to avoid type casting
I added them back
I just remember Mutasem setting up some rule to avoid using it

Copy link
Member

Choose a reason for hiding this comment

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

There are many cases where asserting (not casting) is reasonable if we know the keys. Nice article about it here.

expect(result).toBe(set1Node);
});

describe('nodes with only main outputs', () => {
Copy link
Member

Choose a reason for hiding this comment

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

Hats off, nice coverage!

@bundlemon
Copy link

bundlemon bot commented Jul 25, 2025

BundleMon

Unchanged files (2)
Status Path Size Limits
WASM Dependencies
tree-sitter-bash.wasm
181.26KB -
WASM Dependencies
tree-sitter.wasm
74.47KB -

No change in files bundle size

Groups updated (2)
Status Path Size Limits
**/*.js
5.3MB (+166.36KB +3.16%) -
**/*.css
184.52KB (+5.47KB +3.06%) -

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@cstuncsik cstuncsik requested a review from ivov July 25, 2025 11:22
Copy link
Member

@ivov ivov left a comment

Choose a reason for hiding this comment

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

Copy link
Member

@ivov ivov left a comment

Choose a reason for hiding this comment

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

Haven't tested, change looks clean.

@currents-bot
Copy link

currents-bot bot commented Jul 25, 2025

E2E Tests: n8n tests passed after 5m 27.4s

🟢 521 · 🔴 0 · ⚪️ 1

View Run Details

Run Details

  • Project: n8n

  • Groups: 3

  • Framework: Playwright

  • Run Status: Passed

  • Commit: 69b456a

  • Spec files: 110

  • Overall tests: 522

  • Duration: 5m 27.4s

  • Parallelization: 3

Groups

GroupId Results Spec Files Progress
mode:standard - Parallel 🟢 9 · 🔴 0 · ⚪️ 1 3 / 3
mode:standard - Sequential 🟢 4 · 🔴 0 · ⚪️ 0 2 / 2
No name 🟢 508 · 🔴 0 · ⚪️ 0 105 / 105


This message was posted automatically by currents.dev | Integration Settings

@cstuncsik cstuncsik merged commit 70eab1b into master Jul 25, 2025
78 checks passed
@cstuncsik cstuncsik deleted the pay-3158-optimize-haspath-method-to-avoid-unnecessary-node-type branch July 25, 2025 14:12
This was referenced Jul 28, 2025
@janober
Copy link
Member

janober commented Jul 28, 2025

Got released with n8n@1.105.0

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

Labels

core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team Released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants