diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index fa4761c46..9857cd3e1 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -50,6 +50,7 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore' import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore' import { useWorkspaceStore } from '@/stores/workspaceStore' +import { useEagerExecutionStore } from '@/stores/eagerExecutionStore' import { getAllNonIoNodesInSubgraph, getExecutionIdsForSelectedNodes @@ -1225,6 +1226,25 @@ export function useCoreCommands(): ComfyCommand[] { icon: 'pi pi-database', label: 'toggle linear mode', 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 + }) + } } ] diff --git a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue index 10a06ff15..bf5519abd 100644 --- a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue +++ b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue @@ -78,6 +78,8 @@ import { getComponent, shouldRenderAsVue } 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 { cn } from '@/utils/tailwindUtil' @@ -170,6 +172,14 @@ const processedWidgets = computed((): ProcessedWidget[] => { if (widget.type !== 'asset') { 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) diff --git a/src/services/eagerExecutionService.ts b/src/services/eagerExecutionService.ts new file mode 100644 index 000000000..7d8ece4a8 --- /dev/null +++ b/src/services/eagerExecutionService.ts @@ -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 = new Set() + private graphChangedListenerAttached: boolean = false + private isSettingUpListeners: boolean = false + private lastChangeTimestamp: Map = new Map() + + private wrappedNodes: Set = new Set() + private wrappedWidgets: WeakSet = 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 + ): Set { + const affected = new Set() + const visited = new Set() + + 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() diff --git a/src/stores/eagerExecutionStore.ts b/src/stores/eagerExecutionStore.ts new file mode 100644 index 000000000..cc3b29146 --- /dev/null +++ b/src/stores/eagerExecutionStore.ts @@ -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 + } +})