mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
[fix] Remove FPS tracking to prevent memory leaks
- Remove recursive requestAnimationFrame loop that was causing memory leaks - Remove startFPSTracking, stopFPSTracking, and updateFPS functions - Remove FPS tracking variables and initialization - Refactor code structure with extracted helper functions for better maintainability The FPS tracking was only used for debugging and created an infinite RAF loop that accumulated memory over time in long-running sessions.
This commit is contained in:
@@ -130,41 +130,6 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
|||||||
nodesInIndex: 0
|
nodesInIndex: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// 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
|
// Update batching
|
||||||
const pendingUpdates = new Set<string>()
|
const pendingUpdates = new Set<string>()
|
||||||
const criticalUpdates = new Set<string>()
|
const criticalUpdates = new Set<string>()
|
||||||
@@ -242,39 +207,68 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
|||||||
return nodeRefs.get(id)
|
return nodeRefs.get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates Vue state when widget values change
|
||||||
|
*/
|
||||||
|
const updateVueWidgetState = (
|
||||||
|
nodeId: string,
|
||||||
|
widgetName: string,
|
||||||
|
value: unknown
|
||||||
|
): void => {
|
||||||
|
try {
|
||||||
|
const currentData = vueNodeData.get(nodeId)
|
||||||
|
if (!currentData?.widgets) return
|
||||||
|
|
||||||
|
const updatedWidgets = currentData.widgets.map((w) =>
|
||||||
|
w.name === widgetName ? { ...w, value: value } : w
|
||||||
|
)
|
||||||
|
vueNodeData.set(nodeId, {
|
||||||
|
...currentData,
|
||||||
|
widgets: updatedWidgets
|
||||||
|
})
|
||||||
|
performanceMetrics.callbackUpdateCount++
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore widget update errors to prevent cascade failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a wrapped callback for a widget that maintains LiteGraph/Vue sync
|
||||||
|
*/
|
||||||
|
const createWrappedWidgetCallback = (
|
||||||
|
widget: any,
|
||||||
|
originalCallback: ((value: unknown) => void) | undefined,
|
||||||
|
nodeId: string
|
||||||
|
) => {
|
||||||
|
return (value: unknown) => {
|
||||||
|
// 1. Update the widget value in LiteGraph (critical for LiteGraph state)
|
||||||
|
widget.value = value as string | number | boolean | object | undefined
|
||||||
|
|
||||||
|
// 2. Call the original callback if it exists
|
||||||
|
if (originalCallback) {
|
||||||
|
originalCallback.call(widget, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Update Vue state to maintain synchronization
|
||||||
|
updateVueWidgetState(nodeId, widget.name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up widget callbacks for a node - now with reduced nesting
|
||||||
|
*/
|
||||||
const setupNodeWidgetCallbacks = (node: LGraphNode) => {
|
const setupNodeWidgetCallbacks = (node: LGraphNode) => {
|
||||||
|
if (!node.widgets) return
|
||||||
|
|
||||||
const nodeId = String(node.id)
|
const nodeId = String(node.id)
|
||||||
|
|
||||||
node.widgets?.forEach((widget) => {
|
node.widgets.forEach((widget) => {
|
||||||
// Create a new callback that updates the widget value AND the Vue state
|
|
||||||
const originalCallback = widget.callback
|
const originalCallback = widget.callback
|
||||||
widget.callback = (value: unknown) => {
|
widget.callback = createWrappedWidgetCallback(
|
||||||
// 1. Update the widget value in LiteGraph
|
widget,
|
||||||
// Widget value can be various types, cast appropriately
|
originalCallback,
|
||||||
widget.value = value as string | number | boolean | object | undefined
|
nodeId
|
||||||
|
)
|
||||||
// 2. Call the original callback if it exists
|
|
||||||
if (originalCallback) {
|
|
||||||
originalCallback.call(widget, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Update Vue state
|
|
||||||
try {
|
|
||||||
const currentData = vueNodeData.get(nodeId)
|
|
||||||
if (currentData?.widgets) {
|
|
||||||
const updatedWidgets = currentData.widgets.map((w) =>
|
|
||||||
w.name === widget.name ? { ...w, value: value } : w
|
|
||||||
)
|
|
||||||
vueNodeData.set(nodeId, {
|
|
||||||
...currentData,
|
|
||||||
widgets: updatedWidgets
|
|
||||||
})
|
|
||||||
}
|
|
||||||
performanceMetrics.callbackUpdateCount++
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore widget update errors to prevent cascade failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,56 +409,61 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
|||||||
return visibleIds
|
return visibleIds
|
||||||
}
|
}
|
||||||
|
|
||||||
const detectChangesInRAF = () => {
|
/**
|
||||||
const startTime = performance.now()
|
* Detects position changes for a single node and updates reactive state
|
||||||
|
*/
|
||||||
|
const detectPositionChanges = (node: LGraphNode, id: string): boolean => {
|
||||||
|
const currentPos = nodePositions.get(id)
|
||||||
|
|
||||||
if (!graph?._nodes) return
|
if (
|
||||||
|
!currentPos ||
|
||||||
let positionUpdates = 0
|
currentPos.x !== node.pos[0] ||
|
||||||
let sizeUpdates = 0
|
currentPos.y !== node.pos[1]
|
||||||
|
) {
|
||||||
// Update reactive positions and sizes
|
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
|
||||||
for (const node of graph._nodes) {
|
return true
|
||||||
const id = String(node.id)
|
|
||||||
const currentPos = nodePositions.get(id)
|
|
||||||
const currentSize = nodeSizes.get(id)
|
|
||||||
|
|
||||||
let posChanged = false
|
|
||||||
let sizeChanged = false
|
|
||||||
|
|
||||||
if (
|
|
||||||
!currentPos ||
|
|
||||||
currentPos.x !== node.pos[0] ||
|
|
||||||
currentPos.y !== node.pos[1]
|
|
||||||
) {
|
|
||||||
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
|
|
||||||
positionUpdates++
|
|
||||||
posChanged = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!currentSize ||
|
|
||||||
currentSize.width !== node.size[0] ||
|
|
||||||
currentSize.height !== node.size[1]
|
|
||||||
) {
|
|
||||||
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
|
|
||||||
sizeUpdates++
|
|
||||||
sizeChanged = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update spatial index if position/size changed
|
|
||||||
if (posChanged || sizeChanged) {
|
|
||||||
const bounds: Bounds = {
|
|
||||||
x: node.pos[0],
|
|
||||||
y: node.pos[1],
|
|
||||||
width: node.size[0],
|
|
||||||
height: node.size[1]
|
|
||||||
}
|
|
||||||
spatialIndex.update(id, bounds)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Update performance metrics
|
/**
|
||||||
|
* Detects size changes for a single node and updates reactive state
|
||||||
|
*/
|
||||||
|
const detectSizeChanges = (node: LGraphNode, id: string): boolean => {
|
||||||
|
const currentSize = nodeSizes.get(id)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!currentSize ||
|
||||||
|
currentSize.width !== node.size[0] ||
|
||||||
|
currentSize.height !== node.size[1]
|
||||||
|
) {
|
||||||
|
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates spatial index for a node if bounds changed
|
||||||
|
*/
|
||||||
|
const updateSpatialIndex = (node: LGraphNode, id: string): void => {
|
||||||
|
const bounds: Bounds = {
|
||||||
|
x: node.pos[0],
|
||||||
|
y: node.pos[1],
|
||||||
|
width: node.size[0],
|
||||||
|
height: node.size[1]
|
||||||
|
}
|
||||||
|
spatialIndex.update(id, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates performance metrics after change detection
|
||||||
|
*/
|
||||||
|
const updatePerformanceMetrics = (
|
||||||
|
startTime: number,
|
||||||
|
positionUpdates: number,
|
||||||
|
sizeUpdates: number
|
||||||
|
): void => {
|
||||||
const endTime = performance.now()
|
const endTime = performance.now()
|
||||||
performanceMetrics.updateTime = endTime - startTime
|
performanceMetrics.updateTime = endTime - startTime
|
||||||
performanceMetrics.nodeCount = vueNodeData.size
|
performanceMetrics.nodeCount = vueNodeData.size
|
||||||
@@ -478,75 +477,117 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupEventListeners = (): (() => void) => {
|
/**
|
||||||
// Store original callbacks
|
* Main RAF change detection function - now simplified with extracted helpers
|
||||||
const originalOnNodeAdded = graph.onNodeAdded
|
*/
|
||||||
const originalOnNodeRemoved = graph.onNodeRemoved
|
const detectChangesInRAF = () => {
|
||||||
|
const startTime = performance.now()
|
||||||
|
|
||||||
// Override callbacks
|
if (!graph?._nodes) return
|
||||||
graph.onNodeAdded = (node: LGraphNode) => {
|
|
||||||
|
let positionUpdates = 0
|
||||||
|
let sizeUpdates = 0
|
||||||
|
|
||||||
|
// Process each node for changes
|
||||||
|
for (const node of graph._nodes) {
|
||||||
const id = String(node.id)
|
const id = String(node.id)
|
||||||
|
|
||||||
// Store non-reactive reference to original node
|
const posChanged = detectPositionChanges(node, id)
|
||||||
nodeRefs.set(id, node)
|
const sizeChanged = detectSizeChanges(node, id)
|
||||||
|
|
||||||
// Set up widget callbacks BEFORE extracting data
|
if (posChanged) positionUpdates++
|
||||||
setupNodeWidgetCallbacks(node)
|
if (sizeChanged) sizeUpdates++
|
||||||
|
|
||||||
// Extract safe data for Vue (now with proper callbacks)
|
// Update spatial index if geometry changed
|
||||||
vueNodeData.set(id, extractVueNodeData(node))
|
if (posChanged || sizeChanged) {
|
||||||
|
updateSpatialIndex(node, id)
|
||||||
// Set up reactive tracking
|
|
||||||
nodeState.set(id, {
|
|
||||||
visible: true,
|
|
||||||
dirty: false,
|
|
||||||
lastUpdate: performance.now(),
|
|
||||||
culled: false
|
|
||||||
})
|
|
||||||
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
|
|
||||||
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
|
|
||||||
attachMetadata(node)
|
|
||||||
|
|
||||||
// Add to spatial index
|
|
||||||
const bounds: Bounds = {
|
|
||||||
x: node.pos[0],
|
|
||||||
y: node.pos[1],
|
|
||||||
width: node.size[0],
|
|
||||||
height: node.size[1]
|
|
||||||
}
|
|
||||||
spatialIndex.insert(id, bounds, id)
|
|
||||||
|
|
||||||
if (originalOnNodeAdded) {
|
|
||||||
void originalOnNodeAdded(node)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
graph.onNodeRemoved = (node: LGraphNode) => {
|
updatePerformanceMetrics(startTime, positionUpdates, sizeUpdates)
|
||||||
const id = String(node.id)
|
}
|
||||||
|
|
||||||
// Remove from spatial index
|
/**
|
||||||
spatialIndex.remove(id)
|
* Handles node addition to the graph - sets up Vue state and spatial indexing
|
||||||
|
*/
|
||||||
|
const handleNodeAdded = (
|
||||||
|
node: LGraphNode,
|
||||||
|
originalCallback?: (node: LGraphNode) => void
|
||||||
|
) => {
|
||||||
|
const id = String(node.id)
|
||||||
|
|
||||||
nodeRefs.delete(id)
|
// Store non-reactive reference to original node
|
||||||
vueNodeData.delete(id)
|
nodeRefs.set(id, node)
|
||||||
nodeState.delete(id)
|
|
||||||
nodePositions.delete(id)
|
// Set up widget callbacks BEFORE extracting data (critical order)
|
||||||
nodeSizes.delete(id)
|
setupNodeWidgetCallbacks(node)
|
||||||
lastNodesSnapshot.delete(id)
|
|
||||||
originalOnNodeRemoved?.(node)
|
// Extract safe data for Vue (now with proper callbacks)
|
||||||
|
vueNodeData.set(id, extractVueNodeData(node))
|
||||||
|
|
||||||
|
// Set up reactive tracking state
|
||||||
|
nodeState.set(id, {
|
||||||
|
visible: true,
|
||||||
|
dirty: false,
|
||||||
|
lastUpdate: performance.now(),
|
||||||
|
culled: false
|
||||||
|
})
|
||||||
|
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
|
||||||
|
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
|
||||||
|
attachMetadata(node)
|
||||||
|
|
||||||
|
// Add to spatial index for viewport culling
|
||||||
|
const bounds: Bounds = {
|
||||||
|
x: node.pos[0],
|
||||||
|
y: node.pos[1],
|
||||||
|
width: node.size[0],
|
||||||
|
height: node.size[1]
|
||||||
}
|
}
|
||||||
|
spatialIndex.insert(id, bounds, id)
|
||||||
|
|
||||||
// Initial sync
|
// Call original callback if provided
|
||||||
syncWithGraph()
|
if (originalCallback) {
|
||||||
|
void originalCallback(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start FPS tracking
|
/**
|
||||||
startFPSTracking()
|
* Handles node removal from the graph - cleans up all references
|
||||||
|
*/
|
||||||
|
const handleNodeRemoved = (
|
||||||
|
node: LGraphNode,
|
||||||
|
originalCallback?: (node: LGraphNode) => void
|
||||||
|
) => {
|
||||||
|
const id = String(node.id)
|
||||||
|
|
||||||
// Return cleanup function
|
// Remove from spatial index
|
||||||
|
spatialIndex.remove(id)
|
||||||
|
|
||||||
|
// Clean up all tracking references
|
||||||
|
nodeRefs.delete(id)
|
||||||
|
vueNodeData.delete(id)
|
||||||
|
nodeState.delete(id)
|
||||||
|
nodePositions.delete(id)
|
||||||
|
nodeSizes.delete(id)
|
||||||
|
lastNodesSnapshot.delete(id)
|
||||||
|
|
||||||
|
// Call original callback if provided
|
||||||
|
if (originalCallback) {
|
||||||
|
originalCallback(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates cleanup function for event listeners and state
|
||||||
|
*/
|
||||||
|
const createCleanupFunction = (
|
||||||
|
originalOnNodeAdded: ((node: LGraphNode) => void) | undefined,
|
||||||
|
originalOnNodeRemoved: ((node: LGraphNode) => void) | undefined
|
||||||
|
) => {
|
||||||
return () => {
|
return () => {
|
||||||
// Restore original callbacks
|
// Restore original callbacks
|
||||||
graph.onNodeAdded = originalOnNodeAdded
|
graph.onNodeAdded = originalOnNodeAdded || undefined
|
||||||
graph.onNodeRemoved = originalOnNodeRemoved
|
graph.onNodeRemoved = originalOnNodeRemoved || undefined
|
||||||
|
|
||||||
// Clear pending updates
|
// Clear pending updates
|
||||||
if (batchTimeoutId !== null) {
|
if (batchTimeoutId !== null) {
|
||||||
@@ -554,10 +595,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
|||||||
batchTimeoutId = null
|
batchTimeoutId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop FPS tracking
|
// Clear all state maps
|
||||||
stopFPSTracking()
|
|
||||||
|
|
||||||
// Clear state
|
|
||||||
nodeRefs.clear()
|
nodeRefs.clear()
|
||||||
vueNodeData.clear()
|
vueNodeData.clear()
|
||||||
nodeState.clear()
|
nodeState.clear()
|
||||||
@@ -571,6 +609,33 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up event listeners - now simplified with extracted handlers
|
||||||
|
*/
|
||||||
|
const setupEventListeners = (): (() => void) => {
|
||||||
|
// Store original callbacks
|
||||||
|
const originalOnNodeAdded = graph.onNodeAdded
|
||||||
|
const originalOnNodeRemoved = graph.onNodeRemoved
|
||||||
|
|
||||||
|
// Set up graph event handlers
|
||||||
|
graph.onNodeAdded = (node: LGraphNode) => {
|
||||||
|
handleNodeAdded(node, originalOnNodeAdded)
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.onNodeRemoved = (node: LGraphNode) => {
|
||||||
|
handleNodeRemoved(node, originalOnNodeRemoved)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize state
|
||||||
|
syncWithGraph()
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return createCleanupFunction(
|
||||||
|
originalOnNodeAdded || undefined,
|
||||||
|
originalOnNodeRemoved || undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Set up event listeners immediately
|
// Set up event listeners immediately
|
||||||
const cleanup = setupEventListeners()
|
const cleanup = setupEventListeners()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user