Skip to content

Commit

Permalink
Merge pull request #274 from swiftwasm/yt/fix-multithread-cache-issue
Browse files Browse the repository at this point in the history
Stop use of global variable as a object cache
  • Loading branch information
kateinoigakukun authored Nov 28, 2024
2 parents 115ca29 + 288adb0 commit 02a4dda
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 10 deletions.
13 changes: 11 additions & 2 deletions Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
parentTaskExecutor = executor
// Store the thread ID to the worker. This notifies the main thread that the worker is started.
self.tid.store(tid, ordering: .sequentiallyConsistent)
trace("Worker.start tid=\(tid)")
}

/// Process jobs in the queue.
Expand All @@ -212,7 +213,14 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
guard let executor = parentTaskExecutor else {
preconditionFailure("The worker must be started with a parent executor.")
}
assert(state.load(ordering: .sequentiallyConsistent) == .running, "Invalid state: not running")
do {
// Assert the state at the beginning of the run.
let state = state.load(ordering: .sequentiallyConsistent)
assert(
state == .running || state == .terminated,
"Invalid state: not running (tid=\(self.tid.load(ordering: .sequentiallyConsistent)), \(state))"
)
}
while true {
// Pop a job from the queue.
let job = jobQueue.withLock { queue -> UnownedJob? in
Expand Down Expand Up @@ -247,7 +255,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor {

/// Terminate the worker.
func terminate() {
trace("Worker.terminate")
trace("Worker.terminate tid=\(tid.load(ordering: .sequentiallyConsistent))")
state.store(.terminated, ordering: .sequentiallyConsistent)
let tid = self.tid.load(ordering: .sequentiallyConsistent)
guard tid != 0 else {
Expand Down Expand Up @@ -283,6 +291,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
self.worker = worker
}
}
trace("Executor.start")
// Start worker threads via pthread_create.
for worker in workers {
// NOTE: The context must be allocated on the heap because
Expand Down
4 changes: 2 additions & 2 deletions Sources/JavaScriptKit/BasicObjects/JSArray.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ extension JSArray: RandomAccessCollection {
}
}

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

Expand Down
8 changes: 5 additions & 3 deletions Sources/JavaScriptKit/ConvertibleToJSValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@ extension JSObject: JSValueCompatible {
// from `JSFunction`
}

private let objectConstructor = JSObject.global.Object.function!
private let arrayConstructor = JSObject.global.Array.function!
private let _objectConstructor = LazyThreadLocal(initialize: { JSObject.global.Object.function! })
private var objectConstructor: JSFunction { _objectConstructor.wrappedValue }
private let _arrayConstructor = LazyThreadLocal(initialize: { JSObject.global.Array.function! })
private var arrayConstructor: JSFunction { _arrayConstructor.wrappedValue }

#if !hasFeature(Embedded)
extension Dictionary where Value == ConvertibleToJSValue, Key == String {
Expand Down Expand Up @@ -296,4 +298,4 @@ extension Array where Element == ConvertibleToJSValue {
return _withRawJSValues(self, 0, &_results, body)
}
}
#endif
#endif
3 changes: 2 additions & 1 deletion Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import _CJavaScriptKit

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

/// A wrapper around [the JavaScript `Symbol`
/// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol)
Expand Down
70 changes: 68 additions & 2 deletions Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase {

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

let task = Task(executorPreference: executor) {
await Task.yield()
Expand All @@ -46,8 +47,6 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
}
let taskRunOnMainThread = try await task.value
XCTAssertFalse(taskRunOnMainThread)

executor.terminate()
}

func testSleepInsideTask() async throws {
Expand Down Expand Up @@ -170,6 +169,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
let result = await task.value
XCTAssertEqual(result, 100)
XCTAssertEqual(Check.value, 42)
executor.terminate()
}

func testLazyThreadLocalPerThreadInitialization() async throws {
Expand Down Expand Up @@ -198,6 +198,72 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
let result = await task.value
XCTAssertEqual(result, 100)
XCTAssertEqual(Check.countOfInitialization, 2)
executor.terminate()
}

func testJSValueDecoderOnWorker() async throws {
struct DecodeMe: Codable {
struct Prop1: Codable {
let nested_prop: Int
}

let prop_1: Prop1
let prop_2: Int
let prop_3: Bool
let prop_7: Float
let prop_8: String
let prop_9: [String]
}

func decodeJob() throws {
let json = """
{
"prop_1": {
"nested_prop": 42
},
"prop_2": 100,
"prop_3": true,
"prop_7": 3.14,
"prop_8": "Hello, World!",
"prop_9": ["a", "b", "c"]
}
"""
let object = JSObject.global.JSON.parse(json)
let decoder = JSValueDecoder()
let result = try decoder.decode(DecodeMe.self, from: object)
XCTAssertEqual(result.prop_1.nested_prop, 42)
XCTAssertEqual(result.prop_2, 100)
XCTAssertEqual(result.prop_3, true)
XCTAssertEqual(result.prop_7, 3.14)
XCTAssertEqual(result.prop_8, "Hello, World!")
XCTAssertEqual(result.prop_9, ["a", "b", "c"])
}
// Run the job on the main thread first to initialize the object cache
try decodeJob()

let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
defer { executor.terminate() }
let task = Task(executorPreference: executor) {
// Run the job on the worker thread to test the object cache
// is not shared with the main thread
try decodeJob()
}
try await task.value
}

func testJSArrayCountOnWorker() async throws {
let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
func check() {
let object = JSObject.global.Array.function!.new(1, 2, 3, 4, 5)
let array = JSArray(object)!
XCTAssertEqual(array.count, 5)
}
check()
let task = Task(executorPreference: executor) {
check()
}
await task.value
executor.terminate()
}

/*
Expand Down

0 comments on commit 02a4dda

Please sign in to comment.