Skip to content

Experimental global executor cooperating with JS event loop #141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions IntegrationTests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +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
15 changes: 15 additions & 0 deletions IntegrationTests/TestSuites/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,32 @@ 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"]
),
],
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"]),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
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.
//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<T: Equatable>(
_ 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<T, U>(
_ 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<T>(_ 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<T>(_ 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)
}
}

#endif
146 changes: 146 additions & 0 deletions IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import JavaScriptEventLoop
import JavaScriptKit
#if canImport(WASILibc)
import WASILibc
#elseif canImport(Darwin)
import Darwin
#endif

#if compiler(>=5.5)

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)

let error = try await expectAsyncThrow(
try await withUnsafeThrowingContinuation { (cont: UnsafeContinuation<Never, Error>) 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(_:)") {
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.
// 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


#endif
6 changes: 6 additions & 0 deletions IntegrationTests/bin/concurrency-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { startWasiTask } = require("../lib");

startWasiTask("./dist/ConcurrencyTests.wasm").catch((err) => {
console.log(err);
process.exit(1);
});
12 changes: 12 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,26 @@ 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(
name: "JavaScriptKit",
dependencies: ["_CJavaScriptKit"]
),
.target(name: "_CJavaScriptKit"),
.target(
name: "JavaScriptEventLoop",
dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"]
),
.target(name: "_CJavaScriptEventLoop"),
]
)
Loading