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,168 @@ 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!( JSOneshotClosure { _ in
152+ context. hasReachedNextMacroTask. store ( true , ordering: . sequentiallyConsistent)
153+ return . undefined
154+ } , 0 )
155+
156+ // Enqueue a microtask, not managed by WebWorkerTaskExecutor
157+ JSObject . global. queueMicrotask. function!( JSOneshotClosure { _ in
158+ // Resume the main thread and let it enqueue job B
159+ context. hasEndedFirstWorkerWakeLoop. store ( true , ordering: . sequentiallyConsistent)
160+ // Wait until the enqueue has completed
161+ while !context. hasEnqueuedFromMain. load ( ordering: . sequentiallyConsistent) { }
162+ // Should be still in the same macrotask
163+ XCTAssertFalse ( context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent) )
164+ // Enqueue job C
165+ Task ( executorPreference: executor) {
166+ // Should be still in the same macrotask
167+ XCTAssertFalse ( context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent) )
168+ // Notify that job C has completed
169+ context. hasJobCEnded. store ( true , ordering: . sequentiallyConsistent)
170+ }
171+ return . undefined
172+ } , 0 )
173+ // Wait until job B, C and the next macrotask have completed
174+ while !context. hasJobBEnded. load ( ordering: . sequentiallyConsistent) ||
175+ !context. hasJobCEnded. load ( ordering: . sequentiallyConsistent) ||
176+ !context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent) {
177+ try ! await Task . sleep ( nanoseconds: 1_000 )
178+ }
179+ } . value
180+ }
181+
182+ func testScheduleJobWithinMacroTask2( ) async throws {
183+ let executor = try await WebWorkerTaskExecutor ( numberOfThreads: 1 )
184+ defer { executor. terminate ( ) }
185+
186+ final class Context : @unchecked Sendable {
187+ let hasEndedFirstWorkerWakeLoop = Atomic < Bool > ( false )
188+ let hasEnqueuedFromMain = Atomic < Bool > ( false )
189+ let hasReachedNextMacroTask = Atomic < Bool > ( false )
190+ let hasJobBEnded = Atomic < Bool > ( false )
191+ let hasJobCEnded = Atomic < Bool > ( false )
192+ }
193+
194+ // Scenario 2.
195+ // (The order of enqueue of job B and C are reversed from Scenario 1)
196+ //
197+ // | Main | Worker |
198+ // | +---------------------+--------------------------+
199+ // | | | Start JS macrotask |
200+ // | | | Start 1st wake-loop |
201+ // | | | Enq JS microtask A |
202+ // | | | End 1st wake-loop |
203+ // | | | Start a JS microtask A |
204+ // | | | Enq Swift job C |
205+ // time | Enq job B to Worker | [PAUSE] |
206+ // | | | End JS microtask A |
207+ // | | | Start 2nd wake-loop |
208+ // | | | Run Swift job B |
209+ // | | | Run Swift job C |
210+ // | | | End 2nd wake-loop |
211+ // v | | End JS macrotask |
212+ // +---------------------+--------------------------+
213+
214+ let context = Context ( )
215+ Task {
216+ while !context. hasEndedFirstWorkerWakeLoop. load ( ordering: . sequentiallyConsistent) {
217+ try ! await Task . sleep ( nanoseconds: 1_000 )
218+ }
219+ // Enqueue job B to Worker
220+ Task ( executorPreference: executor) {
221+ XCTAssertFalse ( isMainThread ( ) )
222+ XCTAssertFalse ( context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent) )
223+ context. hasJobBEnded. store ( true , ordering: . sequentiallyConsistent)
224+ }
225+ XCTAssertTrue ( isMainThread ( ) )
226+ // Resume worker thread to let it enqueue job C
227+ context. hasEnqueuedFromMain. store ( true , ordering: . sequentiallyConsistent)
228+ }
229+
230+ // Start worker
231+ await Task ( executorPreference: executor) {
232+ // Schedule a new macrotask to detect if the current macrotask has completed
233+ JSObject . global. setTimeout. function!( JSOneshotClosure { _ in
234+ context. hasReachedNextMacroTask. store ( true , ordering: . sequentiallyConsistent)
235+ return . undefined
236+ } , 0 )
237+
238+ // Enqueue a microtask, not managed by WebWorkerTaskExecutor
239+ JSObject . global. queueMicrotask. function!( JSOneshotClosure { _ in
240+ // Enqueue job C
241+ Task ( executorPreference: executor) {
242+ // Should be still in the same macrotask
243+ XCTAssertFalse ( context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent) )
244+ // Notify that job C has completed
245+ context. hasJobCEnded. store ( true , ordering: . sequentiallyConsistent)
246+ }
247+ // Resume the main thread and let it enqueue job B
248+ context. hasEndedFirstWorkerWakeLoop. store ( true , ordering: . sequentiallyConsistent)
249+ // Wait until the enqueue has completed
250+ while !context. hasEnqueuedFromMain. load ( ordering: . sequentiallyConsistent) { }
251+ // Should be still in the same macrotask
252+ XCTAssertFalse ( context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent) )
253+ return . undefined
254+ } , 0 )
255+ // Wait until job B, C and the next macrotask have completed
256+ while !context. hasJobBEnded. load ( ordering: . sequentiallyConsistent) ||
257+ !context. hasJobCEnded. load ( ordering: . sequentiallyConsistent) ||
258+ !context. hasReachedNextMacroTask. load ( ordering: . sequentiallyConsistent) {
259+ try ! await Task . sleep ( nanoseconds: 1_000 )
260+ }
261+ } . value
262+ }
263+
100264 func testTaskGroupRunOnSameThread( ) async throws {
101265 let executor = try await WebWorkerTaskExecutor ( numberOfThreads: 3 )
102266
0 commit comments