Skip to content

Commit 80b279c

Browse files
Add test case to kill the wrong deallocation issue of JSClosure
1 parent 2f18517 commit 80b279c

File tree

2 files changed

+68
-2
lines changed

2 files changed

+68
-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: 67 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,70 @@ 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+
_ = JSObject.global.process.stdout.write("Found matching object with id: \(obj.id)\n")
123+
break
124+
}
125+
}
126+
127+
// Step 3: Create JSClosure instances and find the one with JSClosure.id == &closurePointers[x]
128+
do {
129+
while true {
130+
let c = JSClosure { _ in .undefined }
131+
if closurePointers.contains(c.id) || c.id > maxClosurePointer {
132+
break
133+
}
134+
// To avoid all JSClosures having a common JSObject.id diffs, randomly allocate a new JS object.
135+
if Bool.random() {
136+
_ = JSObject()
137+
}
138+
}
139+
}
140+
141+
// Step 4: Trigger garbage collection to call the finalizer of the conflicting JSClosure instance
142+
for _ in 0..<100 {
143+
gc()
144+
// Tick the event loop to allow the garbage collector to run finalizers
145+
// registered by FinalizationRegistry.
146+
try await Task.sleep(for: .milliseconds(0))
147+
}
148+
149+
// Step 5: Verify that the JSClosure instances are still alive and can be called
150+
for i in 0..<numberOfSourceClosures {
151+
_ = obj["c\(i)"].function!()
152+
}
153+
}
88154
}

0 commit comments

Comments
 (0)