Skip to content

Commit fcfcb58

Browse files
[test] Clean up SubgraphMemory tests to remove invalid assertions
Removed tests that were not actually testing functionality: - Placeholder tests with expect(true).toBe(true) - Documentation-only tests trying to "prove" memory leaks without GC - Tests with console.log/warn statements - Tests that documented solutions rather than testing behavior Kept valid tests that verify actual behavior: - Event listener registration and cleanup - Widget reference management - Multiple instance handling - Subgraph reference management Fixes #1132
1 parent 2786527 commit fcfcb58

File tree

1 file changed

+3
-285
lines changed

1 file changed

+3
-285
lines changed

test/subgraph/SubgraphMemory.test.ts

Lines changed: 3 additions & 285 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,13 @@ import { describe, expect, it, vi } from "vitest"
33
import { LGraph } from "@/litegraph"
44
import { SubgraphNode } from "@/subgraph/SubgraphNode"
55

6-
// Note: Avoiding createMemoryLeakTest as it relies on non-deterministic GC behavior
76
import { subgraphTest } from "./fixtures/subgraphFixtures"
87
import {
98
createEventCapture,
109
createTestSubgraph,
1110
createTestSubgraphNode,
1211
} from "./fixtures/subgraphHelpers"
1312

14-
// Mock WeakRef to detect if objects can be garbage collected
15-
declare global {
16-
const gc: (() => void) | undefined
17-
}
18-
1913
describe("SubgraphNode Memory Management", () => {
2014
describe("Event Listener Cleanup", () => {
2115
it("should register event listeners on construction", () => {
@@ -40,7 +34,7 @@ describe("SubgraphNode Memory Management", () => {
4034
expect(eventTypes).toContain("renaming-output")
4135
})
4236

43-
it("CRITICAL: should clean up input listeners on removal", () => {
37+
it("should clean up input listeners on removal", () => {
4438
const subgraph = createTestSubgraph({
4539
inputs: [{ name: "input1", type: "number" }],
4640
})
@@ -57,31 +51,6 @@ describe("SubgraphNode Memory Management", () => {
5751
expect(subgraphNode.inputs[0]._listenerController?.signal.aborted).toBe(true)
5852
})
5953

60-
it("CRITICAL: main subgraph event listeners are NOT cleaned up (MEMORY LEAK)", () => {
61-
const subgraph = createTestSubgraph()
62-
63-
// Track listener registration
64-
const removeEventSpy = vi.spyOn(subgraph.events, "removeEventListener")
65-
const addEventSpy = vi.spyOn(subgraph.events, "addEventListener")
66-
const initialAddCalls = addEventSpy.mock.calls.length
67-
68-
const subgraphNode = createTestSubgraphNode(subgraph)
69-
const addCallsAfterCreate = addEventSpy.mock.calls.length
70-
71-
// Verify listeners were added
72-
expect(addCallsAfterCreate).toBeGreaterThan(initialAddCalls)
73-
74-
// Call onRemoved
75-
subgraphNode.onRemoved()
76-
77-
// CRITICAL BUG: Main subgraph event listeners are NOT removed
78-
// onRemoved only cleans up input-specific listeners, not the main ones
79-
expect(removeEventSpy).not.toHaveBeenCalled()
80-
81-
// This proves the memory leak exists
82-
console.warn("MEMORY LEAK CONFIRMED: SubgraphNode event listeners not cleaned up on removal")
83-
})
84-
8554
it("should not accumulate listeners during reconfiguration", () => {
8655
const subgraph = createTestSubgraph({
8756
inputs: [{ name: "input1", type: "number" }],
@@ -111,135 +80,6 @@ describe("SubgraphNode Memory Management", () => {
11180
const finalCalls = addEventSpy.mock.calls.length
11281
expect(finalCalls).toBe(initialCalls) // Main listeners not re-added
11382
})
114-
115-
it("should demonstrate memory leak with multiple instances (limited scope)", () => {
116-
const subgraph = createTestSubgraph()
117-
118-
// Track total listener count
119-
const addEventSpy = vi.spyOn(subgraph.events, "addEventListener")
120-
let totalListenersAdded = 0
121-
122-
// Create and "remove" multiple instances (limited to 3 for test performance)
123-
const instances: SubgraphNode[] = []
124-
125-
for (let i = 0; i < 3; i++) {
126-
const initialCalls = addEventSpy.mock.calls.length
127-
const instance = createTestSubgraphNode(subgraph, { id: i })
128-
instances.push(instance)
129-
130-
const callsAfterCreate = addEventSpy.mock.calls.length
131-
totalListenersAdded += (callsAfterCreate - initialCalls)
132-
133-
// Simulate removal (but listeners won't be cleaned up)
134-
instance.onRemoved()
135-
}
136-
137-
// All listeners are still registered even though nodes are "removed"
138-
expect(totalListenersAdded).toBeGreaterThan(0)
139-
140-
// Document the leak without excessive accumulation
141-
console.warn(`Memory leak demonstrated: ${totalListenersAdded} listeners accumulated from ${instances.length} instances`)
142-
143-
// IMPORTANT: This test intentionally creates a small memory leak to demonstrate the bug.
144-
// In production, this would accumulate over time and cause performance issues.
145-
// The leak is limited to 3 instances to minimize test suite impact.
146-
})
147-
})
148-
149-
describe("Memory Leak Prevention Tests", () => {
150-
it("verifies proper cleanup workflow exists", () => {
151-
// This test documents the expected cleanup workflow without relying on GC
152-
const subgraph = createTestSubgraph()
153-
const subgraphNode = createTestSubgraphNode(subgraph)
154-
155-
// Track cleanup state
156-
const cleanupState = {
157-
inputListenersCleanedUp: false,
158-
mainListenersCleanedUp: false,
159-
widgetReferencesCleared: false,
160-
}
161-
162-
// Check if input listeners exist and are set up
163-
const input = subgraphNode.inputs[0]
164-
165-
// Call cleanup
166-
subgraphNode.onRemoved()
167-
168-
// Verify what gets cleaned up
169-
if (input && "_listenerController" in input && (input as any)._listenerController) {
170-
cleanupState.inputListenersCleanedUp = (input as any)._listenerController.signal.aborted === true
171-
expect(cleanupState.inputListenersCleanedUp).toBe(true) // This works when input exists
172-
} else {
173-
// If no input or no listener controller, that's also valid
174-
cleanupState.inputListenersCleanedUp = true
175-
expect(cleanupState.inputListenersCleanedUp).toBe(true)
176-
}
177-
178-
// TODO: These should be true after proper implementation
179-
// expect(cleanupState.mainListenersCleanedUp).toBe(true)
180-
// expect(cleanupState.widgetReferencesCleared).toBe(true)
181-
182-
// This test serves as documentation for what needs to be implemented
183-
})
184-
185-
it("should clean up widget references properly", () => {
186-
const subgraph = createTestSubgraph({
187-
inputs: [{ name: "input1", type: "number" }],
188-
})
189-
const subgraphNode = createTestSubgraphNode(subgraph)
190-
191-
// Simulate widget promotion
192-
const input = subgraphNode.inputs[0]
193-
input._widget = {
194-
type: "number",
195-
name: "test_widget",
196-
value: 42,
197-
draw: vi.fn(),
198-
mouse: vi.fn(),
199-
computeSize: vi.fn(),
200-
createCopyForNode: vi.fn(),
201-
}
202-
203-
input.widget = { name: "test_widget" }
204-
205-
expect(input._widget).toBeDefined()
206-
expect(input.widget).toBeDefined()
207-
208-
// Removal should clean up widget references
209-
subgraphNode.onRemoved()
210-
211-
// Input-specific listeners are cleaned up, but widget refs may remain
212-
// This tests the current behavior
213-
expect(input._listenerController?.signal.aborted).toBe(true)
214-
})
215-
216-
it("should handle rapid creation/destruction cycles", () => {
217-
const subgraph = createTestSubgraph()
218-
219-
// Simulate rapid creation/destruction that might happen in UI
220-
const instances: SubgraphNode[] = []
221-
222-
for (let cycle = 0; cycle < 10; cycle++) {
223-
// Create instance
224-
const instance = createTestSubgraphNode(subgraph, { id: cycle })
225-
instances.push(instance)
226-
227-
// Add to graph
228-
const parentGraph = new LGraph()
229-
parentGraph.add(instance)
230-
231-
// Remove from graph
232-
parentGraph.remove(instance)
233-
instance.onRemoved()
234-
}
235-
236-
// All instances have been "removed" but event listeners remain
237-
// This demonstrates the accumulation problem
238-
expect(instances).toHaveLength(10)
239-
240-
// Each instance still holds references through event listeners
241-
// In a real app, this would cause memory growth over time
242-
})
24383
})
24484

24585
describe("Widget Promotion Memory Management", () => {
@@ -279,9 +119,6 @@ describe("SubgraphNode Memory Management", () => {
279119

280120
// Widget should be removed from array
281121
expect(subgraphNode.widgets).not.toContain(mockWidget)
282-
283-
// References should be cleaned up (testing current implementation)
284-
// Note: The PR #1107 fix should clean these up
285122
})
286123

287124
it("should not leak widgets during reconfiguration", () => {
@@ -312,115 +149,6 @@ describe("SubgraphNode Memory Management", () => {
312149
expect(subgraphNode.widgets.length).toBe(initialWidgetCount)
313150
})
314151
})
315-
316-
describe("Memory Leak Documentation", () => {
317-
it("documents the known memory leak pattern", () => {
318-
// This test documents the exact memory leak pattern for future fixes
319-
320-
const subgraph = createTestSubgraph()
321-
const eventCapture = createEventCapture(subgraph.events, [
322-
"input-added",
323-
"output-added",
324-
"removing-input",
325-
"removing-output",
326-
"renaming-input",
327-
"renaming-output",
328-
])
329-
330-
// Create SubgraphNode - this registers 6 event listeners
331-
const subgraphNode = createTestSubgraphNode(subgraph)
332-
333-
// The memory leak occurs here:
334-
// 1. SubgraphNode constructor (lines 52-92) adds event listeners
335-
// 2. These create strong references: subgraph.events -> SubgraphNode
336-
// 3. onRemoved() method (lines 302-306) only cleans input listeners
337-
// 4. Main subgraph event listeners are NEVER removed
338-
339-
expect(subgraphNode).toBeDefined()
340-
341-
// Cleanup test resources
342-
eventCapture.cleanup()
343-
344-
console.log("Memory leak pattern documented: SubgraphNode event listeners persist after removal")
345-
})
346-
347-
it("provides solution blueprint for memory leak fix", () => {
348-
// This test shows what the fix should look like
349-
350-
const subgraph = createTestSubgraph()
351-
352-
// SOLUTION: SubgraphNode should use AbortController pattern
353-
//
354-
// In constructor:
355-
// this.eventAbortController = new AbortController()
356-
// const signal = this.eventAbortController.signal
357-
// subgraphEvents.addEventListener("input-added", handler, { signal })
358-
//
359-
// In onRemoved():
360-
// this.eventAbortController.abort() // Removes ALL listeners
361-
362-
const subgraphNode = createTestSubgraphNode(subgraph)
363-
364-
// Current implementation only cleans input listeners
365-
subgraphNode.onRemoved()
366-
367-
// TODO: Implement AbortController pattern to fix memory leak
368-
expect(true).toBe(true) // Placeholder - documents intended solution
369-
})
370-
})
371-
})
372-
373-
describe("Performance Impact of Memory Leak", () => {
374-
it("measures event handler overhead with multiple instances", () => {
375-
const subgraph = createTestSubgraph()
376-
377-
// Create multiple instances (reduced from 50 to 20 for test efficiency)
378-
const instances: SubgraphNode[] = []
379-
const startTime = performance.now()
380-
381-
for (let i = 0; i < 20; i++) {
382-
instances.push(createTestSubgraphNode(subgraph, { id: i }))
383-
}
384-
385-
const creationTime = performance.now() - startTime
386-
387-
// Trigger an event that will call ALL accumulated listeners
388-
const eventStartTime = performance.now()
389-
subgraph.addInput("performance_test", "number")
390-
const eventTime = performance.now() - eventStartTime
391-
392-
console.log(`Created ${instances.length} instances in ${creationTime.toFixed(2)}ms`)
393-
console.log(`Event dispatch took ${eventTime.toFixed(2)}ms (${instances.length} listeners)`)
394-
395-
// More instances = more event listeners = slower event handling
396-
// This demonstrates the performance impact of the memory leak
397-
expect(eventTime).toBeGreaterThan(0)
398-
})
399-
400-
it("demonstrates listener accumulation impact", () => {
401-
// Test with different numbers of instances (reduced scale for efficiency)
402-
const testCases = [5, 10, 15]
403-
404-
for (const instanceCount of testCases) {
405-
// Clean test - create fresh subgraph
406-
const testSubgraph = createTestSubgraph()
407-
408-
// Create instances
409-
for (let i = 0; i < instanceCount; i++) {
410-
createTestSubgraphNode(testSubgraph, { id: i })
411-
}
412-
413-
// Measure event dispatch time
414-
const startTime = performance.now()
415-
testSubgraph.addInput(`test_${instanceCount}`, "number")
416-
const dispatchTime = performance.now() - startTime
417-
418-
console.log(`${instanceCount} instances: ${dispatchTime.toFixed(3)}ms event dispatch`)
419-
}
420-
421-
// This test documents that more instances = more listeners = slower events
422-
expect(true).toBe(true)
423-
})
424152
})
425153

426154
describe("SubgraphMemory - Event Listener Management", () => {
@@ -522,9 +250,6 @@ describe("SubgraphMemory - Reference Management", () => {
522250
const subgraph = createTestSubgraph({ nodeCount: 2 })
523251
const subgraphNode = createTestSubgraphNode(subgraph)
524252

525-
// Before adding to graph, node might already have a graph reference
526-
// (This depends on how createTestSubgraphNode works)
527-
528253
// Add to graph
529254
rootGraph.add(subgraphNode)
530255
expect(subgraphNode.graph).toBe(rootGraph)
@@ -533,9 +258,6 @@ describe("SubgraphMemory - Reference Management", () => {
533258
// Remove from graph
534259
rootGraph.remove(subgraphNode)
535260
expect(rootGraph.nodes).not.toContain(subgraphNode)
536-
537-
// After removal, graph reference behavior may vary by implementation
538-
// The important thing is that it's removed from the graph's nodes array
539261
})
540262

541263
it("prevents circular reference creation", () => {
@@ -546,7 +268,6 @@ describe("SubgraphMemory - Reference Management", () => {
546268
expect(subgraph.nodes).not.toContain(subgraphNode)
547269

548270
// If circular references were attempted, they should be detected
549-
// (This documents the expected behavior - implementation may vary)
550271
expect(subgraphNode.subgraph).toBe(subgraph)
551272
expect(subgraph.nodes.includes(subgraphNode)).toBe(false)
552273
})
@@ -630,9 +351,6 @@ describe("SubgraphMemory - Widget Reference Management", () => {
630351
if (input && "_listenerController" in input) {
631352
expect((input as any)._listenerController?.signal.aborted).toBe(true)
632353
}
633-
634-
// Note: Other references may still exist - this documents current behavior
635-
// In a proper implementation, onRemoved should clean these up too
636354
})
637355
})
638356

@@ -667,7 +385,7 @@ describe("SubgraphMemory - Performance and Scale", () => {
667385
const rootGraph = new LGraph()
668386
const instances = []
669387

670-
// Create instances (reduced from 50 to 25 for test efficiency)
388+
// Create instances
671389
for (let i = 0; i < 25; i++) {
672390
const instance = createTestSubgraphNode(subgraph)
673391
rootGraph.add(instance)
@@ -709,4 +427,4 @@ describe("SubgraphMemory - Performance and Scale", () => {
709427
expect(rootGraph.nodes.length).toBe(0)
710428
}
711429
})
712-
})
430+
})

0 commit comments

Comments
 (0)