Skip to content

Commit fd571a1

Browse files
Add memory stress tests for JSObject and JSClosure
Tests memory exhaustion, heap fragmentation, and boundary conditions without FinalizationRegistry to validate reference counting under extreme allocation pressure.
1 parent 9968357 commit fd571a1

File tree

1 file changed

+259
-0
lines changed

1 file changed

+259
-0
lines changed
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
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

Comments
 (0)