From ea0d6f1f793035c8ae7e6fcf95ee21fc1df4ee34 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 3 Oct 2021 19:43:51 +0900 Subject: [PATCH 1/6] Experimental global executor cooperating with JS event loop --- IntegrationTests/Makefile | 2 + IntegrationTests/TestSuites/Package.swift | 15 +++ .../ConcurrencyTests/UnitTestUtils.swift | 124 ++++++++++++++++++ .../Sources/ConcurrencyTests/main.swift | 94 +++++++++++++ IntegrationTests/bin/concurrency-tests.js | 6 + Package.swift | 12 ++ .../JavaScriptEventLoop.swift | 108 +++++++++++++++ Sources/JavaScriptEventLoop/JobQueue.swift | 85 ++++++++++++ .../_CJavaScriptEventLoop.c | 0 .../include/_CJavaScriptEventLoop.h | 46 +++++++ .../include/module.modulemap | 4 + 11 files changed, 496 insertions(+) create mode 100644 IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift create mode 100644 IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift create mode 100644 IntegrationTests/bin/concurrency-tests.js create mode 100644 Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift create mode 100644 Sources/JavaScriptEventLoop/JobQueue.swift create mode 100644 Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c create mode 100644 Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h create mode 100644 Sources/_CJavaScriptEventLoop/include/module.modulemap diff --git a/IntegrationTests/Makefile b/IntegrationTests/Makefile index cdb295704..c00efb1be 100644 --- a/IntegrationTests/Makefile +++ b/IntegrationTests/Makefile @@ -33,3 +33,5 @@ benchmark: benchmark_setup run_benchmark .PHONY: test test: build_rt dist/PrimaryTests.wasm node bin/primary-tests.js +concurrency_test: build_rt dist/ConcurrencyTests.wasm + node bin/concurrency-tests.js diff --git a/IntegrationTests/TestSuites/Package.swift b/IntegrationTests/TestSuites/Package.swift index 4fca78452..1bb48648b 100644 --- a/IntegrationTests/TestSuites/Package.swift +++ b/IntegrationTests/TestSuites/Package.swift @@ -4,10 +4,19 @@ import PackageDescription let package = Package( name: "TestSuites", + platforms: [ + // This package doesn't work on macOS host, but should be able to be built for it + // for developing on Xcode. This minimum version requirement is to prevent availability + // errors for Concurrency API, whose runtime support is shipped from macOS 12.0 + .macOS("12.0") + ], products: [ .executable( name: "PrimaryTests", targets: ["PrimaryTests"] ), + .executable( + name: "ConcurrencyTests", targets: ["ConcurrencyTests"] + ), .executable( name: "BenchmarkTests", targets: ["BenchmarkTests"] ), @@ -15,6 +24,12 @@ let package = Package( dependencies: [.package(name: "JavaScriptKit", path: "../../")], targets: [ .target(name: "PrimaryTests", dependencies: ["JavaScriptKit"]), + .target( + name: "ConcurrencyTests", + dependencies: [ + .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), + ] + ), .target(name: "BenchmarkTests", dependencies: ["JavaScriptKit"]), ] ) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift new file mode 100644 index 000000000..011fb7443 --- /dev/null +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift @@ -0,0 +1,124 @@ +import JavaScriptKit + +var printTestNames = false +// Uncomment the next line to print the name of each test suite before running it. +// This will make it easier to debug any errors that occur on the JS side. +//printTestNames = true + +func test(_ name: String, testBlock: () throws -> Void) throws { + if printTestNames { print(name) } + do { + try testBlock() + } catch { + print("Error in \(name)") + print(error) + throw error + } +} + +func asyncTest(_ name: String, testBlock: () async throws -> Void) async throws -> Void { + if printTestNames { print(name) } + do { + try await testBlock() + } catch { + print("Error in \(name)") + print(error) + throw error + } +} + +struct MessageError: Error { + let message: String + let file: StaticString + let line: UInt + let column: UInt + init(_ message: String, file: StaticString, line: UInt, column: UInt) { + self.message = message + self.file = file + self.line = line + self.column = column + } +} + +func expectEqual( + _ lhs: T, _ rhs: T, + file: StaticString = #file, line: UInt = #line, column: UInt = #column +) throws { + if lhs != rhs { + throw MessageError("Expect to be equal \"\(lhs)\" and \"\(rhs)\"", file: file, line: line, column: column) + } +} + +func expectCast( + _ value: T, to type: U.Type = U.self, + file: StaticString = #file, line: UInt = #line, column: UInt = #column +) throws -> U { + guard let value = value as? U else { + throw MessageError("Expect \"\(value)\" to be \(U.self)", file: file, line: line, column: column) + } + return value +} + +func expectObject(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSObject { + switch value { + case let .object(ref): return ref + default: + throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column) + } +} + +func expectArray(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSArray { + guard let array = value.array else { + throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column) + } + return array +} + +func expectFunction(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSFunction { + switch value { + case let .function(ref): return ref + default: + throw MessageError("Type of \(value) should be \"function\"", file: file, line: line, column: column) + } +} + +func expectBoolean(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Bool { + switch value { + case let .boolean(bool): return bool + default: + throw MessageError("Type of \(value) should be \"boolean\"", file: file, line: line, column: column) + } +} + +func expectNumber(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Double { + switch value { + case let .number(number): return number + default: + throw MessageError("Type of \(value) should be \"number\"", file: file, line: line, column: column) + } +} + +func expectString(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> String { + switch value { + case let .string(string): return String(string) + default: + throw MessageError("Type of \(value) should be \"string\"", file: file, line: line, column: column) + } +} + +func expectAsyncThrow(_ body: @autoclosure () async throws -> T, file: StaticString = #file, line: UInt = #line, column: UInt = #column) async throws -> Error { + do { + _ = try await body() + } catch { + return error + } + throw MessageError("Expect to throw an exception", file: file, line: line, column: column) +} + +func expectNotNil(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws { + switch value { + case .some: return + case .none: + throw MessageError("Expect a non-nil value", file: file, line: line, column: column) + } +} diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift new file mode 100644 index 000000000..788b92446 --- /dev/null +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -0,0 +1,94 @@ +import JavaScriptEventLoop +import JavaScriptKit + + +func entrypoint() async throws { + struct E: Error, Equatable { + let value: Int + } + + try await asyncTest("Task.init value") { + let handle = Task { 1 } + try expectEqual(await handle.value, 1) + } + + try await asyncTest("Task.init throws") { + let handle = Task { + throw E(value: 2) + } + let error = try await expectAsyncThrow(await handle.value) + let e = try expectCast(error, to: E.self) + try expectEqual(e, E(value: 2)) + } + + try await asyncTest("await resolved Promise") { + let p = JSPromise(resolver: { resolve in + resolve(.success(1)) + }) + try await expectEqual(p.value, 1) + } + + try await asyncTest("await rejected Promise") { + let p = JSPromise(resolver: { resolve in + resolve(.failure(.number(3))) + }) + let error = try await expectAsyncThrow(await p.value) + let jsValue = try expectCast(error, to: JSValue.self) + try expectEqual(jsValue, 3) + } + + try await asyncTest("Continuation") { + let value = await withUnsafeContinuation { cont in + cont.resume(returning: 1) + } + try expectEqual(value, 1) + } + + try await asyncTest("Task.sleep(_:)") { + await Task.sleep(1_000_000_000) + } + + // FIXME(katei): Somehow it doesn't work due to a mysterious unreachable inst + // at the end of thunk. + // This issue is not only on JS host environment, but also on standalone coop executor. + // try await asyncTest("Task.sleep(nanoseconds:)") { + // try await Task.sleep(nanoseconds: 1_000_000_000) + // } +} + + +// Note: Please define `USE_SWIFT_TOOLS_VERSION_NEWER_THAN_5_5` if the swift-tools-version is newer +// than 5.5 to avoid the linking issue. +#if USE_SWIFT_TOOLS_VERSION_NEWER_THAN_5_5 +// Workaround: The latest SwiftPM rename main entry point name of executable target +// to avoid conflicting "main" with test target since `swift-tools-version >= 5.5`. +// The main symbol is renamed to "{{module_name}}_main" and it's renamed again to be +// "main" when linking the executable target. The former renaming is done by Swift compiler, +// and the latter is done by linker, so SwiftPM passes some special linker flags for each platform. +// But SwiftPM assumes that wasm-ld supports it by returning an empty array instead of nil even though +// wasm-ld doesn't support it yet. +// ref: https://github.com/apple/swift-package-manager/blob/1be68e811d0d814ba7abbb8effee45f1e8e6ec0d/Sources/Build/BuildPlan.swift#L117-L126 +// So define an explicit "main" by @_cdecl +@_cdecl("main") +func main(argc: Int32, argv: Int32) -> Int32 { + JavaScriptEventLoop.installGlobalExecutor() + Task { + do { + try await entrypoint() + } catch { + print(error) + } + } + return 0 +} +#else +JavaScriptEventLoop.installGlobalExecutor() +Task { + do { + try await entrypoint() + } catch { + print(error) + } +} + +#endif diff --git a/IntegrationTests/bin/concurrency-tests.js b/IntegrationTests/bin/concurrency-tests.js new file mode 100644 index 000000000..2d705761f --- /dev/null +++ b/IntegrationTests/bin/concurrency-tests.js @@ -0,0 +1,6 @@ +const { startWasiTask } = require("../lib"); + +startWasiTask("./dist/ConcurrencyTests.wasm").catch((err) => { + console.log(err); + process.exit(1); +}); diff --git a/Package.swift b/Package.swift index 813fe7c2d..94d644b94 100644 --- a/Package.swift +++ b/Package.swift @@ -4,8 +4,15 @@ import PackageDescription let package = Package( name: "JavaScriptKit", + platforms: [ + // This package doesn't work on macOS host, but should be able to be built for it + // for developing on Xcode. This minimum version requirement is to prevent availability + // errors for Concurrency API, whose runtime support is shipped from macOS 12.0 + .macOS("12.0") + ], products: [ .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]), + .library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]), ], targets: [ .target( @@ -13,5 +20,10 @@ let package = Package( dependencies: ["_CJavaScriptKit"] ), .target(name: "_CJavaScriptKit"), + .target( + name: "JavaScriptEventLoop", + dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"] + ), + .target(name: "_CJavaScriptEventLoop"), ] ) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift new file mode 100644 index 000000000..667b02780 --- /dev/null +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -0,0 +1,108 @@ +import JavaScriptKit +import _CJavaScriptEventLoop + + +public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { + + /// A function that queues a given closure as a microtask into JavaScript event loop. + /// See also: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide + let queueMicrotask: @Sendable (@escaping () -> Void) -> Void + /// A function that invokes a given closure after a specified number of milliseconds. + let setTimeout: @Sendable (UInt64, @escaping () -> Void) -> Void + + /// A mutable state to manage internal job queue + /// Note that this should be guarded atomically when supporting multi-threaded environment. + var queueState = QueueState() + + private init( + queueTask: @Sendable @escaping (@escaping () -> Void) -> Void, + setTimeout: @Sendable @escaping (UInt64, @escaping () -> Void) -> Void + ) { + self.queueMicrotask = queueTask + self.setTimeout = setTimeout + } + + /// A singleton instance of the Executor + public static let shared: JavaScriptEventLoop = { + let promise = JSPromise(resolver: { resolver -> Void in + resolver(.success(.undefined)) + }) + let setTimeout = JSObject.global.setTimeout.function! + let eventLoop = JavaScriptEventLoop( + queueTask: { job in + // TODO(katei): Should prefer `queueMicrotask` if available? + // We should measure if there is performance advantage. + promise.then { _ in + job() + return JSValue.undefined + } + }, + setTimeout: { delay, job in + setTimeout(JSOneshotClosure { _ in + job() + return JSValue.undefined + }, delay) + } + ) + return eventLoop + }() + + /// Set JavaScript event loop based executor to be the global executor + /// Note that this should be called before any of the jobs are created. + /// This installation step will be unnecessary after the custom-executor will be introduced officially. + /// See also: https://github.com/rjmccall/swift-evolution/blob/custom-executors/proposals/0000-custom-executors.md#the-default-global-concurrent-executor + public static func installGlobalExecutor() { + typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) -> Void + let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in + JavaScriptEventLoop.shared.enqueue(job) + } + swift_task_enqueueGlobal_hook = unsafeBitCast(swift_task_enqueueGlobal_hook_impl, to: UnsafeMutableRawPointer?.self) + + typealias swift_task_enqueueGlobalWithDelay_hook_Fn = @convention(thin) (UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original) -> Void + let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = { delay, job, original in + JavaScriptEventLoop.shared.enqueue(job, withDelay: delay) + } + swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast(swift_task_enqueueGlobalWithDelay_hook_impl, to: UnsafeMutableRawPointer?.self) + + typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueMainExecutor_original) -> Void + let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in + JavaScriptEventLoop.shared.enqueue(job) + } + swift_task_enqueueMainExecutor_hook = unsafeBitCast(swift_task_enqueueMainExecutor_hook_impl, to: UnsafeMutableRawPointer?.self) + } + + private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { + let milliseconds = nanoseconds / 1_000_000 + setTimeout(milliseconds, { + job._runSynchronously(on: self.asUnownedSerialExecutor()) + }) + } + + public func enqueue(_ job: UnownedJob) { + insertJobQueue(job: job) + } + + public func asUnownedSerialExecutor() -> UnownedSerialExecutor { + return UnownedSerialExecutor(ordinary: self) + } +} + +public extension JSPromise { + /// Wait for the promise to complete, returning (or throwing) its result. + var value: JSValue { + get async throws { + try await withUnsafeThrowingContinuation { [self] continuation in + self.then( + success: { + continuation.resume(returning: $0) + return JSValue.undefined + }, + failure: { + continuation.resume(throwing: $0) + return JSValue.undefined + } + ) + } + } + } +} diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift new file mode 100644 index 000000000..2f862f0d4 --- /dev/null +++ b/Sources/JavaScriptEventLoop/JobQueue.swift @@ -0,0 +1,85 @@ +// This file contains the job queue implementation which re-order jobs based on their priority. +// The current implementation is much simple to be easily debugged, but should be re-implemented +// using priority queue ideally. + +import _CJavaScriptEventLoop + +struct QueueState: Sendable { + fileprivate var headJob: UnownedJob? = nil + fileprivate var isSpinning: Bool = false +} + +extension JavaScriptEventLoop { + + func insertJobQueue(job newJob: UnownedJob) { + withUnsafeMutablePointer(to: &queueState.headJob) { headJobPtr in + var position: UnsafeMutablePointer = headJobPtr + while let cur = position.pointee { + if cur.rawPriority < newJob.rawPriority { + newJob.nextInQueue().pointee = cur + position.pointee = newJob + return + } + position = cur.nextInQueue() + } + newJob.nextInQueue().pointee = nil + position.pointee = newJob + } + + // TODO: use CAS when supporting multi-threaded environment + if !queueState.isSpinning { + self.queueState.isSpinning = true + JavaScriptEventLoop.shared.queueMicrotask { + self.runAllJobs() + } + } + } + + func runAllJobs() { + assert(queueState.isSpinning) + + while let job = self.claimNextFromQueue() { + job._runSynchronously(on: self.asUnownedSerialExecutor()) + } + + queueState.isSpinning = false + } + + func claimNextFromQueue() -> UnownedJob? { + if let job = self.queueState.headJob { + self.queueState.headJob = job.nextInQueue().pointee + return job + } + return nil + } +} + +fileprivate extension UnownedJob { + private func asImpl() -> UnsafeMutablePointer<_CJavaScriptEventLoop.Job> { + unsafeBitCast(self, to: UnsafeMutablePointer<_CJavaScriptEventLoop.Job>.self) + } + + var flags: JobFlags { + JobFlags(bits: asImpl().pointee.Flags) + } + + var rawPriority: UInt32 { flags.priority } + + func nextInQueue() -> UnsafeMutablePointer { + return withUnsafeMutablePointer(to: &asImpl().pointee.SchedulerPrivate.0) { rawNextJobPtr in + let nextJobPtr = UnsafeMutableRawPointer(rawNextJobPtr).bindMemory(to: UnownedJob?.self, capacity: 1) + return nextJobPtr + } + } + +} + +fileprivate struct JobFlags { + var bits: UInt32 = 0 + + var priority: UInt32 { + get { + (bits & 0xFF00) >> 8 + } + } +} diff --git a/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c b/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c new file mode 100644 index 000000000..e69de29bb diff --git a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h new file mode 100644 index 000000000..51c98afc6 --- /dev/null +++ b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h @@ -0,0 +1,46 @@ +#ifndef _CJavaScriptEventLoop_h +#define _CJavaScriptEventLoop_h + +#include +#include + +#define SWIFT_CC(CC) SWIFT_CC_##CC +#define SWIFT_CC_swift __attribute__((swiftcall)) + +#define SWIFT_EXPORT_FROM(LIBRARY) __attribute__((__visibility__("default"))) + +/// A schedulable unit +/// Note that this type layout is a part of public ABI, so we expect this field layout won't break in the future versions. +/// Current implementation refers the `swift-5.5-RELEASE` implementation. +/// https://github.com/apple/swift/blob/swift-5.5-RELEASE/include/swift/ABI/Task.h#L43-L129 +/// This definition is used to retrieve priority value of a job. After custom-executor API will be introduced officially, +/// the job priority API will be provided in the Swift world. +typedef __attribute__((aligned(2 * alignof(void *)))) struct { + void *_Nonnull Metadata; + int32_t RefCounts; + void *_Nullable SchedulerPrivate[2]; + uint32_t Flags; +} Job; + +/// A hook to take over global enqueuing. +typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobal_original)( + Job *_Nonnull job); + +SWIFT_EXPORT_FROM(swift_Concurrency) +void *_Nullable swift_task_enqueueGlobal_hook; + +/// A hook to take over global enqueuing with delay. +typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDelay_original)( + unsigned long long delay, Job *_Nonnull job); +SWIFT_EXPORT_FROM(swift_Concurrency) +void *_Nullable swift_task_enqueueGlobalWithDelay_hook; + +unsigned long long foo; + +/// A hook to take over main executor enqueueing. +typedef SWIFT_CC(swift) void (*swift_task_enqueueMainExecutor_original)( + Job *_Nonnull job); +SWIFT_EXPORT_FROM(swift_Concurrency) +void *_Nullable swift_task_enqueueMainExecutor_hook; + +#endif diff --git a/Sources/_CJavaScriptEventLoop/include/module.modulemap b/Sources/_CJavaScriptEventLoop/include/module.modulemap new file mode 100644 index 000000000..915862112 --- /dev/null +++ b/Sources/_CJavaScriptEventLoop/include/module.modulemap @@ -0,0 +1,4 @@ +module _CJavaScriptEventLoop { + header "_CJavaScriptEventLoop.h" + export * +} From 35aa907b30af5cb7fd90c5374325c9c715e4be56 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 4 Oct 2021 08:18:10 +0900 Subject: [PATCH 2/6] Run Concurrency tests on CI --- IntegrationTests/Makefile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/IntegrationTests/Makefile b/IntegrationTests/Makefile index c00efb1be..6e1a4dd05 100644 --- a/IntegrationTests/Makefile +++ b/IntegrationTests/Makefile @@ -30,8 +30,13 @@ run_benchmark: .PHONY: benchmark benchmark: benchmark_setup run_benchmark -.PHONY: test -test: build_rt dist/PrimaryTests.wasm +.PHONY: primary_test +primary_test: build_rt dist/PrimaryTests.wasm node bin/primary-tests.js + +.PHONY: concurrency_test concurrency_test: build_rt dist/ConcurrencyTests.wasm node bin/concurrency-tests.js + +.PHONY: test +test: concurrency_test primary_test From a7e96044673f2613a34f0dfcd33932c99b67b955 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 4 Oct 2021 08:29:19 +0900 Subject: [PATCH 3/6] Skip concurrency test for older toolchains --- .../TestSuites/Sources/ConcurrencyTests/main.swift | 4 ++++ Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 3 +++ Sources/JavaScriptEventLoop/JobQueue.swift | 2 ++ 3 files changed, 9 insertions(+) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index 788b92446..f93619736 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -1,6 +1,7 @@ import JavaScriptEventLoop import JavaScriptKit +#if compiler(>=5.5) func entrypoint() async throws { struct E: Error, Equatable { @@ -92,3 +93,6 @@ Task { } #endif + + +#endif diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 667b02780..6448633b7 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -1,6 +1,7 @@ import JavaScriptKit import _CJavaScriptEventLoop +#if compiler(>=5.5) public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { @@ -106,3 +107,5 @@ public extension JSPromise { } } } + +#endif diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift index 2f862f0d4..5030a6ca7 100644 --- a/Sources/JavaScriptEventLoop/JobQueue.swift +++ b/Sources/JavaScriptEventLoop/JobQueue.swift @@ -4,6 +4,7 @@ import _CJavaScriptEventLoop +#if compiler(>=5.5) struct QueueState: Sendable { fileprivate var headJob: UnownedJob? = nil fileprivate var isSpinning: Bool = false @@ -83,3 +84,4 @@ fileprivate struct JobFlags { } } } +#endif From 4cec05c2f3c95b6c956df153c570e6b94ad13d92 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 4 Oct 2021 08:40:25 +0900 Subject: [PATCH 4/6] Add more tests for concurrency --- .../Sources/ConcurrencyTests/main.swift | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index f93619736..9d8483117 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -1,5 +1,10 @@ import JavaScriptEventLoop import JavaScriptKit +#if canImport(WASILibc) +import WASILibc +#elseif canImport(Darwin) +import Darwin +#endif #if compiler(>=5.5) @@ -43,12 +48,55 @@ func entrypoint() async throws { cont.resume(returning: 1) } try expectEqual(value, 1) + + let error = try await expectAsyncThrow( + try await withUnsafeThrowingContinuation { (cont: UnsafeContinuation) in + cont.resume(throwing: E(value: 2)) + } + ) + let e = try expectCast(error, to: E.self) + try expectEqual(e.value, 2) } try await asyncTest("Task.sleep(_:)") { - await Task.sleep(1_000_000_000) + let start = time(nil) + await Task.sleep(2_000_000_000) + let diff = difftime(time(nil), start); + try expectEqual(diff >= 2, true) } + try await asyncTest("Job reordering based on priority") { + class Context: @unchecked Sendable { + var completed: [String] = [] + } + let context = Context() + + // When no priority, they should be ordered by the enqueued order + let t1 = Task(priority: nil) { + context.completed.append("t1") + } + let t2 = Task(priority: nil) { + context.completed.append("t2") + } + + _ = await (t1.value, t2.value) + try expectEqual(context.completed, ["t1", "t2"]) + + context.completed = [] + // When high priority is enqueued after a low one, they should be re-ordered + let t3 = Task(priority: .low) { + context.completed.append("t3") + } + let t4 = Task(priority: .high) { + context.completed.append("t4") + } + let t5 = Task(priority: .low) { + context.completed.append("t5") + } + + _ = await (t3.value, t4.value, t5.value) + try expectEqual(context.completed, ["t4", "t3", "t5"]) + } // FIXME(katei): Somehow it doesn't work due to a mysterious unreachable inst // at the end of thunk. // This issue is not only on JS host environment, but also on standalone coop executor. From eb9122cee800a1f354d91a24329d22f9295cabde Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 4 Oct 2021 08:41:25 +0900 Subject: [PATCH 5/6] Use the latest snapshot --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55e793e7a..5e72d38a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: toolchain: - wasm-5.3.1-RELEASE - wasm-5.4.0-RELEASE - - wasm-5.5-SNAPSHOT-2021-09-01-a + - wasm-5.5-SNAPSHOT-2021-10-02-a runs-on: ${{ matrix.os }} steps: - name: Checkout From 620a40116ed56183137bc10eef2572fda9d6ece1 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 4 Oct 2021 08:43:12 +0900 Subject: [PATCH 6/6] Add missing compile-time condition to support older toolchain --- .../TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift index 011fb7443..1557764a2 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift @@ -1,5 +1,6 @@ import JavaScriptKit +#if compiler(>=5.5) var printTestNames = false // Uncomment the next line to print the name of each test suite before running it. // This will make it easier to debug any errors that occur on the JS side. @@ -122,3 +123,5 @@ func expectNotNil(_ value: T?, file: StaticString = #file, line: UInt = #line throw MessageError("Expect a non-nil value", file: file, line: line, column: column) } } + +#endif