@@ -92,62 +92,48 @@ class JSClosureTests: XCTestCase {
9292 throw XCTSkip ( " Missing --expose-gc flag " )
9393 }
9494
95- // Step 1: Create many JSClosure instances
95+ // Step 1: Create many source closures and keep only JS references alive.
96+ // These closures must remain callable even after heavy finalizer churn.
9697 let obj = JSObject ( )
97- var closurePointers : Set < UInt32 > = [ ]
9898 let numberOfSourceClosures = 10_000
9999
100100 do {
101101 var closures : [ JSClosure ] = [ ]
102102 for i in 0 ..< numberOfSourceClosures {
103- let closure = JSClosure { _ in . undefined }
103+ let closure = JSClosure { _ in . number ( Double ( i ) ) }
104104 obj [ " c \( i) " ] = closure. jsValue
105105 closures. append ( closure)
106- // Store
107- closurePointers. insert ( UInt32 ( UInt ( bitPattern: Unmanaged . passUnretained ( closure) . toOpaque ( ) ) ) )
108-
109- // To avoid all JSClosures having a common address diffs, randomly allocate a new object.
110- if Bool . random ( ) {
111- _ = JSObject ( )
112- }
113- }
114- }
115-
116- // Step 2: Create many JSObject to make JSObject.id close to Swift heap object address
117- let minClosurePointer = closurePointers. min ( ) ?? 0
118- let maxClosurePointer = closurePointers. max ( ) ?? 0
119- while true {
120- let obj = JSObject ( )
121- if minClosurePointer == obj. id {
122- break
123106 }
124107 }
125108
126- // Step 3: Create JSClosure instances and find the one with JSClosure.id == &closurePointers[x]
109+ // Step 2: Create many temporary objects/closures to stress ID reuse and finalizer paths.
110+ // Under the optimized object heap, IDs are aggressively reused, so this should exercise
111+ // the same misdeallocation surface without relying on monotonic ID growth.
127112 do {
128- while true {
129- let c = JSClosure { _ in . undefined }
130- if closurePointers. contains ( c. id) || c. id > maxClosurePointer {
131- break
113+ let numberOfProbeClosures = 50_000
114+ for i in 0 ..< numberOfProbeClosures {
115+ let tempClosure = JSClosure { _ in . number( Double ( i) ) }
116+ if i % 3 == 0 {
117+ let tempObject = JSObject ( )
118+ tempObject [ " probe " ] = tempClosure. jsValue
132119 }
133- // To avoid all JSClosures having a common JSObject.id diffs, randomly allocate a new JS object.
134- if Bool . random ( ) {
120+ if i % 7 == 0 {
135121 _ = JSObject ( )
136122 }
137123 }
138124 }
139125
140- // Step 4 : Trigger garbage collection to call the finalizer of the conflicting JSClosure instance
126+ // Step 3 : Trigger garbage collection to run finalizers for temporary closures.
141127 for _ in 0 ..< 100 {
142128 gc ( )
143129 // Tick the event loop to allow the garbage collector to run finalizers
144130 // registered by FinalizationRegistry.
145131 try await Task . sleep ( for: . milliseconds( 0 ) )
146132 }
147133
148- // Step 5 : Verify that the JSClosure instances are still alive and can be called
134+ // Step 4 : Verify source closures are still alive and correct.
149135 for i in 0 ..< numberOfSourceClosures {
150- _ = obj [ " c \( i) " ] . function!( )
136+ XCTAssertEqual ( obj [ " c \( i) " ] . function!( ) , . number ( Double ( i ) ) )
151137 }
152138 }
153139}
0 commit comments