[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:
bymyself
2025-07-05 00:01:13 -07:00
parent a23d8be77b
commit c3023e46d9

View File

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