Skip to content

Commit 20f97ed

Browse files
committedJun 27, 2024
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.
1 parent 2bb0694 commit 20f97ed

File tree

3 files changed

+56
-4
lines changed

3 files changed

+56
-4
lines changed
 

Diff for: ‎Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift

+48-4
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,28 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
5757
}
5858

5959
/// A singleton instance of the Executor
60-
public static let shared: JavaScriptEventLoop = {
60+
public static var shared: JavaScriptEventLoop {
61+
return _shared
62+
}
63+
64+
#if _runtime(_multithreaded)
65+
// In multi-threaded environment, we have an event loop executor per
66+
// thread (per Web Worker). A job enqueued in one thread should be
67+
// executed in the same thread under this global executor.
68+
private static var _shared: JavaScriptEventLoop {
69+
if let tls = swjs_thread_local_event_loop {
70+
let eventLoop = Unmanaged<JavaScriptEventLoop>.fromOpaque(tls).takeUnretainedValue()
71+
return eventLoop
72+
}
73+
let eventLoop = create()
74+
swjs_thread_local_event_loop = Unmanaged.passRetained(eventLoop).toOpaque()
75+
return eventLoop
76+
}
77+
#else
78+
private static let _shared: JavaScriptEventLoop = create()
79+
#endif
80+
81+
private static func create() -> JavaScriptEventLoop {
6182
let promise = JSPromise(resolver: { resolver -> Void in
6283
resolver(.success(.undefined))
6384
})
@@ -79,9 +100,13 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
79100
}
80101
)
81102
return eventLoop
82-
}()
103+
}
83104

84105
private static var didInstallGlobalExecutor = false
106+
fileprivate static var _mainThreadEventLoop: JavaScriptEventLoop!
107+
fileprivate static var mainThreadEventLoop: JavaScriptEventLoop {
108+
return _mainThreadEventLoop
109+
}
85110

86111
/// Set JavaScript event loop based executor to be the global executor
87112
/// Note that this should be called before any of the jobs are created.
@@ -91,6 +116,10 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
91116
public static func installGlobalExecutor() {
92117
guard !didInstallGlobalExecutor else { return }
93118

119+
// NOTE: We assume that this function is called before any of the jobs are created, so we can safely
120+
// assume that we are in the main thread.
121+
_mainThreadEventLoop = JavaScriptEventLoop.shared
122+
94123
#if compiler(>=5.9)
95124
typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) (swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override) -> Void
96125
let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in
@@ -121,10 +150,10 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
121150

122151
typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueMainExecutor_original) -> Void
123152
let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in
124-
JavaScriptEventLoop.shared.unsafeEnqueue(job)
153+
JavaScriptEventLoop.enqueueMainJob(job)
125154
}
126155
swift_task_enqueueMainExecutor_hook = unsafeBitCast(swift_task_enqueueMainExecutor_hook_impl, to: UnsafeMutableRawPointer?.self)
127-
156+
128157
didInstallGlobalExecutor = true
129158
}
130159

@@ -159,6 +188,21 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
159188
public func asUnownedSerialExecutor() -> UnownedSerialExecutor {
160189
return UnownedSerialExecutor(ordinary: self)
161190
}
191+
192+
public static func enqueueMainJob(_ job: consuming ExecutorJob) {
193+
self.enqueueMainJob(UnownedJob(job))
194+
}
195+
196+
static func enqueueMainJob(_ job: UnownedJob) {
197+
let currentEventLoop = JavaScriptEventLoop.shared
198+
if currentEventLoop === JavaScriptEventLoop.mainThreadEventLoop {
199+
currentEventLoop.unsafeEnqueue(job)
200+
} else {
201+
// Notify the main thread to execute the job
202+
let jobBitPattern = unsafeBitCast(job, to: UInt.self)
203+
_ = JSObject.global.postMessage!(jobBitPattern)
204+
}
205+
}
162206
}
163207

164208
#if compiler(>=5.7)
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#include "_CJavaScriptEventLoop.h"
2+
3+
_Thread_local void *swjs_thread_local_event_loop;

Diff for: ‎Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h

+5
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,9 @@ typedef SWIFT_CC(swift) void (*swift_task_asyncMainDrainQueue_override)(
6161
SWIFT_EXPORT_FROM(swift_Concurrency)
6262
extern void *_Nullable swift_task_asyncMainDrainQueue_hook;
6363

64+
65+
/// MARK: - thread local storage
66+
67+
extern _Thread_local void * _Nullable swjs_thread_local_event_loop;
68+
6469
#endif

0 commit comments

Comments
 (0)
Please sign in to comment.