@@ -3,19 +3,13 @@ import { describe, expect, it, vi } from "vitest"
33import { LGraph } from "@/litegraph"
44import { SubgraphNode } from "@/subgraph/SubgraphNode"
55
6- // Note: Avoiding createMemoryLeakTest as it relies on non-deterministic GC behavior
76import { subgraphTest } from "./fixtures/subgraphFixtures"
87import {
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-
1913describe ( "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
426154describe ( "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