From 431594a6fc8726388309aebe3d7da22cf7cf8f6a Mon Sep 17 00:00:00 2001 From: AustinMroz Date: Sat, 1 Nov 2025 16:08:57 -0700 Subject: [PATCH] Fix partial execution inside subgraphs (#6487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getExecutionIdsForSelectedNodes` is only used for partial execution. The prior implementation solved the wrong problem. Given a list of nodes, it would explore into subgraphs and return a list of partial ExecutionIds for all contained nodes. Because this does not resolve the partial execution path to the current subgraph, this is incorrect when the current graph is not the root graph. Woefully, this incorrect functionality is never useful because the recursive exploration only applies to subgraph nodes which never satisfy the outputNode filter applied by the parent function. An extra function is used to correctly append the parent execution path, but the existing, probably never useful code for recursively collecting children is otherwise left in place. Resolves #6480 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6487-Fix-partial-execution-inside-subgraphs-29d6d73d36508197924bfb3a0fb6699e) by [Unito](https://www.unito.io) --- src/composables/useCoreCommands.ts | 9 ++ src/locales/en/main.json | 2 + src/utils/graphTraversalUtil.ts | 27 ++++- .../tests/utils/graphTraversalUtil.test.ts | 107 +++++++++++------- 4 files changed, 102 insertions(+), 43 deletions(-) diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index 8168a21c8..c6f8182d8 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -526,6 +526,15 @@ export function useCoreCommands(): ComfyCommand[] { // Get execution IDs for all selected output nodes and their descendants const executionIds = getExecutionIdsForSelectedNodes(selectedOutputNodes) + if (executionIds.length === 0) { + toastStore.add({ + severity: 'error', + summary: t('toastMessages.failedToQueue'), + detail: t('toastMessages.failedExecutionPathResolution'), + life: 3000 + }) + return + } await app.queuePrompt(0, batchCount, executionIds) } }, diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 11cbfa380..b89d46720 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -1472,6 +1472,8 @@ "toastMessages": { "nothingToQueue": "Nothing to queue", "pleaseSelectOutputNodes": "Please select output nodes", + "failedToQueue": "Failed to queue", + "failedExecutionPathResolution": "Could not resolve path to selected nodes", "no3dScene": "No 3D scene to apply texture", "failedToApplyTexture": "Failed to apply texture", "no3dSceneToExport": "No 3D scene to export", diff --git a/src/utils/graphTraversalUtil.ts b/src/utils/graphTraversalUtil.ts index edb4003e2..2339338f7 100644 --- a/src/utils/graphTraversalUtil.ts +++ b/src/utils/graphTraversalUtil.ts @@ -541,8 +541,16 @@ export function collectFromNodes( * @returns Array of execution IDs for selected nodes and all nodes within selected subgraphs */ export function getExecutionIdsForSelectedNodes( - selectedNodes: LGraphNode[] + selectedNodes: LGraphNode[], + startGraph = selectedNodes[0]?.graph ): NodeExecutionId[] { + if (!startGraph) return [] + const rootGraph = startGraph.rootGraph + const parentPath = startGraph.isRootGraph + ? '' + : findPartialExecutionPathToGraph(startGraph, rootGraph) + if (parentPath === undefined) return [] + return collectFromNodes(selectedNodes, { collector: (node, parentExecutionId) => { const nodeId = String(node.id) @@ -552,7 +560,22 @@ export function getExecutionIdsForSelectedNodes( const nodeId = String(node.id) return parentExecutionId ? `${parentExecutionId}:${nodeId}` : nodeId }, - initialContext: '', + initialContext: parentPath, expandSubgraphs: true }) } + +function findPartialExecutionPathToGraph( + target: LGraph, + root: LGraph +): string | undefined { + for (const node of root.nodes) { + if (!node.isSubgraphNode()) continue + + if (node.subgraph === target) return `${node.id}` + + const subpath = findPartialExecutionPathToGraph(target, node.subgraph) + if (subpath !== undefined) return node.id + ':' + subpath + } + return undefined +} diff --git a/tests-ui/tests/utils/graphTraversalUtil.test.ts b/tests-ui/tests/utils/graphTraversalUtil.test.ts index f6cb74804..2af5a6ee9 100644 --- a/tests-ui/tests/utils/graphTraversalUtil.test.ts +++ b/tests-ui/tests/utils/graphTraversalUtil.test.ts @@ -35,14 +35,18 @@ function createMockNode( isSubgraph?: boolean subgraph?: Subgraph callback?: () => void + graph?: LGraph } = {} ): LGraphNode { - return { + const node = { id, isSubgraphNode: options.isSubgraph ? () => true : undefined, subgraph: options.subgraph, - onExecutionStart: options.callback + onExecutionStart: options.callback, + graph: options.graph } as unknown as LGraphNode + options.graph?.nodes?.push(node) + return node } // Mock graph factory @@ -50,20 +54,28 @@ function createMockGraph(nodes: LGraphNode[]): LGraph { return { _nodes: nodes, nodes: nodes, + isRootGraph: true, getNodeById: (id: string | number) => nodes.find((n) => String(n.id) === String(id)) || null } as unknown as LGraph } // Mock subgraph factory -function createMockSubgraph(id: string, nodes: LGraphNode[]): Subgraph { - return { +function createMockSubgraph( + id: string, + nodes: LGraphNode[], + rootGraph?: LGraph +): Subgraph { + const graph = { id, _nodes: nodes, nodes: nodes, + isRootGraph: false, + rootGraph, getNodeById: (nodeId: string | number) => nodes.find((n) => String(n.id) === String(nodeId)) || null } as unknown as Subgraph + return graph } describe('graphTraversalUtil', () => { @@ -983,31 +995,30 @@ describe('graphTraversalUtil', () => { describe('getExecutionIdsForSelectedNodes', () => { it('should return simple IDs for top-level nodes', () => { - const nodes = [ - createMockNode('123'), - createMockNode('456'), - createMockNode('789') - ] + const graph = createMockGraph([]) + createMockNode('123', { graph }) + createMockNode('456', { graph }) + createMockNode('789', { graph }) - const executionIds = getExecutionIdsForSelectedNodes(nodes) + const executionIds = getExecutionIdsForSelectedNodes(graph.nodes) expect(executionIds).toEqual(['789', '456', '123']) // DFS processes in LIFO order }) it('should expand subgraph nodes to include all children', () => { + const graph = createMockGraph([]) const subNodes = [createMockNode('10'), createMockNode('11')] const subgraph = createMockSubgraph('sub-uuid', subNodes) - const nodes = [ - createMockNode('1'), - createMockNode('2', { isSubgraph: true, subgraph }) - ] + createMockNode('1', { graph }) + createMockNode('2', { isSubgraph: true, subgraph, graph }) - const executionIds = getExecutionIdsForSelectedNodes(nodes) + const executionIds = getExecutionIdsForSelectedNodes(graph.nodes) expect(executionIds).toEqual(['2', '2:10', '2:11', '1']) // DFS: node 2 first, then its children }) it('should handle deeply nested subgraphs correctly', () => { + const graph = createMockGraph([]) const deepNodes = [createMockNode('30'), createMockNode('31')] const deepSubgraph = createMockSubgraph('deep-uuid', deepNodes) @@ -1019,7 +1030,8 @@ describe('graphTraversalUtil', () => { const topNode = createMockNode('10', { isSubgraph: true, - subgraph: midSubgraph + subgraph: midSubgraph, + graph }) const executionIds = getExecutionIdsForSelectedNodes([topNode]) @@ -1028,16 +1040,15 @@ describe('graphTraversalUtil', () => { }) it('should handle mixed selection of regular and subgraph nodes', () => { + const graph = createMockGraph([]) const subNodes = [createMockNode('100'), createMockNode('101')] const subgraph = createMockSubgraph('sub-uuid', subNodes) - const nodes = [ - createMockNode('1'), - createMockNode('2', { isSubgraph: true, subgraph }), - createMockNode('3') - ] + createMockNode('1', { graph }) + createMockNode('2', { isSubgraph: true, subgraph, graph }) + createMockNode('3', { graph }) - const executionIds = getExecutionIdsForSelectedNodes(nodes) + const executionIds = getExecutionIdsForSelectedNodes(graph.nodes) expect(executionIds).toEqual([ '3', @@ -1054,10 +1065,12 @@ describe('graphTraversalUtil', () => { }) it('should handle subgraph with no children', () => { + const graph = createMockGraph([]) const emptySubgraph = createMockSubgraph('empty-uuid', []) const node = createMockNode('1', { isSubgraph: true, - subgraph: emptySubgraph + subgraph: emptySubgraph, + graph }) const executionIds = getExecutionIdsForSelectedNodes([node]) @@ -1079,9 +1092,11 @@ describe('graphTraversalUtil', () => { currentSubgraph = createMockSubgraph(`deep-${i}`, [node]) } + const graph = createMockGraph([]) const topNode = createMockNode('1', { isSubgraph: true, - subgraph: currentSubgraph + subgraph: currentSubgraph, + graph }) const executionIds = getExecutionIdsForSelectedNodes([topNode]) @@ -1103,12 +1118,11 @@ describe('graphTraversalUtil', () => { createMockNode('101') // Same ID as in subgraph1 ]) - const nodes = [ - createMockNode('1', { isSubgraph: true, subgraph: subgraph1 }), - createMockNode('2', { isSubgraph: true, subgraph: subgraph2 }) - ] + const graph = createMockGraph([]) + createMockNode('1', { isSubgraph: true, subgraph: subgraph1, graph }) + createMockNode('2', { isSubgraph: true, subgraph: subgraph2, graph }) - const executionIds = getExecutionIdsForSelectedNodes(nodes) + const executionIds = getExecutionIdsForSelectedNodes(graph.nodes) expect(executionIds).toEqual([ '2', @@ -1128,9 +1142,11 @@ describe('graphTraversalUtil', () => { } const bigSubgraph = createMockSubgraph('big-uuid', manyNodes) + const graph = createMockGraph([]) const node = createMockNode('parent', { isSubgraph: true, - subgraph: bigSubgraph + subgraph: bigSubgraph, + graph }) const start = performance.now() @@ -1157,19 +1173,17 @@ describe('graphTraversalUtil', () => { }) const midSubgraph = createMockSubgraph('mid-uuid', [midNode1, midNode2]) - const topNode = createMockNode('100', { - isSubgraph: true, - subgraph: midSubgraph - }) - + const graph = createMockGraph([]) // Select nodes at different nesting levels - const selectedNodes = [ - createMockNode('1'), // Root level - topNode, // Contains subgraph - createMockNode('2') // Root level - ] + createMockNode('100', { + isSubgraph: true, + subgraph: midSubgraph, + graph + }) + createMockNode('1', { graph }) + createMockNode('2', { graph }) - const executionIds = getExecutionIdsForSelectedNodes(selectedNodes) + const executionIds = getExecutionIdsForSelectedNodes(graph.nodes) expect(executionIds).toContain('1') expect(executionIds).toContain('2') @@ -1178,6 +1192,17 @@ describe('graphTraversalUtil', () => { expect(executionIds).toContain('100:202') expect(executionIds).toContain('100:202:300') }) + it('should resolve full execution path of a node inside a subgraph', () => { + const graph = createMockGraph([]) + const subgraph = createMockSubgraph('sub-uuid', [], graph) + createMockNode('11', { graph: subgraph }) + createMockNode('10', { graph: subgraph }) + createMockNode('2', { isSubgraph: true, subgraph, graph }) + + const executionIds = getExecutionIdsForSelectedNodes(subgraph.nodes) + + expect(executionIds).toEqual(['2:10', '2:11']) + }) }) }) })