Skip to content

Commit 825f947

Browse files
Add test case to kill the wrong deallocation issue of JSClosure
1 parent 7c153c3 commit 825f947

File tree

2 files changed

+67
-2
lines changed

2 files changed

+67
-2
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ unittest:
1313
-Xlinker --global-base=524288 \
1414
-Xlinker -z \
1515
-Xlinker stack-size=524288 \
16-
js test --prelude ./Tests/prelude.mjs
16+
js test --prelude ./Tests/prelude.mjs -Xnode --expose-gc
1717

1818
.PHONY: regenerate_swiftpm_resources
1919
regenerate_swiftpm_resources:

Tests/JavaScriptKitTests/JSClosureTests.swift

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import JavaScriptKit
1+
@_spi(JSObject_id) import JavaScriptKit
22
import XCTest
33

44
class JSClosureTests: XCTestCase {
@@ -85,4 +85,69 @@ class JSClosureTests: XCTestCase {
8585
hostFunc2.release()
8686
#endif
8787
}
88+
89+
func testRegressionTestForMisDeallocation() async throws {
90+
// Use Node.js's `--expose-gc` flag to enable manual garbage collection.
91+
guard let gc = JSObject.global.gc.function else {
92+
throw XCTSkip("Missing --expose-gc flag")
93+
}
94+
95+
// Step 1: Create many JSClosure instances
96+
let obj = JSObject()
97+
var closurePointers: Set<UInt32> = []
98+
let numberOfSourceClosures = 10_000
99+
100+
do {
101+
var closures: [JSClosure] = []
102+
for i in 0..<numberOfSourceClosures {
103+
let closure = JSClosure { _ in .undefined }
104+
obj["c\(i)"] = closure.jsValue
105+
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
123+
}
124+
}
125+
126+
// Step 3: Create JSClosure instances and find the one with JSClosure.id == &closurePointers[x]
127+
do {
128+
while true {
129+
let c = JSClosure { _ in .undefined }
130+
if closurePointers.contains(c.id) || c.id > maxClosurePointer {
131+
break
132+
}
133+
// To avoid all JSClosures having a common JSObject.id diffs, randomly allocate a new JS object.
134+
if Bool.random() {
135+
_ = JSObject()
136+
}
137+
}
138+
}
139+
140+
// Step 4: Trigger garbage collection to call the finalizer of the conflicting JSClosure instance
141+
for _ in 0..<100 {
142+
gc()
143+
// Tick the event loop to allow the garbage collector to run finalizers
144+
// registered by FinalizationRegistry.
145+
try await Task.sleep(for: .milliseconds(0))
146+
}
147+
148+
// Step 5: Verify that the JSClosure instances are still alive and can be called
149+
for i in 0..<numberOfSourceClosures {
150+
_ = obj["c\(i)"].function!()
151+
}
152+
}
88153
}

0 commit comments

Comments
 (0)