Skip to content
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

Add a mechanism to "Transfer" JSObject between Workers #292

Merged
merged 19 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
28d5ec0
Add `JSObject.transfer` and `JSObject.receive` APIs
kateinoigakukun Mar 10, 2025
e406cd3
Stop hardcoding the Swift toolchain version in the Multithreading exa…
kateinoigakukun Mar 10, 2025
cfa1b2d
Update Multithreading example to support transferable objects
kateinoigakukun Mar 10, 2025
9d335a8
Add OffscreenCanvas example
kateinoigakukun Mar 10, 2025
98cec71
Rename `JSObject.receive` to `JSObject.Transferring.receive`
kateinoigakukun Mar 10, 2025
9b84176
Update test harness to support transferring
kateinoigakukun Mar 10, 2025
c481614
Fix JSObject lifetime issue while transferring
kateinoigakukun Mar 10, 2025
65ddcd3
Add basic tests for transferring objects between threads
kateinoigakukun Mar 10, 2025
f0bd60c
Fix native build
kateinoigakukun Mar 10, 2025
8d4bba6
Add cautionary notes to the documentation of `JSObject.transfer()`.
kateinoigakukun Mar 10, 2025
09d5311
Rename `JSObject.Transferring` to `JSTransferring<T>`
kateinoigakukun Mar 11, 2025
f25bfec
MessageBroker
kateinoigakukun Mar 11, 2025
58f91c3
Relax deinit requirement
kateinoigakukun Mar 11, 2025
2a081de
Remove dead code and fix error message
kateinoigakukun Mar 11, 2025
4fe37e7
Rename JSTransferring to JSSending
kateinoigakukun Mar 11, 2025
eeff111
Add `JSSending.receive(...)` to receive multiple objects at once
kateinoigakukun Mar 11, 2025
44a5dba
Build fix
kateinoigakukun Mar 11, 2025
b678f71
Skip multi-transfer tests
kateinoigakukun Mar 11, 2025
f5e3a95
Rename JSObject+Transferring.swift to JSSending.swift
kateinoigakukun Mar 11, 2025
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
16 changes: 14 additions & 2 deletions Examples/Multithreading/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
# Multithreading example

Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` from [swift.org/install](https://www.swift.org/install/) and run the following commands:
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:

```sh
$ 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
$ (
set -eo pipefail; \
V="$(swiftc --version | head -n1)"; \
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]')"; \
curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x
)
$ export SWIFT_SDK_ID=$(
V="$(swiftc --version | head -n1)"; \
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]')"; \
curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"]["id"]'
)
$ ./build.sh
$ npx serve
```
4 changes: 2 additions & 2 deletions Examples/Multithreading/Sources/JavaScript/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ class ThreadRegistry {
};
}

postMessageToWorkerThread(tid, data) {
postMessageToWorkerThread(tid, data, transfer) {
const worker = this.workers.get(tid);
worker.postMessage(data);
worker.postMessage(data, transfer);
}

terminateWorkerThread(tid) {
Expand Down
4 changes: 2 additions & 2 deletions Examples/Multithreading/Sources/JavaScript/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ self.onmessage = async (event) => {
const { instance, wasi, swiftRuntime } = await instantiate({
module,
threadChannel: {
postMessageToMainThread: (message) => {
postMessageToMainThread: (message, transfer) => {
// Send the job to the main thread
postMessage(message);
postMessage(message, transfer);
},
listenMessageFromMainThread: (listener) => {
self.onmessage = (event) => listener(event.data);
Expand Down
2 changes: 1 addition & 1 deletion Examples/Multithreading/build.sh
Original file line number Diff line number Diff line change
@@ -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
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
8 changes: 8 additions & 0 deletions Examples/OffscrenCanvas/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
20 changes: 20 additions & 0 deletions Examples/OffscrenCanvas/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// swift-tools-version: 5.10

import PackageDescription

let package = Package(
name: "Example",
platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")],
dependencies: [
.package(path: "../../"),
],
targets: [
.executableTarget(
name: "MyApp",
dependencies: [
.product(name: "JavaScriptKit", package: "JavaScriptKit"),
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
]
),
]
)
21 changes: 21 additions & 0 deletions Examples/OffscrenCanvas/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# OffscreenCanvas example

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:

```sh
$ (
set -eo pipefail; \
V="$(swiftc --version | head -n1)"; \
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]')"; \
curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x
)
$ export SWIFT_SDK_ID=$(
V="$(swiftc --version | head -n1)"; \
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]')"; \
curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"]["id"]'
)
$ ./build.sh
$ npx serve
```
1 change: 1 addition & 0 deletions Examples/OffscrenCanvas/Sources/JavaScript
139 changes: 139 additions & 0 deletions Examples/OffscrenCanvas/Sources/MyApp/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import JavaScriptEventLoop
import JavaScriptKit

JavaScriptEventLoop.installGlobalExecutor()
WebWorkerTaskExecutor.installGlobalExecutor()

protocol CanvasRenderer {
func render(canvas: JSObject, size: Int) async throws
}

struct BackgroundRenderer: CanvasRenderer {
func render(canvas: JSObject, size: Int) async throws {
let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
let transfer = JSSending.transfer(canvas)
let renderingTask = Task(executorPreference: executor) {
let canvas = try await transfer.receive()
try await renderAnimation(canvas: canvas, size: size)
}
await withTaskCancellationHandler {
try? await renderingTask.value
} onCancel: {
renderingTask.cancel()
}
executor.terminate()
}
}

struct MainThreadRenderer: CanvasRenderer {
func render(canvas: JSObject, size: Int) async throws {
try await renderAnimation(canvas: canvas, size: size)
}
}

// FPS Counter for CSS animation
func startFPSMonitor() {
let fpsCounterElement = JSObject.global.document.getElementById("fps-counter").object!

var lastTime = JSObject.global.performance.now().number!
var frames = 0

// Create a frame counter function
func countFrame() {
frames += 1
let currentTime = JSObject.global.performance.now().number!
let elapsed = currentTime - lastTime

if elapsed >= 1000 {
let fps = Int(Double(frames) * 1000 / elapsed)
fpsCounterElement.textContent = .string("FPS: \(fps)")
frames = 0
lastTime = currentTime
}

// Request next frame
_ = JSObject.global.requestAnimationFrame!(
JSClosure { _ in
countFrame()
return .undefined
})
}

// Start counting
countFrame()
}

@MainActor
func onClick(renderer: CanvasRenderer) async throws {
let document = JSObject.global.document

let canvasContainerElement = document.getElementById("canvas-container").object!

// Remove all child elements from the canvas container
for i in 0..<Int(canvasContainerElement.children.length.number!) {
let child = canvasContainerElement.children[i]
_ = canvasContainerElement.removeChild!(child)
}

let canvasElement = document.createElement("canvas").object!
_ = canvasContainerElement.appendChild!(canvasElement)

let size = 800
canvasElement.width = .number(Double(size))
canvasElement.height = .number(Double(size))

let offscreenCanvas = canvasElement.transferControlToOffscreen!().object!
try await renderer.render(canvas: offscreenCanvas, size: size)
}

func main() async throws {
let renderButtonElement = JSObject.global.document.getElementById("render-button").object!
let cancelButtonElement = JSObject.global.document.getElementById("cancel-button").object!
let rendererSelectElement = JSObject.global.document.getElementById("renderer-select").object!

var renderingTask: Task<Void, Error>? = nil

// Start the FPS monitor for CSS animations
startFPSMonitor()

_ = renderButtonElement.addEventListener!(
"click",
JSClosure { _ in
renderingTask?.cancel()
renderingTask = Task {
let selectedValue = rendererSelectElement.value.string!
let renderer: CanvasRenderer =
selectedValue == "main" ? MainThreadRenderer() : BackgroundRenderer()
try await onClick(renderer: renderer)
}
return JSValue.undefined
})

_ = cancelButtonElement.addEventListener!(
"click",
JSClosure { _ in
renderingTask?.cancel()
return JSValue.undefined
})
}

Task {
try await main()
}

#if canImport(wasi_pthread)
import wasi_pthread
import WASILibc

/// Trick to avoid blocking the main thread. pthread_mutex_lock function is used by
/// the Swift concurrency runtime.
@_cdecl("pthread_mutex_lock")
func pthread_mutex_lock(_ mutex: UnsafeMutablePointer<pthread_mutex_t>) -> Int32 {
// DO NOT BLOCK MAIN THREAD
var ret: Int32
repeat {
ret = pthread_mutex_trylock(mutex)
} while ret == EBUSY
return ret
}
#endif
Loading