1
+ import JavaScriptKit
2
+ import XCTest
3
+
4
+ final class StressTests : XCTestCase {
5
+
6
+ func testJSObjectMemoryExhaustion( ) async throws {
7
+ guard let gc = JSObject . global. gc. function else {
8
+ throw XCTSkip ( " Missing --expose-gc flag " )
9
+ }
10
+
11
+ // Push JSObject allocation to stress memory management
12
+ // This tests reference counting and cleanup under heavy load
13
+ let maxIterations = 25_000
14
+ var objects : [ JSObject ] = [ ]
15
+ var lastSuccessfulCount = 0
16
+
17
+ do {
18
+ for i in 0 ..< maxIterations {
19
+ let obj = JSObject ( )
20
+ // Add properties to increase memory pressure
21
+ obj [ " index " ] = JSValue . number ( Double ( i) )
22
+ obj [ " data " ] = JSValue . string ( String ( repeating: " x " , count: 1000 ) ) // 1KB string per object
23
+
24
+ // Create nested objects to stress the reference graph
25
+ let nested = JSObject ( )
26
+ nested [ " parent_ref " ] = obj. jsValue // Circular reference
27
+ obj [ " nested " ] = nested. jsValue
28
+
29
+ objects. append ( obj)
30
+ lastSuccessfulCount = i
31
+
32
+ // Aggressive GC every 1000 objects to test cleanup under pressure
33
+ if i % 1000 == 0 {
34
+ gc ( )
35
+ try await Task . sleep ( for: . milliseconds( 0 ) )
36
+ }
37
+ }
38
+ } catch {
39
+ // Expected to eventually fail due to memory pressure
40
+ print ( " JSObject stress test stopped at \( lastSuccessfulCount) objects: \( error) " )
41
+ }
42
+
43
+ // Verify objects are still accessible after memory pressure
44
+ let sampleCount = min ( 1000 , objects. count)
45
+ for i in 0 ..< sampleCount {
46
+ XCTAssertEqual ( objects [ i] [ " index " ] , JSValue . number ( Double ( i) ) )
47
+ XCTAssertNotNil ( objects [ i] [ " nested " ] . object)
48
+ }
49
+
50
+ // Force cleanup
51
+ objects. removeAll ( )
52
+ for _ in 0 ..< 20 {
53
+ gc ( )
54
+ try await Task . sleep ( for: . milliseconds( 10 ) )
55
+ }
56
+ }
57
+
58
+ func testJSClosureMemoryPressureWithoutFinalizationRegistry( ) async throws {
59
+ guard let gc = JSObject . global. gc. function else {
60
+ throw XCTSkip ( " Missing --expose-gc flag " )
61
+ }
62
+
63
+ // Test heavy closure allocation to stress Swift heap management
64
+ // Focus on scenarios where FinalizationRegistry is not used
65
+ let maxClosures = 15_000
66
+ var closures : [ JSClosure ] = [ ]
67
+ var successCount = 0
68
+
69
+ do {
70
+ for i in 0 ..< maxClosures {
71
+ // Create closures that capture significant data
72
+ let capturedData = Array ( 0 ..< 100 ) . map { " item_ \( $0) _ \( i) " }
73
+ let closure = JSClosure { arguments in
74
+ // Force usage of captured data to prevent optimization
75
+ let result = capturedData. count + Int( arguments. first? . number ?? 0 )
76
+ return JSValue . number ( Double ( result) )
77
+ }
78
+
79
+ closures. append ( closure)
80
+ successCount = i + 1
81
+
82
+ // Test closure immediately to ensure it works under memory pressure
83
+ let result = closure ( [ JSValue . number ( 10 ) ] )
84
+ XCTAssertEqual ( result. number, 110.0 ) // 100 (capturedData.count) + 10
85
+
86
+ // More frequent GC to stress the system
87
+ if i % 500 == 0 {
88
+ gc ( )
89
+ try await Task . sleep ( for: . milliseconds( 0 ) )
90
+ }
91
+ }
92
+ } catch {
93
+ print ( " JSClosure stress test stopped at \( successCount) closures: \( error) " )
94
+ }
95
+
96
+ // Test random closures still work after extreme memory pressure
97
+ for _ in 0 ..< min ( 100 , closures. count) {
98
+ let randomIndex = Int . random ( in: 0 ..< closures. count)
99
+ let result = closures [ randomIndex] ( [ JSValue . number ( 5 ) ] )
100
+ XCTAssertTrue ( result. number! > 5 ) // Should be 5 + capturedData.count (100+)
101
+ }
102
+
103
+ #if JAVASCRIPTKIT_WITHOUT_WEAKREFS
104
+ for closure in closures {
105
+ closure. release ( )
106
+ }
107
+ #endif
108
+
109
+ closures. removeAll ( )
110
+ for _ in 0 ..< 20 {
111
+ gc ( )
112
+ try await Task . sleep ( for: . milliseconds( 10 ) )
113
+ }
114
+ }
115
+
116
+ func testMixedAllocationMemoryBoundaries( ) async throws {
117
+ guard let gc = JSObject . global. gc. function else {
118
+ throw XCTSkip ( " Missing --expose-gc flag " )
119
+ }
120
+
121
+ // Test system behavior at memory boundaries with mixed object types
122
+ let cycles = 200
123
+ var totalObjects = 0
124
+ var totalClosures = 0
125
+
126
+ for cycle in 0 ..< cycles {
127
+ var cycleObjects : [ JSObject ] = [ ]
128
+ var cycleClosure : [ JSClosure ] = [ ]
129
+
130
+ // Exponentially increase allocation pressure each cycle
131
+ let objectsThisCycle = min ( 100 + cycle, 1000 )
132
+ let closuresThisCycle = min ( 50 + cycle / 2 , 500 )
133
+
134
+ do {
135
+ // Allocate objects
136
+ for i in 0 ..< objectsThisCycle {
137
+ let obj = JSObject ( )
138
+ // Create memory-intensive properties
139
+ obj [ " large_array " ] = JSObject . global. Array. function!. from!(
140
+ ( 0 ..< 1000 ) . map { JSValue . number ( Double ( $0) ) } . jsValue
141
+ ) . jsValue
142
+ obj [ " metadata " ] = [
143
+ " cycle " : cycle,
144
+ " index " : i,
145
+ " timestamp " : Int ( Date ( ) . timeIntervalSince1970)
146
+ ] . jsValue
147
+
148
+ cycleObjects. append ( obj)
149
+ totalObjects += 1
150
+ }
151
+
152
+ // Allocate closures with increasing complexity
153
+ for i in 0 ..< closuresThisCycle {
154
+ let heavyData = String ( repeating: " data " , count: cycle + 100 )
155
+ let closure = JSClosure { arguments in
156
+ // Force retention of heavy data
157
+ return JSValue . string ( heavyData. prefix ( 10 ) . description)
158
+ }
159
+ cycleClosure. append ( closure)
160
+ totalClosures += 1
161
+ }
162
+
163
+ } catch {
164
+ print ( " Memory boundary reached at cycle \( cycle) : \( error) " )
165
+ print ( " Total objects created: \( totalObjects) , closures: \( totalClosures) " )
166
+ break
167
+ }
168
+
169
+ // Test system still works under extreme pressure
170
+ if !cycleObjects. isEmpty {
171
+ XCTAssertNotNil ( cycleObjects [ 0 ] [ " large_array " ] . object)
172
+ }
173
+ if !cycleClosure. isEmpty {
174
+ let result = cycleClosure [ 0 ] ( arguments: [ ] )
175
+ XCTAssertNotNil ( result. string)
176
+ }
177
+
178
+ #if JAVASCRIPTKIT_WITHOUT_WEAKREFS
179
+ for closure in cycleClosure {
180
+ closure. release ( )
181
+ }
182
+ #endif
183
+
184
+ cycleObjects. removeAll ( )
185
+ cycleClosure. removeAll ( )
186
+
187
+ // Aggressive cleanup every 10 cycles
188
+ if cycle % 10 == 0 {
189
+ for _ in 0 ..< 10 {
190
+ gc ( )
191
+ try await Task . sleep ( for: . milliseconds( 1 ) )
192
+ }
193
+ }
194
+ }
195
+
196
+ print ( " Stress test completed: \( totalObjects) objects, \( totalClosures) closures allocated " )
197
+ }
198
+
199
+ func testHeapFragmentationRecovery( ) async throws {
200
+ guard let gc = JSObject . global. gc. function else {
201
+ throw XCTSkip ( " Missing --expose-gc flag " )
202
+ }
203
+
204
+ // Test system recovery from heap fragmentation by creating/destroying
205
+ // patterns that stress the memory allocator
206
+ let fragmentationCycles = 100
207
+
208
+ for cycle in 0 ..< fragmentationCycles {
209
+ var shortLivedObjects : [ JSObject ] = [ ]
210
+ var longLivedObjects : [ JSObject ] = [ ]
211
+
212
+ // Create fragmentation pattern: many short-lived, few long-lived
213
+ for i in 0 ..< 1000 {
214
+ let obj = JSObject ( )
215
+ obj [ " data " ] = JSValue . string ( String ( repeating: " fragment " , count: 100 ) )
216
+
217
+ if i % 10 == 0 {
218
+ // Long-lived objects
219
+ longLivedObjects. append ( obj)
220
+ } else {
221
+ // Short-lived objects
222
+ shortLivedObjects. append ( obj)
223
+ }
224
+ }
225
+
226
+ // Immediately release short-lived objects to create fragmentation
227
+ shortLivedObjects. removeAll ( )
228
+
229
+ // Force GC to reclaim fragmented memory
230
+ for _ in 0 ..< 5 {
231
+ gc ( )
232
+ try await Task . sleep ( for: . milliseconds( 1 ) )
233
+ }
234
+
235
+ // Test system can still allocate efficiently after fragmentation
236
+ var recoveryTest : [ JSObject ] = [ ]
237
+ for i in 0 ..< 500 {
238
+ let obj = JSObject ( )
239
+ obj [ " recovery_test " ] = JSValue . number ( Double ( i) )
240
+ recoveryTest. append ( obj)
241
+ }
242
+
243
+ // Verify recovery objects work correctly
244
+ for (i, obj) in recoveryTest. enumerated ( ) {
245
+ XCTAssertEqual ( obj [ " recovery_test " ] , JSValue . number ( Double ( i) ) )
246
+ }
247
+
248
+ recoveryTest. removeAll ( )
249
+ longLivedObjects. removeAll ( )
250
+
251
+ if cycle % 20 == 0 {
252
+ for _ in 0 ..< 10 {
253
+ gc ( )
254
+ try await Task . sleep ( for: . milliseconds( 5 ) )
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
0 commit comments