Skip to content

Commit 26c3f30

Browse files
Merge pull request #292 from swiftwasm/yt/transfer
Add a mechanism to "Transfer" JSObject between Workers
2 parents a732a0c + f5e3a95 commit 26c3f30

File tree

26 files changed

+2130
-149
lines changed

26 files changed

+2130
-149
lines changed

Examples/Multithreading/README.md

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
# Multithreading example
22

3-
Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` from [swift.org/install](https://www.swift.org/install/) and run the following commands:
3+
Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` or later from [swift.org/install](https://www.swift.org/install/) and run the following commands:
44

55
```sh
6-
$ swift sdk install https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-07-09-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-07-09-a-wasm32-unknown-wasip1-threads.artifactbundle.zip
6+
$ (
7+
set -eo pipefail; \
8+
V="$(swiftc --version | head -n1)"; \
9+
TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
10+
curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
11+
jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x
12+
)
13+
$ export SWIFT_SDK_ID=$(
14+
V="$(swiftc --version | head -n1)"; \
15+
TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
16+
curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
17+
jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"]["id"]'
18+
)
719
$ ./build.sh
820
$ npx serve
921
```

Examples/Multithreading/Sources/JavaScript/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ class ThreadRegistry {
2727
};
2828
}
2929

30-
postMessageToWorkerThread(tid, data) {
30+
postMessageToWorkerThread(tid, data, transfer) {
3131
const worker = this.workers.get(tid);
32-
worker.postMessage(data);
32+
worker.postMessage(data, transfer);
3333
}
3434

3535
terminateWorkerThread(tid) {

Examples/Multithreading/Sources/JavaScript/worker.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ self.onmessage = async (event) => {
55
const { instance, wasi, swiftRuntime } = await instantiate({
66
module,
77
threadChannel: {
8-
postMessageToMainThread: (message) => {
8+
postMessageToMainThread: (message, transfer) => {
99
// Send the job to the main thread
10-
postMessage(message);
10+
postMessage(message, transfer);
1111
},
1212
listenMessageFromMainThread: (listener) => {
1313
self.onmessage = (event) => listener(event.data);

Examples/Multithreading/build.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
swift build --swift-sdk DEVELOPMENT-SNAPSHOT-2024-07-09-a-wasm32-unknown-wasip1-threads -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv -c release -Xswiftc -g
1+
swift build --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasip1-threads}" -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv -c release -Xswiftc -g

Examples/OffscrenCanvas/.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc

Examples/OffscrenCanvas/Package.swift

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// swift-tools-version: 5.10
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "Example",
7+
platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")],
8+
dependencies: [
9+
.package(path: "../../"),
10+
],
11+
targets: [
12+
.executableTarget(
13+
name: "MyApp",
14+
dependencies: [
15+
.product(name: "JavaScriptKit", package: "JavaScriptKit"),
16+
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
17+
]
18+
),
19+
]
20+
)

Examples/OffscrenCanvas/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# OffscreenCanvas example
2+
3+
Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` or later from [swift.org/install](https://www.swift.org/install/) and run the following commands:
4+
5+
```sh
6+
$ (
7+
set -eo pipefail; \
8+
V="$(swiftc --version | head -n1)"; \
9+
TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
10+
curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
11+
jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x
12+
)
13+
$ export SWIFT_SDK_ID=$(
14+
V="$(swiftc --version | head -n1)"; \
15+
TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
16+
curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
17+
jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"]["id"]'
18+
)
19+
$ ./build.sh
20+
$ npx serve
21+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../Multithreading/Sources/JavaScript
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import JavaScriptEventLoop
2+
import JavaScriptKit
3+
4+
JavaScriptEventLoop.installGlobalExecutor()
5+
WebWorkerTaskExecutor.installGlobalExecutor()
6+
7+
protocol CanvasRenderer {
8+
func render(canvas: JSObject, size: Int) async throws
9+
}
10+
11+
struct BackgroundRenderer: CanvasRenderer {
12+
func render(canvas: JSObject, size: Int) async throws {
13+
let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
14+
let transfer = JSSending.transfer(canvas)
15+
let renderingTask = Task(executorPreference: executor) {
16+
let canvas = try await transfer.receive()
17+
try await renderAnimation(canvas: canvas, size: size)
18+
}
19+
await withTaskCancellationHandler {
20+
try? await renderingTask.value
21+
} onCancel: {
22+
renderingTask.cancel()
23+
}
24+
executor.terminate()
25+
}
26+
}
27+
28+
struct MainThreadRenderer: CanvasRenderer {
29+
func render(canvas: JSObject, size: Int) async throws {
30+
try await renderAnimation(canvas: canvas, size: size)
31+
}
32+
}
33+
34+
// FPS Counter for CSS animation
35+
func startFPSMonitor() {
36+
let fpsCounterElement = JSObject.global.document.getElementById("fps-counter").object!
37+
38+
var lastTime = JSObject.global.performance.now().number!
39+
var frames = 0
40+
41+
// Create a frame counter function
42+
func countFrame() {
43+
frames += 1
44+
let currentTime = JSObject.global.performance.now().number!
45+
let elapsed = currentTime - lastTime
46+
47+
if elapsed >= 1000 {
48+
let fps = Int(Double(frames) * 1000 / elapsed)
49+
fpsCounterElement.textContent = .string("FPS: \(fps)")
50+
frames = 0
51+
lastTime = currentTime
52+
}
53+
54+
// Request next frame
55+
_ = JSObject.global.requestAnimationFrame!(
56+
JSClosure { _ in
57+
countFrame()
58+
return .undefined
59+
})
60+
}
61+
62+
// Start counting
63+
countFrame()
64+
}
65+
66+
@MainActor
67+
func onClick(renderer: CanvasRenderer) async throws {
68+
let document = JSObject.global.document
69+
70+
let canvasContainerElement = document.getElementById("canvas-container").object!
71+
72+
// Remove all child elements from the canvas container
73+
for i in 0..<Int(canvasContainerElement.children.length.number!) {
74+
let child = canvasContainerElement.children[i]
75+
_ = canvasContainerElement.removeChild!(child)
76+
}
77+
78+
let canvasElement = document.createElement("canvas").object!
79+
_ = canvasContainerElement.appendChild!(canvasElement)
80+
81+
let size = 800
82+
canvasElement.width = .number(Double(size))
83+
canvasElement.height = .number(Double(size))
84+
85+
let offscreenCanvas = canvasElement.transferControlToOffscreen!().object!
86+
try await renderer.render(canvas: offscreenCanvas, size: size)
87+
}
88+
89+
func main() async throws {
90+
let renderButtonElement = JSObject.global.document.getElementById("render-button").object!
91+
let cancelButtonElement = JSObject.global.document.getElementById("cancel-button").object!
92+
let rendererSelectElement = JSObject.global.document.getElementById("renderer-select").object!
93+
94+
var renderingTask: Task<Void, Error>? = nil
95+
96+
// Start the FPS monitor for CSS animations
97+
startFPSMonitor()
98+
99+
_ = renderButtonElement.addEventListener!(
100+
"click",
101+
JSClosure { _ in
102+
renderingTask?.cancel()
103+
renderingTask = Task {
104+
let selectedValue = rendererSelectElement.value.string!
105+
let renderer: CanvasRenderer =
106+
selectedValue == "main" ? MainThreadRenderer() : BackgroundRenderer()
107+
try await onClick(renderer: renderer)
108+
}
109+
return JSValue.undefined
110+
})
111+
112+
_ = cancelButtonElement.addEventListener!(
113+
"click",
114+
JSClosure { _ in
115+
renderingTask?.cancel()
116+
return JSValue.undefined
117+
})
118+
}
119+
120+
Task {
121+
try await main()
122+
}
123+
124+
#if canImport(wasi_pthread)
125+
import wasi_pthread
126+
import WASILibc
127+
128+
/// Trick to avoid blocking the main thread. pthread_mutex_lock function is used by
129+
/// the Swift concurrency runtime.
130+
@_cdecl("pthread_mutex_lock")
131+
func pthread_mutex_lock(_ mutex: UnsafeMutablePointer<pthread_mutex_t>) -> Int32 {
132+
// DO NOT BLOCK MAIN THREAD
133+
var ret: Int32
134+
repeat {
135+
ret = pthread_mutex_trylock(mutex)
136+
} while ret == EBUSY
137+
return ret
138+
}
139+
#endif

0 commit comments

Comments
 (0)