mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
prototype - eager execution
This commit is contained in:
@@ -50,6 +50,7 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
|||||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
|
import { useEagerExecutionStore } from '@/stores/eagerExecutionStore'
|
||||||
import {
|
import {
|
||||||
getAllNonIoNodesInSubgraph,
|
getAllNonIoNodesInSubgraph,
|
||||||
getExecutionIdsForSelectedNodes
|
getExecutionIdsForSelectedNodes
|
||||||
@@ -1225,6 +1226,25 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-database',
|
icon: 'pi pi-database',
|
||||||
label: 'toggle linear mode',
|
label: 'toggle linear mode',
|
||||||
function: () => (canvasStore.linearMode = !canvasStore.linearMode)
|
function: () => (canvasStore.linearMode = !canvasStore.linearMode)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Comfy.ToggleEagerExecution',
|
||||||
|
icon: 'pi pi-bolt',
|
||||||
|
label: 'Toggle Eager Execution',
|
||||||
|
function: () => {
|
||||||
|
const eagerExecutionStore = useEagerExecutionStore()
|
||||||
|
eagerExecutionStore.toggle()
|
||||||
|
toastStore.add({
|
||||||
|
severity: 'info',
|
||||||
|
summary: eagerExecutionStore.enabled
|
||||||
|
? t('Eager execution enabled')
|
||||||
|
: t('Eager execution disabled'),
|
||||||
|
detail: eagerExecutionStore.enabled
|
||||||
|
? t('Nodes will auto-execute when ancestors change')
|
||||||
|
: t('Auto-execution disabled'),
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ import {
|
|||||||
getComponent,
|
getComponent,
|
||||||
shouldRenderAsVue
|
shouldRenderAsVue
|
||||||
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
|
import { eagerExecutionService } from '@/services/eagerExecutionService'
|
||||||
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
@@ -170,6 +172,14 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
|||||||
if (widget.type !== 'asset') {
|
if (widget.type !== 'asset') {
|
||||||
widget.callback?.(value)
|
widget.callback?.(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger eager execution for this widget change in Vue Nodes mode
|
||||||
|
if (nodeData?.id) {
|
||||||
|
const node = app.rootGraph?.getNodeById(Number(nodeData.id))
|
||||||
|
if (node) {
|
||||||
|
eagerExecutionService.onNodeChanged(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tooltipText = getWidgetTooltip(widget)
|
const tooltipText = getWidgetTooltip(widget)
|
||||||
|
|||||||
296
src/services/eagerExecutionService.ts
Normal file
296
src/services/eagerExecutionService.ts
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import log from 'loglevel'
|
||||||
|
|
||||||
|
import { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||||
|
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
|
||||||
|
import { api } from '@/scripts/api'
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
|
import { useQueuePendingTaskCountStore } from '@/stores/queueStore'
|
||||||
|
|
||||||
|
const logger = log.getLogger('EagerExecutionService')
|
||||||
|
logger.setLevel('debug')
|
||||||
|
|
||||||
|
class EagerExecutionService {
|
||||||
|
private enabled: boolean = false
|
||||||
|
private executionPending: boolean = false
|
||||||
|
private isExecuting: boolean = false
|
||||||
|
private changedNodes: Set<string> = new Set()
|
||||||
|
private graphChangedListenerAttached: boolean = false
|
||||||
|
private isSettingUpListeners: boolean = false
|
||||||
|
private lastChangeTimestamp: Map<string, number> = new Map()
|
||||||
|
|
||||||
|
private wrappedNodes: Set<NodeId> = new Set()
|
||||||
|
private wrappedWidgets: WeakSet<object> = new WeakSet()
|
||||||
|
private graphPatched: boolean = false
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
if (this.enabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.enabled = true
|
||||||
|
this.setupEventListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
if (!this.enabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupEventListeners() {
|
||||||
|
this.setupWidgetListeners()
|
||||||
|
|
||||||
|
if (!this.graphChangedListenerAttached) {
|
||||||
|
logger.debug('Attaching graphChanged event listener')
|
||||||
|
api.addEventListener('graphChanged', () => {
|
||||||
|
if (this.enabled && !this.isExecuting) {
|
||||||
|
logger.debug('Graph changed, re-setting up widget listeners')
|
||||||
|
this.setupWidgetListeners()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.graphChangedListenerAttached = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupWidgetListeners() {
|
||||||
|
if (this.isSettingUpListeners) {
|
||||||
|
logger.debug('Already setting up listeners, skipping duplicate call')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const graph = app.rootGraph
|
||||||
|
if (!graph) {
|
||||||
|
setTimeout(() => this.setupWidgetListeners(), 100)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSettingUpListeners = true
|
||||||
|
logger.debug('Setting up widget listeners for all nodes')
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.wrappedNodes.clear()
|
||||||
|
|
||||||
|
graph.nodes.forEach((node) => {
|
||||||
|
this.attachWidgetCallbacks(node)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!this.graphPatched) {
|
||||||
|
const originalAdd = LGraph.prototype.add
|
||||||
|
LGraph.prototype.add = function (node: LGraphNode) {
|
||||||
|
const result = originalAdd.call(this, node)
|
||||||
|
if (eagerExecutionService.enabled) {
|
||||||
|
eagerExecutionService.attachWidgetCallbacks(node)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
this.graphPatched = true
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Finished setting up widget listeners')
|
||||||
|
} finally {
|
||||||
|
this.isSettingUpListeners = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachWidgetCallbacks(node: LGraphNode) {
|
||||||
|
if (!node.widgets) return
|
||||||
|
|
||||||
|
if (this.wrappedNodes.has(node.id)) {
|
||||||
|
logger.debug(
|
||||||
|
`Skipping callback attachment for ${node.title || node.type} (${node.id}) - already attached`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.wrappedNodes.add(node.id)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Attaching callbacks to ${node.title || node.type} (${node.id}) - ${node.widgets.length} widget(s)`
|
||||||
|
)
|
||||||
|
|
||||||
|
node.widgets.forEach((widget, index) => {
|
||||||
|
if (this.wrappedWidgets.has(widget)) {
|
||||||
|
logger.debug(
|
||||||
|
` Widget ${index} (${widget.name}) already wrapped, skipping`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.wrappedWidgets.add(widget)
|
||||||
|
|
||||||
|
const originalCallback = widget.callback
|
||||||
|
|
||||||
|
widget.callback = (value?: any) => {
|
||||||
|
if (originalCallback) {
|
||||||
|
originalCallback.call(widget, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.enabled) {
|
||||||
|
this.onNodeChanged(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeChanged(node: LGraphNode) {
|
||||||
|
if (!this.enabled) return
|
||||||
|
|
||||||
|
const nodeId = String(node.id)
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const lastChange = this.lastChangeTimestamp.get(nodeId) || 0
|
||||||
|
const timeSinceLastChange = now - lastChange
|
||||||
|
|
||||||
|
if (timeSinceLastChange < 50) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastChangeTimestamp.set(nodeId, now)
|
||||||
|
|
||||||
|
if (this.isExecuting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueStore = useQueuePendingTaskCountStore()
|
||||||
|
if (queueStore.count > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.changedNodes.add(nodeId)
|
||||||
|
|
||||||
|
if (!this.executionPending) {
|
||||||
|
this.scheduleExecution()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleExecution() {
|
||||||
|
this.executionPending = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
void this.executeEager()
|
||||||
|
this.executionPending = false
|
||||||
|
this.changedNodes.clear()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeEager() {
|
||||||
|
if (!app.rootGraph || this.changedNodes.size === 0) return
|
||||||
|
|
||||||
|
const queueStore = useQueuePendingTaskCountStore()
|
||||||
|
|
||||||
|
if (queueStore.count > 0 || this.isExecuting) {
|
||||||
|
logger.info('Execution already in progress, skipping eager execution')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isExecuting = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info('===== EAGER EXECUTION TRIGGERED =====')
|
||||||
|
|
||||||
|
const changedNodesInfo = Array.from(this.changedNodes).map((nodeId) => {
|
||||||
|
const node = app.rootGraph?.getNodeById(Number(nodeId))
|
||||||
|
const title = node?.title || node?.type || `Node ${nodeId}`
|
||||||
|
return `${title} (${nodeId})`
|
||||||
|
})
|
||||||
|
logger.info(`Changed nodes: [${changedNodesInfo.join(', ')}]`)
|
||||||
|
|
||||||
|
const affectedNodes = this.getAffectedNodes(
|
||||||
|
app.rootGraph,
|
||||||
|
this.changedNodes
|
||||||
|
)
|
||||||
|
|
||||||
|
if (affectedNodes.size === 0) {
|
||||||
|
logger.info('No downstream nodes to execute')
|
||||||
|
logger.info('===== EAGER EXECUTION COMPLETE =====')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const affectedNodesInfo = Array.from(affectedNodes).map((nodeId) => {
|
||||||
|
const node = app.rootGraph?.getNodeById(Number(nodeId))
|
||||||
|
const title = node?.title || node?.type || `Node ${nodeId}`
|
||||||
|
return `${title} (${nodeId})`
|
||||||
|
})
|
||||||
|
logger.info(`Nodes to execute: [${affectedNodesInfo.join(', ')}]`)
|
||||||
|
logger.info(`Total: ${affectedNodes.size} node(s) will execute`)
|
||||||
|
|
||||||
|
const allNodesInfo = app.rootGraph?.nodes.map((node) => {
|
||||||
|
const nodeId = String(node.id)
|
||||||
|
const title = node.title || node.type || `Node ${nodeId}`
|
||||||
|
const willExecute = affectedNodes.has(nodeId)
|
||||||
|
|
||||||
|
if (willExecute) return `WILL EXECUTE - ${title} (${nodeId})`
|
||||||
|
return `SKIPPED - ${title} (${nodeId})`
|
||||||
|
})
|
||||||
|
logger.info('Full execution plan:')
|
||||||
|
allNodesInfo?.forEach((info) => logger.info(` ${info}`))
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Triggering partial execution with ${affectedNodes.size} targets...`
|
||||||
|
)
|
||||||
|
|
||||||
|
await app.queuePrompt(0, 1, Array.from(affectedNodes))
|
||||||
|
logger.info('===== EAGER EXECUTION COMPLETE =====')
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to execute eagerly:', error)
|
||||||
|
} finally {
|
||||||
|
this.isExecuting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAffectedNodes(
|
||||||
|
graph: LGraph,
|
||||||
|
changedNodeIds: Set<string>
|
||||||
|
): Set<string> {
|
||||||
|
const affected = new Set<string>()
|
||||||
|
const visited = new Set<string>()
|
||||||
|
|
||||||
|
logger.info('Analyzing downstream dependencies...')
|
||||||
|
|
||||||
|
const findDownstream = (nodeId: string, depth: number = 0) => {
|
||||||
|
if (visited.has(nodeId)) return
|
||||||
|
visited.add(nodeId)
|
||||||
|
|
||||||
|
const node = graph.getNodeById(Number(nodeId))
|
||||||
|
if (!node) return
|
||||||
|
|
||||||
|
const indent = ' '.repeat(depth)
|
||||||
|
|
||||||
|
if (node.outputs) {
|
||||||
|
node.outputs.forEach((output) => {
|
||||||
|
if (!output.links || output.links.length === 0) return
|
||||||
|
|
||||||
|
output.links.forEach((linkId) => {
|
||||||
|
const link = graph.links.get(linkId)
|
||||||
|
if (!link) return
|
||||||
|
|
||||||
|
const targetNodeId = String(link.target_id)
|
||||||
|
const targetNode = graph.getNodeById(link.target_id)
|
||||||
|
const targetTitle =
|
||||||
|
targetNode?.title || targetNode?.type || `Node ${targetNodeId}`
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`${indent} └─> ${targetTitle} (${targetNodeId}) - will execute`
|
||||||
|
)
|
||||||
|
affected.add(targetNodeId)
|
||||||
|
findDownstream(targetNodeId, depth + 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changedNodeIds.forEach((nodeId) => {
|
||||||
|
const node = graph.getNodeById(Number(nodeId))
|
||||||
|
const nodeTitle = node?.title || node?.type || `Node ${nodeId}`
|
||||||
|
|
||||||
|
logger.info(`Starting from: ${nodeTitle} (${nodeId})`)
|
||||||
|
affected.add(nodeId)
|
||||||
|
|
||||||
|
findDownstream(nodeId, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(`Analysis complete: ${affected.size} node(s) will execute`)
|
||||||
|
|
||||||
|
return affected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eagerExecutionService = new EagerExecutionService()
|
||||||
51
src/stores/eagerExecutionStore.ts
Normal file
51
src/stores/eagerExecutionStore.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { eagerExecutionService } from '@/services/eagerExecutionService'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store for managing eager execution settings and state
|
||||||
|
*/
|
||||||
|
export const useEagerExecutionStore = defineStore('eagerExecution', () => {
|
||||||
|
// State
|
||||||
|
const enabled = ref(false)
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable eager execution
|
||||||
|
*/
|
||||||
|
function enable() {
|
||||||
|
enabled.value = true
|
||||||
|
eagerExecutionService.enable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable eager execution
|
||||||
|
*/
|
||||||
|
function disable() {
|
||||||
|
enabled.value = false
|
||||||
|
eagerExecutionService.disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle eager execution
|
||||||
|
*/
|
||||||
|
function toggle() {
|
||||||
|
if (enabled.value) {
|
||||||
|
disable()
|
||||||
|
} else {
|
||||||
|
enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
enabled,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
enable,
|
||||||
|
disable,
|
||||||
|
toggle
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user