From 20f97eded34a2ca88515c053b5fbd9d40d9d1aaf Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 27 Jun 2024 15:16:31 +0900 Subject: [PATCH 1/6] Allocate `JavaScriptEventLoop` per thread in multi-threaded environment This change makes `JavaScriptEventLoop` to be allocated per thread in multi-threaded environment. This is necessary to ensure that a job enqueued in one thread is executed in the same thread because JSObject managed by a worker can only be accessed in the same thread. --- .../JavaScriptEventLoop.swift | 52 +++++++++++++++++-- .../_CJavaScriptEventLoop.c | 3 ++ .../include/_CJavaScriptEventLoop.h | 5 ++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 8f09279af..06522d2bc 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -57,7 +57,28 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } /// A singleton instance of the Executor - public static let shared: JavaScriptEventLoop = { + public static var shared: JavaScriptEventLoop { + return _shared + } + + #if _runtime(_multithreaded) + // In multi-threaded environment, we have an event loop executor per + // thread (per Web Worker). A job enqueued in one thread should be + // executed in the same thread under this global executor. + private static var _shared: JavaScriptEventLoop { + if let tls = swjs_thread_local_event_loop { + let eventLoop = Unmanaged.fromOpaque(tls).takeUnretainedValue() + return eventLoop + } + let eventLoop = create() + swjs_thread_local_event_loop = Unmanaged.passRetained(eventLoop).toOpaque() + return eventLoop + } + #else + private static let _shared: JavaScriptEventLoop = create() + #endif + + private static func create() -> JavaScriptEventLoop { let promise = JSPromise(resolver: { resolver -> Void in resolver(.success(.undefined)) }) @@ -79,9 +100,13 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } ) return eventLoop - }() + } private static var didInstallGlobalExecutor = false + fileprivate static var _mainThreadEventLoop: JavaScriptEventLoop! + fileprivate static var mainThreadEventLoop: JavaScriptEventLoop { + return _mainThreadEventLoop + } /// Set JavaScript event loop based executor to be the global executor /// Note that this should be called before any of the jobs are created. @@ -91,6 +116,10 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { public static func installGlobalExecutor() { guard !didInstallGlobalExecutor else { return } + // NOTE: We assume that this function is called before any of the jobs are created, so we can safely + // assume that we are in the main thread. + _mainThreadEventLoop = JavaScriptEventLoop.shared + #if compiler(>=5.9) typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) (swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override) -> Void let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in @@ -121,10 +150,10 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueMainExecutor_original) -> Void let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in - JavaScriptEventLoop.shared.unsafeEnqueue(job) + JavaScriptEventLoop.enqueueMainJob(job) } swift_task_enqueueMainExecutor_hook = unsafeBitCast(swift_task_enqueueMainExecutor_hook_impl, to: UnsafeMutableRawPointer?.self) - + didInstallGlobalExecutor = true } @@ -159,6 +188,21 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { public func asUnownedSerialExecutor() -> UnownedSerialExecutor { return UnownedSerialExecutor(ordinary: self) } + + public static func enqueueMainJob(_ job: consuming ExecutorJob) { + self.enqueueMainJob(UnownedJob(job)) + } + + static func enqueueMainJob(_ job: UnownedJob) { + let currentEventLoop = JavaScriptEventLoop.shared + if currentEventLoop === JavaScriptEventLoop.mainThreadEventLoop { + currentEventLoop.unsafeEnqueue(job) + } else { + // Notify the main thread to execute the job + let jobBitPattern = unsafeBitCast(job, to: UInt.self) + _ = JSObject.global.postMessage!(jobBitPattern) + } + } } #if compiler(>=5.7) diff --git a/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c b/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c index e69de29bb..009672933 100644 --- a/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c +++ b/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c @@ -0,0 +1,3 @@ +#include "_CJavaScriptEventLoop.h" + +_Thread_local void *swjs_thread_local_event_loop; diff --git a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h index 2880772d6..890e26a01 100644 --- a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h +++ b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h @@ -61,4 +61,9 @@ typedef SWIFT_CC(swift) void (*swift_task_asyncMainDrainQueue_override)( SWIFT_EXPORT_FROM(swift_Concurrency) extern void *_Nullable swift_task_asyncMainDrainQueue_hook; + +/// MARK: - thread local storage + +extern _Thread_local void * _Nullable swjs_thread_local_event_loop; + #endif From 4ee27e6e2dd00c113bf18d762346a1bc61e85f1e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 27 Jun 2024 15:18:20 +0900 Subject: [PATCH 2/6] Suppress concurrency warning about `JSObject.global` --- .../JavaScriptKit/FundamentalObjects/JSObject.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 1883cbf32..861758497 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -137,7 +137,16 @@ public class JSObject: Equatable { /// A `JSObject` of the global scope object. /// This allows access to the global properties and global names by accessing the `JSObject` returned. - public static let global = JSObject(id: _JS_Predef_Value_Global) + public static var global: JSObject { return _global } + + // `JSObject` storage itself is immutable, and use of `JSObject.global` from other + // threads maintains the same semantics as `globalThis` in JavaScript. + #if compiler(>=5.10) + nonisolated(unsafe) + static let _global = JSObject(id: _JS_Predef_Value_Global) + #else + static let _global = JSObject(id: _JS_Predef_Value_Global) + #endif deinit { swjs_release(id) } From a0c4602607576c370edabc871c675131b59faedb Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 27 Jun 2024 15:26:12 +0900 Subject: [PATCH 3/6] `_runtime(multithreaded)` is only available in Swift 6.0 and later --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 06522d2bc..67cd88042 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -61,7 +61,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { return _shared } - #if _runtime(_multithreaded) + #if compiler(>=6.0) && _runtime(_multithreaded) // In multi-threaded environment, we have an event loop executor per // thread (per Web Worker). A job enqueued in one thread should be // executed in the same thread under this global executor. From 888de173f108a831aeb10d52727094f8b2fe3ec8 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 27 Jun 2024 15:36:34 +0900 Subject: [PATCH 4/6] Remove `enqueueMainJob` --- .../JavaScriptEventLoop.swift | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 67cd88042..a47491982 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -150,7 +150,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueMainExecutor_original) -> Void let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in - JavaScriptEventLoop.enqueueMainJob(job) + JavaScriptEventLoop.shared.unsafeEnqueue(job) } swift_task_enqueueMainExecutor_hook = unsafeBitCast(swift_task_enqueueMainExecutor_hook_impl, to: UnsafeMutableRawPointer?.self) @@ -188,21 +188,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { public func asUnownedSerialExecutor() -> UnownedSerialExecutor { return UnownedSerialExecutor(ordinary: self) } - - public static func enqueueMainJob(_ job: consuming ExecutorJob) { - self.enqueueMainJob(UnownedJob(job)) - } - - static func enqueueMainJob(_ job: UnownedJob) { - let currentEventLoop = JavaScriptEventLoop.shared - if currentEventLoop === JavaScriptEventLoop.mainThreadEventLoop { - currentEventLoop.unsafeEnqueue(job) - } else { - // Notify the main thread to execute the job - let jobBitPattern = unsafeBitCast(job, to: UInt.self) - _ = JSObject.global.postMessage!(jobBitPattern) - } - } } #if compiler(>=5.7) From 9c9ae1dfb68364c1e823125908d7a532959f5d2b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 27 Jun 2024 15:42:44 +0900 Subject: [PATCH 5/6] Drop Swift 5.7 support Swift 5.7 doesn't support short-circuit evaluation in `#if` conditions --- .github/workflows/test.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aad5a3555..2a4625d3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,11 +17,11 @@ jobs: - { os: ubuntu-22.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations - - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node } - - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: MicroWASI } + - { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: MicroWASI } + - { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: MicroWASI } - os: ubuntu-22.04 toolchain: DEVELOPMENT-SNAPSHOT-2024-05-01-a swift-sdk: @@ -76,8 +76,6 @@ jobs: strategy: matrix: include: - - os: macos-12 - xcode: Xcode_14.0 - os: macos-13 xcode: Xcode_14.3 - os: macos-14 From eb47bcbd93eed9e435e41d3ebf433126bc5507d4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 27 Jun 2024 15:54:13 +0900 Subject: [PATCH 6/6] Stop tracking main thread event loop --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index a47491982..7a0364a5c 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -103,10 +103,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } private static var didInstallGlobalExecutor = false - fileprivate static var _mainThreadEventLoop: JavaScriptEventLoop! - fileprivate static var mainThreadEventLoop: JavaScriptEventLoop { - return _mainThreadEventLoop - } /// Set JavaScript event loop based executor to be the global executor /// Note that this should be called before any of the jobs are created. @@ -116,10 +112,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { public static func installGlobalExecutor() { guard !didInstallGlobalExecutor else { return } - // NOTE: We assume that this function is called before any of the jobs are created, so we can safely - // assume that we are in the main thread. - _mainThreadEventLoop = JavaScriptEventLoop.shared - #if compiler(>=5.9) typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) (swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override) -> Void let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in