[backport core/1.30] Fix partial execution inside subgraphs (#6704)

Backport of #6487 to `core/1.30`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6704-backport-core-1-30-Fix-partial-execution-inside-subgraphs-2ab6d73d365081d09667e6c44528ea74)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <austin@comfy.org>
This commit is contained in:
Comfy Org PR Bot
2025-11-15 03:55:53 +09:00
committed by GitHub
parent bf24b6e729
commit fe9f423b9a
4 changed files with 102 additions and 43 deletions

View File

@@ -501,6 +501,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)
}
},

View File

@@ -1456,6 +1456,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",

View File

@@ -520,8 +520,16 @@ export function collectFromNodes<T = LGraphNode, C = void>(
* @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<NodeExecutionId, string>(selectedNodes, {
collector: (node, parentExecutionId) => {
const nodeId = String(node.id)
@@ -531,7 +539,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
}

View File

@@ -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'])
})
})
})
})