[feat] Implement callback-driven widget updates

- Chain widget callbacks to trigger immediate Vue state updates
- Remove need for RAF polling of widget values
- Add performance tracking for callback vs RAF updates
- Implement proper FPS tracking with 1-second intervals

This change makes widget updates reactive and immediate rather than
waiting for the next RAF cycle, improving responsiveness.
This commit is contained in:
bymyself
2025-07-02 22:14:58 -07:00
parent 222a52d347
commit 124db5991f

View File

@@ -4,6 +4,7 @@
*/
import type { LGraph, LGraphNode } from '@comfyorg/litegraph'
import { nextTick, reactive, readonly } from 'vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
export interface NodeState {
visible: boolean
@@ -20,10 +21,13 @@ export interface NodeMetadata {
}
export interface PerformanceMetrics {
fps: number
frameTime: number
updateTime: number
nodeCount: number
culledCount: number
callbackUpdateCount: number
rafUpdateCount: number
adaptiveQuality: boolean
}
@@ -92,13 +96,51 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
// Performance tracking
const performanceMetrics = reactive<PerformanceMetrics>({
fps: 0,
frameTime: 0,
updateTime: 0,
nodeCount: 0,
culledCount: 0,
callbackUpdateCount: 0,
rafUpdateCount: 0,
adaptiveQuality: false
})
// FPS tracking state
let lastFrameTime = performance.now()
let frameCount = 0
let fpsUpdateInterval: number | null = null
const updateFPS = () => {
frameCount++
const now = performance.now()
const elapsed = now - lastFrameTime
if (elapsed >= 1000) {
performanceMetrics.fps = Math.round((frameCount * 1000) / elapsed)
performanceMetrics.frameTime = elapsed / frameCount
frameCount = 0
lastFrameTime = now
}
}
const startFPSTracking = () => {
if (fpsUpdateInterval) return
const trackFrame = () => {
updateFPS()
fpsUpdateInterval = requestAnimationFrame(trackFrame)
}
fpsUpdateInterval = requestAnimationFrame(trackFrame)
}
const stopFPSTracking = () => {
if (fpsUpdateInterval) {
cancelAnimationFrame(fpsUpdateInterval)
fpsUpdateInterval = null
}
}
// Update batching
const pendingUpdates = new Set<string>()
const criticalUpdates = new Set<string>()
@@ -126,6 +168,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
// Extract safe widget data
const safeWidgets = node.widgets?.map((widget) => {
try {
// TODO: Use widget.getReactiveData() once TypeScript types are updated
let value = widget.value
// For combo widgets, if value is undefined, use the first option as default
@@ -181,6 +224,32 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
return nodeRefs.get(id)
}
const setupNodeWidgetCallbacks = (node: LGraphNode) => {
const nodeId = String(node.id)
node.widgets?.forEach(widget => {
widget.callback = useChainCallback(widget.callback, () => {
try {
const currentData = vueNodeData.get(nodeId)
if (currentData?.widgets) {
const updatedWidgets = currentData.widgets.map(w =>
w.name === widget.name
? { ...w, value: widget.value }
: w
)
vueNodeData.set(nodeId, {
...currentData,
widgets: updatedWidgets
})
}
performanceMetrics.callbackUpdateCount++
} catch (error) {
console.warn(`[useGraphNodeManager] Failed to update Vue state for widget ${widget.name}:`, error)
}
})
})
}
// Uncomment when needed for future features
// const getNodeMetadata = (node: LGraphNode): NodeMetadata => {
// let metadata = nodeMetadata.get(node)
@@ -295,8 +364,13 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
// Most performant: Direct position sync without re-setting entire node
const detectChangesInRAF = () => {
const startTime = performance.now()
if (!graph?._nodes) return
let positionUpdates = 0
let sizeUpdates = 0
// Update reactive positions and sizes
for (const node of graph._nodes) {
const id = String(node.id)
@@ -309,6 +383,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
currentPos.y !== node.pos[1]
) {
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
positionUpdates++
}
if (
@@ -317,12 +392,21 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
currentSize.height !== node.size[1]
) {
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
sizeUpdates++
}
}
// Update performance metrics
performanceMetrics.frameTime = performance.now()
performanceMetrics.updateTime++
const endTime = performance.now()
performanceMetrics.updateTime = endTime - startTime
performanceMetrics.nodeCount = vueNodeData.size
performanceMetrics.culledCount = Array.from(nodeState.values()).filter(
state => state.culled
).length
if (positionUpdates > 0 || sizeUpdates > 0) {
performanceMetrics.rafUpdateCount++
}
}
const setupEventListeners = (): (() => void) => {
@@ -351,6 +435,9 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
attachMetadata(node)
setupNodeWidgetCallbacks(node)
if (originalOnNodeAdded) {
void originalOnNodeAdded(node)
}
@@ -369,6 +456,9 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
// Initial sync
syncWithGraph()
// Start FPS tracking
startFPSTracking()
// Return cleanup function
return () => {
@@ -381,6 +471,9 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
clearTimeout(batchTimeoutId)
batchTimeoutId = null
}
// Stop FPS tracking
stopFPSTracking()
// Clear state
nodeRefs.clear()