Skip to content

Commit b9a2573

Browse files
authoredOct 3, 2021
Experimental global executor cooperating with JS event loop (#141)
* Experimental global executor cooperating with JS event loop * Run Concurrency tests on CI * Skip concurrency test for older toolchains * Add more tests for concurrency * Use the latest snapshot * Add missing compile-time condition to support older toolchain
1 parent aa521a1 commit b9a2573

File tree

12 files changed

+564
-3
lines changed

12 files changed

+564
-3
lines changed
 

‎.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
toolchain:
1313
- wasm-5.3.1-RELEASE
1414
- wasm-5.4.0-RELEASE
15-
- wasm-5.5-SNAPSHOT-2021-09-01-a
15+
- wasm-5.5-SNAPSHOT-2021-10-02-a
1616
runs-on: ${{ matrix.os }}
1717
steps:
1818
- name: Checkout

‎IntegrationTests/Makefile

+9-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ run_benchmark:
3030
.PHONY: benchmark
3131
benchmark: benchmark_setup run_benchmark
3232

33-
.PHONY: test
34-
test: build_rt dist/PrimaryTests.wasm
33+
.PHONY: primary_test
34+
primary_test: build_rt dist/PrimaryTests.wasm
3535
node bin/primary-tests.js
36+
37+
.PHONY: concurrency_test
38+
concurrency_test: build_rt dist/ConcurrencyTests.wasm
39+
node bin/concurrency-tests.js
40+
41+
.PHONY: test
42+
test: concurrency_test primary_test

‎IntegrationTests/TestSuites/Package.swift

+15
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,32 @@ import PackageDescription
44

55
let package = Package(
66
name: "TestSuites",
7+
platforms: [
8+
// This package doesn't work on macOS host, but should be able to be built for it
9+
// for developing on Xcode. This minimum version requirement is to prevent availability
10+
// errors for Concurrency API, whose runtime support is shipped from macOS 12.0
11+
.macOS("12.0")
12+
],
713
products: [
814
.executable(
915
name: "PrimaryTests", targets: ["PrimaryTests"]
1016
),
17+
.executable(
18+
name: "ConcurrencyTests", targets: ["ConcurrencyTests"]
19+
),
1120
.executable(
1221
name: "BenchmarkTests", targets: ["BenchmarkTests"]
1322
),
1423
],
1524
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
1625
targets: [
1726
.target(name: "PrimaryTests", dependencies: ["JavaScriptKit"]),
27+
.target(
28+
name: "ConcurrencyTests",
29+
dependencies: [
30+
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
31+
]
32+
),
1833
.target(name: "BenchmarkTests", dependencies: ["JavaScriptKit"]),
1934
]
2035
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import JavaScriptKit
2+
3+
#if compiler(>=5.5)
4+
var printTestNames = false
5+
// Uncomment the next line to print the name of each test suite before running it.
6+
// This will make it easier to debug any errors that occur on the JS side.
7+
//printTestNames = true
8+
9+
func test(_ name: String, testBlock: () throws -> Void) throws {
10+
if printTestNames { print(name) }
11+
do {
12+
try testBlock()
13+
} catch {
14+
print("Error in \(name)")
15+
print(error)
16+
throw error
17+
}
18+
}
19+
20+
func asyncTest(_ name: String, testBlock: () async throws -> Void) async throws -> Void {
21+
if printTestNames { print(name) }
22+
do {
23+
try await testBlock()
24+
} catch {
25+
print("Error in \(name)")
26+
print(error)
27+
throw error
28+
}
29+
}
30+
31+
struct MessageError: Error {
32+
let message: String
33+
let file: StaticString
34+
let line: UInt
35+
let column: UInt
36+
init(_ message: String, file: StaticString, line: UInt, column: UInt) {
37+
self.message = message
38+
self.file = file
39+
self.line = line
40+
self.column = column
41+
}
42+
}
43+
44+
func expectEqual<T: Equatable>(
45+
_ lhs: T, _ rhs: T,
46+
file: StaticString = #file, line: UInt = #line, column: UInt = #column
47+
) throws {
48+
if lhs != rhs {
49+
throw MessageError("Expect to be equal \"\(lhs)\" and \"\(rhs)\"", file: file, line: line, column: column)
50+
}
51+
}
52+
53+
func expectCast<T, U>(
54+
_ value: T, to type: U.Type = U.self,
55+
file: StaticString = #file, line: UInt = #line, column: UInt = #column
56+
) throws -> U {
57+
guard let value = value as? U else {
58+
throw MessageError("Expect \"\(value)\" to be \(U.self)", file: file, line: line, column: column)
59+
}
60+
return value
61+
}
62+
63+
func expectObject(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSObject {
64+
switch value {
65+
case let .object(ref): return ref
66+
default:
67+
throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column)
68+
}
69+
}
70+
71+
func expectArray(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSArray {
72+
guard let array = value.array else {
73+
throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column)
74+
}
75+
return array
76+
}
77+
78+
func expectFunction(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSFunction {
79+
switch value {
80+
case let .function(ref): return ref
81+
default:
82+
throw MessageError("Type of \(value) should be \"function\"", file: file, line: line, column: column)
83+
}
84+
}
85+
86+
func expectBoolean(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Bool {
87+
switch value {
88+
case let .boolean(bool): return bool
89+
default:
90+
throw MessageError("Type of \(value) should be \"boolean\"", file: file, line: line, column: column)
91+
}
92+
}
93+
94+
func expectNumber(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Double {
95+
switch value {
96+
case let .number(number): return number
97+
default:
98+
throw MessageError("Type of \(value) should be \"number\"", file: file, line: line, column: column)
99+
}
100+
}
101+
102+
func expectString(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> String {
103+
switch value {
104+
case let .string(string): return String(string)
105+
default:
106+
throw MessageError("Type of \(value) should be \"string\"", file: file, line: line, column: column)
107+
}
108+
}
109+
110+
func expectAsyncThrow<T>(_ body: @autoclosure () async throws -> T, file: StaticString = #file, line: UInt = #line, column: UInt = #column) async throws -> Error {
111+
do {
112+
_ = try await body()
113+
} catch {
114+
return error
115+
}
116+
throw MessageError("Expect to throw an exception", file: file, line: line, column: column)
117+
}
118+
119+
func expectNotNil<T>(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws {
120+
switch value {
121+
case .some: return
122+
case .none:
123+
throw MessageError("Expect a non-nil value", file: file, line: line, column: column)
124+
}
125+
}
126+
127+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import JavaScriptEventLoop
2+
import JavaScriptKit
3+
#if canImport(WASILibc)
4+
import WASILibc
5+
#elseif canImport(Darwin)
6+
import Darwin
7+
#endif
8+
9+
#if compiler(>=5.5)
10+
11+
func entrypoint() async throws {
12+
struct E: Error, Equatable {
13+
let value: Int
14+
}
15+
16+
try await asyncTest("Task.init value") {
17+
let handle = Task { 1 }
18+
try expectEqual(await handle.value, 1)
19+
}
20+
21+
try await asyncTest("Task.init throws") {
22+
let handle = Task {
23+
throw E(value: 2)
24+
}
25+
let error = try await expectAsyncThrow(await handle.value)
26+
let e = try expectCast(error, to: E.self)
27+
try expectEqual(e, E(value: 2))
28+
}
29+
30+
try await asyncTest("await resolved Promise") {
31+
let p = JSPromise(resolver: { resolve in
32+
resolve(.success(1))
33+
})
34+
try await expectEqual(p.value, 1)
35+
}
36+
37+
try await asyncTest("await rejected Promise") {
38+
let p = JSPromise(resolver: { resolve in
39+
resolve(.failure(.number(3)))
40+
})
41+
let error = try await expectAsyncThrow(await p.value)
42+
let jsValue = try expectCast(error, to: JSValue.self)
43+
try expectEqual(jsValue, 3)
44+
}
45+
46+
try await asyncTest("Continuation") {
47+
let value = await withUnsafeContinuation { cont in
48+
cont.resume(returning: 1)
49+
}
50+
try expectEqual(value, 1)
51+
52+
let error = try await expectAsyncThrow(
53+
try await withUnsafeThrowingContinuation { (cont: UnsafeContinuation<Never, Error>) in
54+
cont.resume(throwing: E(value: 2))
55+
}
56+
)
57+
let e = try expectCast(error, to: E.self)
58+
try expectEqual(e.value, 2)
59+
}
60+
61+
try await asyncTest("Task.sleep(_:)") {
62+
let start = time(nil)
63+
await Task.sleep(2_000_000_000)
64+
let diff = difftime(time(nil), start);
65+
try expectEqual(diff >= 2, true)
66+
}
67+
68+
try await asyncTest("Job reordering based on priority") {
69+
class Context: @unchecked Sendable {
70+
var completed: [String] = []
71+
}
72+
let context = Context()
73+
74+
// When no priority, they should be ordered by the enqueued order
75+
let t1 = Task(priority: nil) {
76+
context.completed.append("t1")
77+
}
78+
let t2 = Task(priority: nil) {
79+
context.completed.append("t2")
80+
}
81+
82+
_ = await (t1.value, t2.value)
83+
try expectEqual(context.completed, ["t1", "t2"])
84+
85+
context.completed = []
86+
// When high priority is enqueued after a low one, they should be re-ordered
87+
let t3 = Task(priority: .low) {
88+
context.completed.append("t3")
89+
}
90+
let t4 = Task(priority: .high) {
91+
context.completed.append("t4")
92+
}
93+
let t5 = Task(priority: .low) {
94+
context.completed.append("t5")
95+
}
96+
97+
_ = await (t3.value, t4.value, t5.value)
98+
try expectEqual(context.completed, ["t4", "t3", "t5"])
99+
}
100+
// FIXME(katei): Somehow it doesn't work due to a mysterious unreachable inst
101+
// at the end of thunk.
102+
// This issue is not only on JS host environment, but also on standalone coop executor.
103+
// try await asyncTest("Task.sleep(nanoseconds:)") {
104+
// try await Task.sleep(nanoseconds: 1_000_000_000)
105+
// }
106+
}
107+
108+
109+
// Note: Please define `USE_SWIFT_TOOLS_VERSION_NEWER_THAN_5_5` if the swift-tools-version is newer
110+
// than 5.5 to avoid the linking issue.
111+
#if USE_SWIFT_TOOLS_VERSION_NEWER_THAN_5_5
112+
// Workaround: The latest SwiftPM rename main entry point name of executable target
113+
// to avoid conflicting "main" with test target since `swift-tools-version >= 5.5`.
114+
// The main symbol is renamed to "{{module_name}}_main" and it's renamed again to be
115+
// "main" when linking the executable target. The former renaming is done by Swift compiler,
116+
// and the latter is done by linker, so SwiftPM passes some special linker flags for each platform.
117+
// But SwiftPM assumes that wasm-ld supports it by returning an empty array instead of nil even though
118+
// wasm-ld doesn't support it yet.
119+
// ref: https://github.com/apple/swift-package-manager/blob/1be68e811d0d814ba7abbb8effee45f1e8e6ec0d/Sources/Build/BuildPlan.swift#L117-L126
120+
// So define an explicit "main" by @_cdecl
121+
@_cdecl("main")
122+
func main(argc: Int32, argv: Int32) -> Int32 {
123+
JavaScriptEventLoop.installGlobalExecutor()
124+
Task {
125+
do {
126+
try await entrypoint()
127+
} catch {
128+
print(error)
129+
}
130+
}
131+
return 0
132+
}
133+
#else
134+
JavaScriptEventLoop.installGlobalExecutor()
135+
Task {
136+
do {
137+
try await entrypoint()
138+
} catch {
139+
print(error)
140+
}
141+
}
142+
143+
#endif
144+
145+
146+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const { startWasiTask } = require("../lib");
2+
3+
startWasiTask("./dist/ConcurrencyTests.wasm").catch((err) => {
4+
console.log(err);
5+
process.exit(1);
6+
});

‎Package.swift

+12
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,26 @@ import PackageDescription
44

55
let package = Package(
66
name: "JavaScriptKit",
7+
platforms: [
8+
// This package doesn't work on macOS host, but should be able to be built for it
9+
// for developing on Xcode. This minimum version requirement is to prevent availability
10+
// errors for Concurrency API, whose runtime support is shipped from macOS 12.0
11+
.macOS("12.0")
12+
],
713
products: [
814
.library(name: "JavaScriptKit", targets: ["JavaScriptKit"]),
15+
.library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]),
916
],
1017
targets: [
1118
.target(
1219
name: "JavaScriptKit",
1320
dependencies: ["_CJavaScriptKit"]
1421
),
1522
.target(name: "_CJavaScriptKit"),
23+
.target(
24+
name: "JavaScriptEventLoop",
25+
dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"]
26+
),
27+
.target(name: "_CJavaScriptEventLoop"),
1628
]
1729
)

0 commit comments

Comments
 (0)