diff --git a/Makefile b/Makefile index e2aef5f8..e9fd7c50 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SWIFT_SDK_ID ?= wasm32-unknown-wasi .PHONY: bootstrap bootstrap: npm ci - npx playwright install + npx playwright install chromium-headless-shell .PHONY: unittest unittest: diff --git a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift index d42c5add..28bc4545 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift @@ -39,12 +39,19 @@ public final class WebWorkerDedicatedExecutor: SerialExecutor { private let underlying: WebWorkerTaskExecutor /// - Parameters: + /// - stackSize: The stack size for each worker thread. Default is `nil` (use the platform default stack size). /// - timeout: The maximum time to wait for all worker threads to be started. Default is 3 seconds. /// - checkInterval: The interval to check if all worker threads are started. Default is 5 microseconds. /// - Throws: An error if any worker thread fails to initialize within the timeout period. - public init(timeout: Duration = .seconds(3), checkInterval: Duration = .microseconds(5)) async throws { + /// - Note: The default stack size of wasi-libc is typically 128KB. + public init( + stackSize: Int? = nil, + timeout: Duration = .seconds(3), + checkInterval: Duration = .microseconds(5) + ) async throws { let underlying = try await WebWorkerTaskExecutor( numberOfThreads: 1, + stackSize: stackSize, timeout: timeout, checkInterval: checkInterval ) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 651e7be2..f3923117 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -347,7 +347,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { self.workers = workers } - func start(timeout: Duration, checkInterval: Duration) async throws { + func start(stackSize: Int?, timeout: Duration, checkInterval: Duration) async throws { #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) class Context: @unchecked Sendable { let executor: WebWorkerTaskExecutor.Executor @@ -375,9 +375,21 @@ public final class WebWorkerTaskExecutor: TaskExecutor { let unmanagedContext = Unmanaged.passRetained(context) contexts.append(unmanagedContext) let ptr = unmanagedContext.toOpaque() + var attr = pthread_attr_t() + pthread_attr_init(&attr) + // Set the stack size if specified. + if let stackSize { + let ret = pthread_attr_setstacksize(&attr, stackSize) + guard ret == 0 else { + let strerror = String(cString: strerror(ret)) + throw SpawnError( + reason: "Failed to set stack size (\(stackSize)) for thread (\(ret): \(strerror))" + ) + } + } let ret = pthread_create( nil, - nil, + &attr, { ptr in // Cast to a optional pointer to absorb nullability variations between platforms. let ptr: UnsafeMutableRawPointer? = ptr @@ -390,6 +402,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { }, ptr ) + pthread_attr_destroy(&attr) guard ret == 0 else { let strerror = String(cString: strerror(ret)) throw SpawnError(reason: "Failed to create a thread (\(ret): \(strerror))") @@ -467,16 +480,19 @@ public final class WebWorkerTaskExecutor: TaskExecutor { /// /// - Parameters: /// - numberOfThreads: The number of Web Worker threads to spawn. + /// - stackSize: The stack size for each worker thread. Default is `nil` (use the platform default stack size). /// - timeout: The maximum time to wait for all worker threads to be started. Default is 3 seconds. /// - checkInterval: The interval to check if all worker threads are started. Default is 5 microseconds. /// - Throws: An error if any worker thread fails to initialize within the timeout period. + /// - Note: The default stack size of wasi-libc is typically 128KB. public init( numberOfThreads: Int, + stackSize: Int? = nil, timeout: Duration = .seconds(3), checkInterval: Duration = .microseconds(5) ) async throws { self.executor = Executor(numberOfThreads: numberOfThreads) - try await self.executor.start(timeout: timeout, checkInterval: checkInterval) + try await self.executor.start(stackSize: stackSize, timeout: timeout, checkInterval: checkInterval) } /// Terminates all worker threads managed by this executor. diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index b9c42c02..46afc9c8 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -122,6 +122,12 @@ final class WebWorkerTaskExecutorTests: XCTestCase { executor.terminate() } + func testThreadStackSize() async throws { + // Sanity check for stackSize parameter + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 3, stackSize: 512 * 1024) + executor.terminate() + } + func testTaskGroupRunOnDifferentThreads() async throws { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 2)