1
1
#if compiler(>=6.1) && _runtime(_multithreaded)
2
+ import Synchronization
2
3
import XCTest
3
4
import _CJavaScriptKit // For swjs_get_worker_thread_id
4
5
@testable import JavaScriptKit
@@ -22,6 +23,7 @@ func pthread_mutex_lock(_ mutex: UnsafeMutablePointer<pthread_mutex_t>) -> Int32
22
23
}
23
24
#endif
24
25
26
+ @available ( macOS 15 . 0 , iOS 18 . 0 , watchOS 11 . 0 , tvOS 18 . 0 , visionOS 2 . 0 , * )
25
27
final class WebWorkerTaskExecutorTests : XCTestCase {
26
28
func testTaskRunOnMainThread( ) async throws {
27
29
let executor = try await WebWorkerTaskExecutor ( numberOfThreads: 1 )
@@ -97,6 +99,182 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
97
99
executor. terminate ( )
98
100
}
99
101
102
+ func testScheduleJobWithinMacroTask1( ) async throws {
103
+ let executor = try await WebWorkerTaskExecutor ( numberOfThreads: 1 )
104
+ defer { executor. terminate ( ) }
105
+
106
+ final class Context : @unchecked Sendable {
107
+ let hasEndedFirstWorkerWakeLoop = Atomic < Bool > ( false )
108
+ let hasEnqueuedFromMain = Atomic < Bool > ( false )
109
+ let hasReachedNextMacroTask = Atomic < Bool > ( false )
110
+ let hasJobBEnded = Atomic < Bool > ( false )
111
+ let hasJobCEnded = Atomic < Bool > ( false )
112
+ }
113
+
114
+ // Scenario 1.
115
+ // | Main | Worker |
116
+ // | +---------------------+--------------------------+
117
+ // | | | Start JS macrotask |
118
+ // | | | Start 1st wake-loop |
119
+ // | | | Enq JS microtask A |
120
+ // | | | End 1st wake-loop |
121
+ // | | | Start a JS microtask A |
122
+ // time | Enq job B to Worker | [PAUSE] |
123
+ // | | | Enq Swift job C |
124
+ // | | | End JS microtask A |
125
+ // | | | Start 2nd wake-loop |
126
+ // | | | Run Swift job B |
127
+ // | | | Run Swift job C |
128
+ // | | | End 2nd wake-loop |
129
+ // v | | End JS macrotask |
130
+ // +---------------------+--------------------------+
131
+
132
+ let context = Context ( )
133
+ Task {
134
+ while !context. hasEndedFirstWorkerWakeLoop. load ( ordering: . sequentiallyConsistent) {
135
+ try ! await Task . sleep ( nanoseconds: 1_000 )
136
+ }
137
+ // Enqueue job B to Worker
138
+ Task ( executorPreference: executor) {
139
+ XCTAssertFalse ( isMainThread ( ) )
140
+ XCTAssertFalse ( context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent) )
141
+ context. hasJobBEnded. store ( true , ordering: . sequentiallyConsistent)
142
+ }
143
+ XCTAssertTrue ( isMainThread ( ) )
144
+ // Resume worker thread to let it enqueue job C
145
+ context. hasEnqueuedFromMain. store ( true , ordering: . sequentiallyConsistent)
146
+ }
147
+
148
+ // Start worker
149
+ await Task ( executorPreference: executor) {
150
+ // Schedule a new macrotask to detect if the current macrotask has completed
151
+ JSObject . global. setTimeout. function!(
152
+ JSOneshotClosure { _ in
153
+ context. hasReachedNextMacroTask. store ( true , ordering: . sequentiallyConsistent)
154
+ return . undefined
155
+ } ,
156
+ 0
157
+ )
158
+
159
+ // Enqueue a microtask, not managed by WebWorkerTaskExecutor
160
+ JSObject . global. queueMicrotask. function!(
161
+ JSOneshotClosure { _ in
162
+ // Resume the main thread and let it enqueue job B
163
+ context. hasEndedFirstWorkerWakeLoop. store ( true , ordering: . sequentiallyConsistent)
164
+ // Wait until the enqueue has completed
165
+ while !context. hasEnqueuedFromMain. load ( ordering: . sequentiallyConsistent) { }
166
+ // Should be still in the same macrotask
167
+ XCTAssertFalse ( context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent) )
168
+ // Enqueue job C
169
+ Task ( executorPreference: executor) {
170
+ // Should be still in the same macrotask
171
+ XCTAssertFalse ( context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent) )
172
+ // Notify that job C has completed
173
+ context. hasJobCEnded. store ( true , ordering: . sequentiallyConsistent)
174
+ }
175
+ return . undefined
176
+ } ,
177
+ 0
178
+ )
179
+ // Wait until job B, C and the next macrotask have completed
180
+ while !context. hasJobBEnded. load ( ordering: . sequentiallyConsistent)
181
+ || !context. hasJobCEnded. load ( ordering: . sequentiallyConsistent)
182
+ || !context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent)
183
+ {
184
+ try ! await Task . sleep ( nanoseconds: 1_000 )
185
+ }
186
+ } . value
187
+ }
188
+
189
+ func testScheduleJobWithinMacroTask2( ) async throws {
190
+ let executor = try await WebWorkerTaskExecutor ( numberOfThreads: 1 )
191
+ defer { executor. terminate ( ) }
192
+
193
+ final class Context : @unchecked Sendable {
194
+ let hasEndedFirstWorkerWakeLoop = Atomic < Bool > ( false )
195
+ let hasEnqueuedFromMain = Atomic < Bool > ( false )
196
+ let hasReachedNextMacroTask = Atomic < Bool > ( false )
197
+ let hasJobBEnded = Atomic < Bool > ( false )
198
+ let hasJobCEnded = Atomic < Bool > ( false )
199
+ }
200
+
201
+ // Scenario 2.
202
+ // (The order of enqueue of job B and C are reversed from Scenario 1)
203
+ //
204
+ // | Main | Worker |
205
+ // | +---------------------+--------------------------+
206
+ // | | | Start JS macrotask |
207
+ // | | | Start 1st wake-loop |
208
+ // | | | Enq JS microtask A |
209
+ // | | | End 1st wake-loop |
210
+ // | | | Start a JS microtask A |
211
+ // | | | Enq Swift job C |
212
+ // time | Enq job B to Worker | [PAUSE] |
213
+ // | | | End JS microtask A |
214
+ // | | | Start 2nd wake-loop |
215
+ // | | | Run Swift job B |
216
+ // | | | Run Swift job C |
217
+ // | | | End 2nd wake-loop |
218
+ // v | | End JS macrotask |
219
+ // +---------------------+--------------------------+
220
+
221
+ let context = Context ( )
222
+ Task {
223
+ while !context. hasEndedFirstWorkerWakeLoop. load ( ordering: . sequentiallyConsistent) {
224
+ try ! await Task . sleep ( nanoseconds: 1_000 )
225
+ }
226
+ // Enqueue job B to Worker
227
+ Task ( executorPreference: executor) {
228
+ XCTAssertFalse ( isMainThread ( ) )
229
+ XCTAssertFalse ( context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent) )
230
+ context. hasJobBEnded. store ( true , ordering: . sequentiallyConsistent)
231
+ }
232
+ XCTAssertTrue ( isMainThread ( ) )
233
+ // Resume worker thread to let it enqueue job C
234
+ context. hasEnqueuedFromMain. store ( true , ordering: . sequentiallyConsistent)
235
+ }
236
+
237
+ // Start worker
238
+ await Task ( executorPreference: executor) {
239
+ // Schedule a new macrotask to detect if the current macrotask has completed
240
+ JSObject . global. setTimeout. function!(
241
+ JSOneshotClosure { _ in
242
+ context. hasReachedNextMacroTask. store ( true , ordering: . sequentiallyConsistent)
243
+ return . undefined
244
+ } ,
245
+ 0
246
+ )
247
+
248
+ // Enqueue a microtask, not managed by WebWorkerTaskExecutor
249
+ JSObject . global. queueMicrotask. function!(
250
+ JSOneshotClosure { _ in
251
+ // Enqueue job C
252
+ Task ( executorPreference: executor) {
253
+ // Should be still in the same macrotask
254
+ XCTAssertFalse ( context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent) )
255
+ // Notify that job C has completed
256
+ context. hasJobCEnded. store ( true , ordering: . sequentiallyConsistent)
257
+ }
258
+ // Resume the main thread and let it enqueue job B
259
+ context. hasEndedFirstWorkerWakeLoop. store ( true , ordering: . sequentiallyConsistent)
260
+ // Wait until the enqueue has completed
261
+ while !context. hasEnqueuedFromMain. load ( ordering: . sequentiallyConsistent) { }
262
+ // Should be still in the same macrotask
263
+ XCTAssertFalse ( context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent) )
264
+ return . undefined
265
+ } ,
266
+ 0
267
+ )
268
+ // Wait until job B, C and the next macrotask have completed
269
+ while !context. hasJobBEnded. load ( ordering: . sequentiallyConsistent)
270
+ || !context. hasJobCEnded. load ( ordering: . sequentiallyConsistent)
271
+ || !context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent)
272
+ {
273
+ try ! await Task . sleep ( nanoseconds: 1_000 )
274
+ }
275
+ } . value
276
+ }
277
+
100
278
func testTaskGroupRunOnSameThread( ) async throws {
101
279
let executor = try await WebWorkerTaskExecutor ( numberOfThreads: 3 )
102
280
0 commit comments