Skip to content

Commit 02a4dda

Browse files
Merge pull request #274 from swiftwasm/yt/fix-multithread-cache-issue
Stop use of global variable as a object cache
2 parents 115ca29 + 288adb0 commit 02a4dda

File tree

5 files changed

+88
-10
lines changed

5 files changed

+88
-10
lines changed

Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift

+11-2
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
200200
parentTaskExecutor = executor
201201
// Store the thread ID to the worker. This notifies the main thread that the worker is started.
202202
self.tid.store(tid, ordering: .sequentiallyConsistent)
203+
trace("Worker.start tid=\(tid)")
203204
}
204205

205206
/// Process jobs in the queue.
@@ -212,7 +213,14 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
212213
guard let executor = parentTaskExecutor else {
213214
preconditionFailure("The worker must be started with a parent executor.")
214215
}
215-
assert(state.load(ordering: .sequentiallyConsistent) == .running, "Invalid state: not running")
216+
do {
217+
// Assert the state at the beginning of the run.
218+
let state = state.load(ordering: .sequentiallyConsistent)
219+
assert(
220+
state == .running || state == .terminated,
221+
"Invalid state: not running (tid=\(self.tid.load(ordering: .sequentiallyConsistent)), \(state))"
222+
)
223+
}
216224
while true {
217225
// Pop a job from the queue.
218226
let job = jobQueue.withLock { queue -> UnownedJob? in
@@ -247,7 +255,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
247255

248256
/// Terminate the worker.
249257
func terminate() {
250-
trace("Worker.terminate")
258+
trace("Worker.terminate tid=\(tid.load(ordering: .sequentiallyConsistent))")
251259
state.store(.terminated, ordering: .sequentiallyConsistent)
252260
let tid = self.tid.load(ordering: .sequentiallyConsistent)
253261
guard tid != 0 else {
@@ -283,6 +291,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
283291
self.worker = worker
284292
}
285293
}
294+
trace("Executor.start")
286295
// Start worker threads via pthread_create.
287296
for worker in workers {
288297
// NOTE: The context must be allocated on the heap because

Sources/JavaScriptKit/BasicObjects/JSArray.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,9 @@ extension JSArray: RandomAccessCollection {
9393
}
9494
}
9595

96-
private let alwaysTrue = JSClosure { _ in .boolean(true) }
96+
private let alwaysTrue = LazyThreadLocal(initialize: { JSClosure { _ in .boolean(true) } })
9797
private func getObjectValuesLength(_ object: JSObject) -> Int {
98-
let values = object.filter!(alwaysTrue).object!
98+
let values = object.filter!(alwaysTrue.wrappedValue).object!
9999
return Int(values.length.number!)
100100
}
101101

Sources/JavaScriptKit/ConvertibleToJSValue.swift

+5-3
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,10 @@ extension JSObject: JSValueCompatible {
8585
// from `JSFunction`
8686
}
8787

88-
private let objectConstructor = JSObject.global.Object.function!
89-
private let arrayConstructor = JSObject.global.Array.function!
88+
private let _objectConstructor = LazyThreadLocal(initialize: { JSObject.global.Object.function! })
89+
private var objectConstructor: JSFunction { _objectConstructor.wrappedValue }
90+
private let _arrayConstructor = LazyThreadLocal(initialize: { JSObject.global.Array.function! })
91+
private var arrayConstructor: JSFunction { _arrayConstructor.wrappedValue }
9092

9193
#if !hasFeature(Embedded)
9294
extension Dictionary where Value == ConvertibleToJSValue, Key == String {
@@ -296,4 +298,4 @@ extension Array where Element == ConvertibleToJSValue {
296298
return _withRawJSValues(self, 0, &_results, body)
297299
}
298300
}
299-
#endif
301+
#endif

Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import _CJavaScriptKit
22

3-
private let Symbol = JSObject.global.Symbol.function!
3+
private let _Symbol = LazyThreadLocal(initialize: { JSObject.global.Symbol.function! })
4+
private var Symbol: JSFunction { _Symbol.wrappedValue }
45

56
/// A wrapper around [the JavaScript `Symbol`
67
/// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol)

Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift

+68-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
3838

3939
func testAwaitInsideTask() async throws {
4040
let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
41+
defer { executor.terminate() }
4142

4243
let task = Task(executorPreference: executor) {
4344
await Task.yield()
@@ -46,8 +47,6 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
4647
}
4748
let taskRunOnMainThread = try await task.value
4849
XCTAssertFalse(taskRunOnMainThread)
49-
50-
executor.terminate()
5150
}
5251

5352
func testSleepInsideTask() async throws {
@@ -170,6 +169,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
170169
let result = await task.value
171170
XCTAssertEqual(result, 100)
172171
XCTAssertEqual(Check.value, 42)
172+
executor.terminate()
173173
}
174174

175175
func testLazyThreadLocalPerThreadInitialization() async throws {
@@ -198,6 +198,72 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
198198
let result = await task.value
199199
XCTAssertEqual(result, 100)
200200
XCTAssertEqual(Check.countOfInitialization, 2)
201+
executor.terminate()
202+
}
203+
204+
func testJSValueDecoderOnWorker() async throws {
205+
struct DecodeMe: Codable {
206+
struct Prop1: Codable {
207+
let nested_prop: Int
208+
}
209+
210+
let prop_1: Prop1
211+
let prop_2: Int
212+
let prop_3: Bool
213+
let prop_7: Float
214+
let prop_8: String
215+
let prop_9: [String]
216+
}
217+
218+
func decodeJob() throws {
219+
let json = """
220+
{
221+
"prop_1": {
222+
"nested_prop": 42
223+
},
224+
"prop_2": 100,
225+
"prop_3": true,
226+
"prop_7": 3.14,
227+
"prop_8": "Hello, World!",
228+
"prop_9": ["a", "b", "c"]
229+
}
230+
"""
231+
let object = JSObject.global.JSON.parse(json)
232+
let decoder = JSValueDecoder()
233+
let result = try decoder.decode(DecodeMe.self, from: object)
234+
XCTAssertEqual(result.prop_1.nested_prop, 42)
235+
XCTAssertEqual(result.prop_2, 100)
236+
XCTAssertEqual(result.prop_3, true)
237+
XCTAssertEqual(result.prop_7, 3.14)
238+
XCTAssertEqual(result.prop_8, "Hello, World!")
239+
XCTAssertEqual(result.prop_9, ["a", "b", "c"])
240+
}
241+
// Run the job on the main thread first to initialize the object cache
242+
try decodeJob()
243+
244+
let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
245+
defer { executor.terminate() }
246+
let task = Task(executorPreference: executor) {
247+
// Run the job on the worker thread to test the object cache
248+
// is not shared with the main thread
249+
try decodeJob()
250+
}
251+
try await task.value
252+
}
253+
254+
func testJSArrayCountOnWorker() async throws {
255+
let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
256+
func check() {
257+
let object = JSObject.global.Array.function!.new(1, 2, 3, 4, 5)
258+
let array = JSArray(object)!
259+
XCTAssertEqual(array.count, 5)
260+
}
261+
check()
262+
let task = Task(executorPreference: executor) {
263+
check()
264+
}
265+
await task.value
266+
executor.terminate()
201267
}
202268

203269
/*

0 commit comments

Comments
 (0)