11#if compiler(>=6.1) && _runtime(_multithreaded)
2+ import Synchronization
23import XCTest
34import _CJavaScriptKit // For swjs_get_worker_thread_id
45@testable import JavaScriptKit
@@ -22,6 +23,7 @@ func pthread_mutex_lock(_ mutex: UnsafeMutablePointer<pthread_mutex_t>) -> Int32
2223}
2324#endif
2425
26+ @available ( macOS 15 . 0 , iOS 18 . 0 , watchOS 11 . 0 , tvOS 18 . 0 , visionOS 2 . 0 , * )
2527final class WebWorkerTaskExecutorTests : XCTestCase {
2628 func testTaskRunOnMainThread( ) async throws {
2729 let executor = try await WebWorkerTaskExecutor ( numberOfThreads: 1 )
@@ -97,6 +99,182 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
9799 executor. terminate ( )
98100 }
99101
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+
100278 func testTaskGroupRunOnSameThread( ) async throws {
101279 let executor = try await WebWorkerTaskExecutor ( numberOfThreads: 3 )
102280
0 commit comments