prototype - eager execution

This commit is contained in:
Terry Jia
2025-11-22 22:24:29 -05:00
parent 9da82f47ef
commit 3e316ac9b7
4 changed files with 377 additions and 0 deletions

View File

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

View File

@@ -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)

View 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()

View 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
}
})