From 2e8d04933aa373b8742f0f5eb3b69d86e4f59841 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Nov 2025 04:54:45 +0000 Subject: [PATCH 1/3] CI: Fix documentation redirect for "https://swiftwasm.org/JavaScriptKit" --- .github/workflows/test.yml | 3 ++- .gitignore | 1 + Utilities/prepare-gh-pages.sh | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100755 Utilities/prepare-gh-pages.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a07d73915..5df11d0aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -123,11 +123,12 @@ jobs: env: SWIFT_SDK_ID_wasm32_unknown_wasip1_threads: ${{ steps.setup-wasm32-unknown-wasip1-threads.outputs.swift-sdk-id }} SWIFT_SDK_ID_wasm32_unknown_wasip1: ${{ steps.setup-wasm32-unknown-wasip1.outputs.swift-sdk-id }} + - run: ./Utilities/prepare-gh-pages.sh - name: Upload static files as artifact id: deployment uses: actions/upload-pages-artifact@v4 with: - path: Examples/ + path: ./_site deploy-examples: runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/main' diff --git a/.gitignore b/.gitignore index a62100fde..8dba07278 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ Package.resolved Plugins/BridgeJS/Sources/JavaScript/package-lock.json Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/**/*.actual bridge-js.config.local.json +_site/ diff --git a/Utilities/prepare-gh-pages.sh b/Utilities/prepare-gh-pages.sh new file mode 100755 index 000000000..7260717a6 --- /dev/null +++ b/Utilities/prepare-gh-pages.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +mkdir -p ./_site +# Copy all files from ./Examples to ./_site, excluding specified path patterns +rsync -av --progress ./Examples/ ./_site/ --exclude=".build" +cat < _site/index.html + + + + + Redirecting... + + +

If you are not redirected automatically, follow this link to JavaScriptKit documentation.

+ + +EOF + From 27b8279bb20ace493631efdecfff60bbf9eb1ee7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Nov 2025 22:32:35 +0000 Subject: [PATCH 2/3] Refactor job scheduling queues --- .../ConcurrentBucketPriorityQueue.swift | 153 ++++++++++++++++++ Sources/JavaScriptEventLoop/Job.swift | 32 ++++ Sources/JavaScriptEventLoop/JobQueue.swift | 94 ----------- .../JavaScriptEventLoop/PriorityQueue.swift | 75 +++++++++ 4 files changed, 260 insertions(+), 94 deletions(-) create mode 100644 Sources/JavaScriptEventLoop/ConcurrentBucketPriorityQueue.swift create mode 100644 Sources/JavaScriptEventLoop/Job.swift delete mode 100644 Sources/JavaScriptEventLoop/JobQueue.swift create mode 100644 Sources/JavaScriptEventLoop/PriorityQueue.swift diff --git a/Sources/JavaScriptEventLoop/ConcurrentBucketPriorityQueue.swift b/Sources/JavaScriptEventLoop/ConcurrentBucketPriorityQueue.swift new file mode 100644 index 000000000..67e1c45e3 --- /dev/null +++ b/Sources/JavaScriptEventLoop/ConcurrentBucketPriorityQueue.swift @@ -0,0 +1,153 @@ +import Synchronization + +#if compiler(>=5.5) && canImport(Synchronization) + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class ConcurrentBucketPriorityQueue: @unchecked Sendable { + private struct JobList: Sendable { + var head: UnownedJob? = nil + var tail: UnownedJob? = nil + + mutating func enqueue(_ job: UnownedJob) { + job.nextInQueue().pointee = nil + if let tail { + tail.nextInQueue().pointee = job + self.tail = job + } else { + head = job + tail = job + } + } + + mutating func dequeue() -> UnownedJob? { + guard let job = head else { return nil } + head = job.nextInQueue().pointee + if head == nil { + tail = nil + } + job.nextInQueue().pointee = nil + return job + } + + var isEmpty: Bool { + head == nil + } + } + + private final class Bucket: @unchecked Sendable { + let lock = Mutex(JobList()) + } + + private final class OccupancyWord: @unchecked Sendable { + let value = Atomic(0) + } + + private let maxPriority: Int + private let buckets: [Bucket] + private let occupancy: [OccupancyWord] + + init(maxPriority: Int = 255) { + self.maxPriority = max(0, maxPriority) + let bucketCount = self.maxPriority + 1 + self.buckets = (0.. Bool { + let index = min(Int(job.rawPriority), maxPriority) + let (word, mask) = bitComponents(for: index) + guard buckets[index].lock.withLockIfAvailable({ list in + list.enqueue(job) + }) != nil else { + return false + } + setOccupancyBit(wordIndex: word, mask: mask) + return true + } + + func dequeue() -> UnownedJob? { + var wordIndex = occupancy.count - 1 + while wordIndex >= 0 { + var word = occupancy[wordIndex].value.load(ordering: .sequentiallyConsistent) + while word != 0 { + let leadingZeros = word.leadingZeroBitCount + if leadingZeros == UInt64.bitWidth { + break + } + let bitIndex = UInt64.bitWidth - 1 - leadingZeros + let mask = UInt64(1) << bitIndex + let bucketIndex = wordIndex * UInt64.bitWidth + Int(bitIndex) + if bucketIndex > maxPriority { + clearOccupancyBit(wordIndex: wordIndex, mask: mask) + word = occupancy[wordIndex].value.load(ordering: .sequentiallyConsistent) + continue + } + let job = buckets[bucketIndex].lock.withLock { list -> UnownedJob? in + guard let job = list.dequeue() else { + return nil + } + if list.isEmpty { + clearOccupancyBit(wordIndex: wordIndex, mask: mask) + } + return job + } + if let job { + return job + } + clearOccupancyBit(wordIndex: wordIndex, mask: mask) + word = occupancy[wordIndex].value.load(ordering: .sequentiallyConsistent) + } + wordIndex -= 1 + } + return nil + } + + var isEmpty: Bool { + for word in occupancy { + if word.value.load(ordering: .sequentiallyConsistent) != 0 { + return false + } + } + return true + } + + private func bitComponents(for priority: Int) -> (Int, UInt64) { + let clampedPriority = max(0, min(priority, maxPriority)) + let word = clampedPriority / UInt64.bitWidth + let bit = clampedPriority % UInt64.bitWidth + return (word, UInt64(1) << bit) + } + + private func setOccupancyBit(wordIndex: Int, mask: UInt64) { + while true { + let current = occupancy[wordIndex].value.load(ordering: .sequentiallyConsistent) + let desired = current | mask + let (exchanged, _) = occupancy[wordIndex].value.compareExchange( + expected: current, + desired: desired, + ordering: .sequentiallyConsistent + ) + if exchanged { return } + } + } + + private func clearOccupancyBit(wordIndex: Int, mask: UInt64) { + while true { + let current = occupancy[wordIndex].value.load(ordering: .sequentiallyConsistent) + let desired = current & ~mask + let (exchanged, _) = occupancy[wordIndex].value.compareExchange( + expected: current, + desired: desired, + ordering: .sequentiallyConsistent + ) + if exchanged { return } + } + } +} +#endif diff --git a/Sources/JavaScriptEventLoop/Job.swift b/Sources/JavaScriptEventLoop/Job.swift new file mode 100644 index 000000000..bc28c7395 --- /dev/null +++ b/Sources/JavaScriptEventLoop/Job.swift @@ -0,0 +1,32 @@ +import _CJavaScriptEventLoop + +#if compiler(>=5.5) + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension UnownedJob { + var rawPriority: UInt32 { flags.priority } + + func nextInQueue() -> UnsafeMutablePointer { + withUnsafeMutablePointer(to: &asImpl().pointee.SchedulerPrivate.0) { rawNextJobPtr in + let nextJobPtr = UnsafeMutableRawPointer(rawNextJobPtr).bindMemory(to: UnownedJob?.self, capacity: 1) + return nextJobPtr + } + } + + private func asImpl() -> UnsafeMutablePointer<_CJavaScriptEventLoop.Job> { + unsafeBitCast(self, to: UnsafeMutablePointer<_CJavaScriptEventLoop.Job>.self) + } + + private var flags: JobFlags { + JobFlags(bits: asImpl().pointee.Flags) + } +} + +private struct JobFlags { + var bits: UInt32 = 0 + + var priority: UInt32 { + (bits & 0xFF00) >> 8 + } +} +#endif diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift deleted file mode 100644 index a0f2c4bbb..000000000 --- a/Sources/JavaScriptEventLoop/JobQueue.swift +++ /dev/null @@ -1,94 +0,0 @@ -// 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 _Concurrency -import _CJavaScriptEventLoop - -#if compiler(>=5.5) - -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -struct QueueState: Sendable { - fileprivate var headJob: UnownedJob? = nil - fileprivate var isSpinning: Bool = false -} - -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -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() { - #if compiler(>=5.9) - job.runSynchronously(on: self.asUnownedSerialExecutor()) - #else - job._runSynchronously(on: self.asUnownedSerialExecutor()) - #endif - } - - queueState.isSpinning = false - } - - func claimNextFromQueue() -> UnownedJob? { - if let job = self.queueState.headJob { - self.queueState.headJob = job.nextInQueue().pointee - return job - } - return nil - } -} - -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -extension UnownedJob { - private func asImpl() -> UnsafeMutablePointer<_CJavaScriptEventLoop.Job> { - unsafeBitCast(self, to: UnsafeMutablePointer<_CJavaScriptEventLoop.Job>.self) - } - - fileprivate var flags: JobFlags { - JobFlags(bits: asImpl().pointee.Flags) - } - - fileprivate var rawPriority: UInt32 { flags.priority } - - fileprivate func nextInQueue() -> UnsafeMutablePointer { - return withUnsafeMutablePointer(to: &asImpl().pointee.SchedulerPrivate.0) { rawNextJobPtr in - let nextJobPtr = UnsafeMutableRawPointer(rawNextJobPtr).bindMemory(to: UnownedJob?.self, capacity: 1) - return nextJobPtr - } - } - -} - -private struct JobFlags { - var bits: UInt32 = 0 - - var priority: UInt32 { - (bits & 0xFF00) >> 8 - } -} -#endif diff --git a/Sources/JavaScriptEventLoop/PriorityQueue.swift b/Sources/JavaScriptEventLoop/PriorityQueue.swift new file mode 100644 index 000000000..662533655 --- /dev/null +++ b/Sources/JavaScriptEventLoop/PriorityQueue.swift @@ -0,0 +1,75 @@ +import _Concurrency + +#if compiler(>=5.5) + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +struct PriorityQueue: Sendable { + private var headJob: UnownedJob? = nil + + mutating func enqueue(_ newJob: UnownedJob) { + withUnsafeMutablePointer(to: &headJob) { headJobPtr in + var position: UnsafeMutablePointer = headJobPtr + while let current = position.pointee { + if current.rawPriority < newJob.rawPriority { + newJob.nextInQueue().pointee = current + position.pointee = newJob + return + } + position = current.nextInQueue() + } + newJob.nextInQueue().pointee = nil + position.pointee = newJob + } + } + + mutating func dequeue() -> UnownedJob? { + guard let job = headJob else { return nil } + headJob = job.nextInQueue().pointee + return job + } + + var isEmpty: Bool { + headJob == nil + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +struct QueueState: Sendable { + fileprivate var jobQueue = PriorityQueue() + fileprivate var isSpinning: Bool = false +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension JavaScriptEventLoop { + + func insertJobQueue(job newJob: UnownedJob) { + queueState.jobQueue.enqueue(newJob) + + // TODO: use CAS when supporting multi-threaded environment + if !queueState.isSpinning { + queueState.isSpinning = true + JavaScriptEventLoop.shared.queueMicrotask { + self.runAllJobs() + } + } + } + + func runAllJobs() { + assert(queueState.isSpinning) + + while let job = claimNextFromQueue() { + #if compiler(>=5.9) + job.runSynchronously(on: asUnownedSerialExecutor()) + #else + job._runSynchronously(on: asUnownedSerialExecutor()) + #endif + } + + queueState.isSpinning = false + } + + func claimNextFromQueue() -> UnownedJob? { + queueState.jobQueue.dequeue() + } +} +#endif From 0e16c02bf3395c1df7160ac820ed337498f40b5e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Nov 2025 22:41:05 +0000 Subject: [PATCH 3/3] Use bucket-based priority queue for event loop --- .../JavaScriptEventLoop/PriorityQueue.swift | 82 +++++++++++++++---- 1 file changed, 66 insertions(+), 16 deletions(-) diff --git a/Sources/JavaScriptEventLoop/PriorityQueue.swift b/Sources/JavaScriptEventLoop/PriorityQueue.swift index 662533655..5890fe7ad 100644 --- a/Sources/JavaScriptEventLoop/PriorityQueue.swift +++ b/Sources/JavaScriptEventLoop/PriorityQueue.swift @@ -4,32 +4,82 @@ import _Concurrency @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) struct PriorityQueue: Sendable { - private var headJob: UnownedJob? = nil + private struct Bucket: Sendable { + var storage: [UnownedJob] = [] + var head: Int = 0 + + var isEmpty: Bool { + head >= storage.count + } + + mutating func enqueue(_ job: UnownedJob) { + storage.append(job) + } + + mutating func dequeue() -> UnownedJob? { + guard head < storage.count else { return nil } + let job = storage[head] + head += 1 + if head >= 32 && head * 2 >= storage.count { + storage.removeFirst(head) + head = 0 + } + return job + } + } + + private static let maxPriority = Int(UInt8.max) + private var buckets: [Bucket] = Array(repeating: Bucket(), count: maxPriority + 1) + private var highestNonEmpty: Int? = nil mutating func enqueue(_ newJob: UnownedJob) { - withUnsafeMutablePointer(to: &headJob) { headJobPtr in - var position: UnsafeMutablePointer = headJobPtr - while let current = position.pointee { - if current.rawPriority < newJob.rawPriority { - newJob.nextInQueue().pointee = current - position.pointee = newJob - return - } - position = current.nextInQueue() + let index = bucketIndex(for: newJob) + buckets[index].enqueue(newJob) + if let current = highestNonEmpty { + if index > current { + highestNonEmpty = index } - newJob.nextInQueue().pointee = nil - position.pointee = newJob + } else { + highestNonEmpty = index } } mutating func dequeue() -> UnownedJob? { - guard let job = headJob else { return nil } - headJob = job.nextInQueue().pointee - return job + guard var index = highestNonEmpty else { return nil } + while index >= 0 { + if let job = buckets[index].dequeue() { + highestNonEmpty = buckets[index].isEmpty ? nextNonEmptyBucket(from: index - 1) : index + return job + } + index -= 1 + } + highestNonEmpty = nil + return nil } var isEmpty: Bool { - headJob == nil + highestNonEmpty == nil + } + + private func bucketIndex(for job: UnownedJob) -> Int { + let priority = Int(job.rawPriority) + if priority > PriorityQueue.maxPriority { + return PriorityQueue.maxPriority + } else if priority < 0 { + return 0 + } + return priority + } + + private mutating func nextNonEmptyBucket(from start: Int) -> Int? { + var index = start + while index >= 0 { + if !buckets[index].isEmpty { + return index + } + index -= 1 + } + return nil } }