[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
})
// 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>()
@@ -242,15 +207,41 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
return nodeRefs.get(id)
}
const setupNodeWidgetCallbacks = (node: LGraphNode) => {
const nodeId = String(node.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
node.widgets?.forEach((widget) => {
// Create a new callback that updates the widget value AND the Vue state
const originalCallback = widget.callback
widget.callback = (value: unknown) => {
// 1. Update the widget value in LiteGraph
// Widget value can be various types, cast appropriately
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
@@ -258,23 +249,26 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
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
// 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) => {
if (!node.widgets) return
const nodeId = String(node.id)
node.widgets.forEach((widget) => {
const originalCallback = widget.callback
widget.callback = createWrappedWidgetCallback(
widget,
originalCallback,
nodeId
)
vueNodeData.set(nodeId, {
...currentData,
widgets: updatedWidgets
})
}
performanceMetrics.callbackUpdateCount++
} catch (error) {
// Ignore widget update errors to prevent cascade failures
}
}
})
}
@@ -415,22 +409,11 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
return visibleIds
}
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)
/**
* Detects position changes for a single node and updates reactive state
*/
const detectPositionChanges = (node: LGraphNode, id: string): boolean => {
const currentPos = nodePositions.get(id)
const currentSize = nodeSizes.get(id)
let posChanged = false
let sizeChanged = false
if (
!currentPos ||
@@ -438,9 +421,16 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
currentPos.y !== node.pos[1]
) {
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
positionUpdates++
posChanged = true
return true
}
return false
}
/**
* 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 ||
@@ -448,12 +438,15 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
currentSize.height !== node.size[1]
) {
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
sizeUpdates++
sizeChanged = true
return true
}
return false
}
// Update spatial index if position/size changed
if (posChanged || sizeChanged) {
/**
* 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],
@@ -462,9 +455,15 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
}
spatialIndex.update(id, bounds)
}
}
// Update performance metrics
/**
* Updates performance metrics after change detection
*/
const updatePerformanceMetrics = (
startTime: number,
positionUpdates: number,
sizeUpdates: number
): void => {
const endTime = performance.now()
performanceMetrics.updateTime = endTime - startTime
performanceMetrics.nodeCount = vueNodeData.size
@@ -478,25 +477,55 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
}
}
const setupEventListeners = (): (() => void) => {
// Store original callbacks
const originalOnNodeAdded = graph.onNodeAdded
const originalOnNodeRemoved = graph.onNodeRemoved
/**
* Main RAF change detection function - now simplified with extracted helpers
*/
const detectChangesInRAF = () => {
const startTime = performance.now()
// Override callbacks
graph.onNodeAdded = (node: LGraphNode) => {
if (!graph?._nodes) return
let positionUpdates = 0
let sizeUpdates = 0
// Process each node for changes
for (const node of graph._nodes) {
const id = String(node.id)
const posChanged = detectPositionChanges(node, id)
const sizeChanged = detectSizeChanges(node, id)
if (posChanged) positionUpdates++
if (sizeChanged) sizeUpdates++
// Update spatial index if geometry changed
if (posChanged || sizeChanged) {
updateSpatialIndex(node, id)
}
}
updatePerformanceMetrics(startTime, positionUpdates, sizeUpdates)
}
/**
* 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)
// Store non-reactive reference to original node
nodeRefs.set(id, node)
// Set up widget callbacks BEFORE extracting data
// Set up widget callbacks BEFORE extracting data (critical order)
setupNodeWidgetCallbacks(node)
// Extract safe data for Vue (now with proper callbacks)
vueNodeData.set(id, extractVueNodeData(node))
// Set up reactive tracking
// Set up reactive tracking state
nodeState.set(id, {
visible: true,
dirty: false,
@@ -507,7 +536,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
attachMetadata(node)
// Add to spatial index
// Add to spatial index for viewport culling
const bounds: Bounds = {
x: node.pos[0],
y: node.pos[1],
@@ -516,37 +545,49 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
}
spatialIndex.insert(id, bounds, id)
if (originalOnNodeAdded) {
void originalOnNodeAdded(node)
// Call original callback if provided
if (originalCallback) {
void originalCallback(node)
}
}
graph.onNodeRemoved = (node: LGraphNode) => {
/**
* Handles node removal from the graph - cleans up all references
*/
const handleNodeRemoved = (
node: LGraphNode,
originalCallback?: (node: LGraphNode) => void
) => {
const id = String(node.id)
// 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)
originalOnNodeRemoved?.(node)
// Call original callback if provided
if (originalCallback) {
originalCallback(node)
}
}
// Initial sync
syncWithGraph()
// Start FPS tracking
startFPSTracking()
// Return cleanup function
/**
* Creates cleanup function for event listeners and state
*/
const createCleanupFunction = (
originalOnNodeAdded: ((node: LGraphNode) => void) | undefined,
originalOnNodeRemoved: ((node: LGraphNode) => void) | undefined
) => {
return () => {
// Restore original callbacks
graph.onNodeAdded = originalOnNodeAdded
graph.onNodeRemoved = originalOnNodeRemoved
graph.onNodeAdded = originalOnNodeAdded || undefined
graph.onNodeRemoved = originalOnNodeRemoved || undefined
// Clear pending updates
if (batchTimeoutId !== null) {
@@ -554,10 +595,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
batchTimeoutId = null
}
// Stop FPS tracking
stopFPSTracking()
// Clear state
// Clear all state maps
nodeRefs.clear()
vueNodeData.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
const cleanup = setupEventListeners()