|
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 |
|
|