From a23bf889f787f603e71dd2882dde5229c329e142 Mon Sep 17 00:00:00 2001 From: admirsaheta Date: Thu, 28 Nov 2024 18:55:04 +0100 Subject: [PATCH 1/3] implement:priority-queue/support --- Sources/JavaScriptEventLoop/JobQueue.swift | 85 ++++++++++++---------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift index 5ad71f0a0..9a9734af9 100644 --- a/Sources/JavaScriptEventLoop/JobQueue.swift +++ b/Sources/JavaScriptEventLoop/JobQueue.swift @@ -1,11 +1,7 @@ // 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 _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 @@ -14,51 +10,66 @@ struct QueueState: Sendable { @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension JavaScriptEventLoop { + private var queueLock: NSLock { + NSLock() + } 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 - } + queueLock.lock() + defer { queueLock.unlock() } + + insertJob(newJob) - // TODO: use CAS when supporting multi-threaded environment if !queueState.isSpinning { - self.queueState.isSpinning = true + queueState.isSpinning = true JavaScriptEventLoop.shared.queueMicrotask { self.runAllJobs() } } } + private func insertJob(_ newJob: UnownedJob) { + var current = queueState.headJob + var previous: UnownedJob? = nil + + while let cur = current, cur.rawPriority >= newJob.rawPriority { + previous = cur + current = cur.nextInQueue().pointee + } + + newJob.nextInQueue().pointee = current + if let prev = previous { + prev.nextInQueue().pointee = newJob + } else { + queueState.headJob = newJob + } + } + 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 + while let job = claimNextFromQueue() { + executeJob(job) } queueState.isSpinning = false } + private func executeJob(_ job: UnownedJob) { + #if compiler(>=5.9) + job.runSynchronously(on: self.asUnownedSerialExecutor()) + #else + job._runSynchronously(on: self.asUnownedSerialExecutor()) + #endif + } + func claimNextFromQueue() -> UnownedJob? { - if let job = self.queueState.headJob { - self.queueState.headJob = job.nextInQueue().pointee - return job - } - return nil + queueLock.lock() + defer { queueLock.unlock() } + + guard let job = queueState.headJob else { return nil } + queueState.headJob = job.nextInQueue().pointee + return job } } @@ -75,21 +86,17 @@ fileprivate extension UnownedJob { var rawPriority: UInt32 { flags.priority } func nextInQueue() -> UnsafeMutablePointer { - return withUnsafeMutablePointer(to: &asImpl().pointee.SchedulerPrivate.0) { rawNextJobPtr in - let nextJobPtr = UnsafeMutableRawPointer(rawNextJobPtr).bindMemory(to: UnownedJob?.self, capacity: 1) - return nextJobPtr + withUnsafeMutablePointer(to: &asImpl().pointee.SchedulerPrivate.0) { rawNextJobPtr in + UnsafeMutableRawPointer(rawNextJobPtr).bindMemory(to: UnownedJob?.self, capacity: 1) } } - } fileprivate struct JobFlags { var bits: UInt32 = 0 - var priority: UInt32 { - get { - (bits & 0xFF00) >> 8 + var priority: UInt32 { + (bits & 0xFF00) >> 8 } - } } -#endif +#endif \ No newline at end of file From 0903691d1da6c8c872351896b5987dee9042e03a Mon Sep 17 00:00:00 2001 From: admirsaheta Date: Fri, 29 Nov 2024 10:18:36 +0100 Subject: [PATCH 2/3] add:docs --- Sources/JavaScriptEventLoop/JobQueue.swift | 37 ++++++++++++++++++---- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift index 9a9734af9..6556c78d0 100644 --- a/Sources/JavaScriptEventLoop/JobQueue.swift +++ b/Sources/JavaScriptEventLoop/JobQueue.swift @@ -1,19 +1,27 @@ -// This file contains the job queue implementation which re-order jobs based on their priority. +// This file contains the job queue implementation for JavaScriptEventLoop. +// It manages job insertion and execution based on priority. import _CJavaScriptEventLoop +import Foundation +/// Represents the state of the job queue. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) struct QueueState: Sendable { + /// The head of the job queue. fileprivate var headJob: UnownedJob? = nil + /// Indicates if the queue is actively processing jobs. fileprivate var isSpinning: Bool = false } @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension JavaScriptEventLoop { + /// A lock to synchronize queue access. private var queueLock: NSLock { NSLock() } + /// Inserts a job into the queue and ensures jobs are processed. + /// - Parameter job: The job to add to the queue. func insertJobQueue(job newJob: UnownedJob) { queueLock.lock() defer { queueLock.unlock() } @@ -28,6 +36,8 @@ extension JavaScriptEventLoop { } } + /// Inserts a job into the queue at the correct priority position. + /// - Parameter job: The job to insert into the queue. private func insertJob(_ newJob: UnownedJob) { var current = queueState.headJob var previous: UnownedJob? = nil @@ -45,8 +55,9 @@ extension JavaScriptEventLoop { } } + /// Processes all jobs in the queue until it is empty. func runAllJobs() { - assert(queueState.isSpinning) + assert(queueState.isSpinning, "runAllJobs called while queueState.isSpinning is false.") while let job = claimNextFromQueue() { executeJob(job) @@ -55,6 +66,8 @@ extension JavaScriptEventLoop { queueState.isSpinning = false } + /// Executes a specific job. + /// - Parameter job: The job to execute. private func executeJob(_ job: UnownedJob) { #if compiler(>=5.9) job.runSynchronously(on: self.asUnownedSerialExecutor()) @@ -63,6 +76,8 @@ extension JavaScriptEventLoop { #endif } + /// Removes and returns the next job from the queue. + /// - Returns: The next job in the queue, or `nil` if the queue is empty. func claimNextFromQueue() -> UnownedJob? { queueLock.lock() defer { queueLock.unlock() } @@ -75,16 +90,24 @@ extension JavaScriptEventLoop { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) fileprivate extension UnownedJob { + /// Converts the job to its internal implementation. + /// - Returns: A raw pointer to the job's internal structure. private func asImpl() -> UnsafeMutablePointer<_CJavaScriptEventLoop.Job> { unsafeBitCast(self, to: UnsafeMutablePointer<_CJavaScriptEventLoop.Job>.self) } + /// The job's priority flags. var flags: JobFlags { JobFlags(bits: asImpl().pointee.Flags) } - var rawPriority: UInt32 { flags.priority } + /// The raw priority value of the job. + var rawPriority: UInt32 { + flags.priority + } + /// Retrieves a pointer to the next job in the queue. + /// - Returns: A pointer to the next job, or `nil` if there are no further jobs. func nextInQueue() -> UnsafeMutablePointer { withUnsafeMutablePointer(to: &asImpl().pointee.SchedulerPrivate.0) { rawNextJobPtr in UnsafeMutableRawPointer(rawNextJobPtr).bindMemory(to: UnownedJob?.self, capacity: 1) @@ -92,11 +115,13 @@ fileprivate extension UnownedJob { } } +/// Represents job flags including priority. fileprivate struct JobFlags { - var bits: UInt32 = 0 + /// The raw bit representation of the flags. + var bits: UInt32 = 0 + /// Extracts the priority value from the flags. var priority: UInt32 { (bits & 0xFF00) >> 8 } -} -#endif \ No newline at end of file +} \ No newline at end of file From 0260884abc177a13090ef312222064233e0efcf7 Mon Sep 17 00:00:00 2001 From: admirsaheta Date: Sun, 1 Dec 2024 23:29:25 +0100 Subject: [PATCH 3/3] reduce:overhead-queue --- Sources/JavaScriptEventLoop/JobQueue.swift | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift index 6556c78d0..3367bac33 100644 --- a/Sources/JavaScriptEventLoop/JobQueue.swift +++ b/Sources/JavaScriptEventLoop/JobQueue.swift @@ -1,8 +1,8 @@ // This file contains the job queue implementation for JavaScriptEventLoop. -// It manages job insertion and execution based on priority. +// It manages job insertion and execution based on priority, ensuring thread safety and performance. import _CJavaScriptEventLoop -import Foundation +import os.lock /// Represents the state of the job queue. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @@ -15,16 +15,14 @@ struct QueueState: Sendable { @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension JavaScriptEventLoop { - /// A lock to synchronize queue access. - private var queueLock: NSLock { - NSLock() - } + /// A lock to synchronize queue access using `os_unfair_lock` for lightweight thread safety. + private static var queueLock = os_unfair_lock_s() /// Inserts a job into the queue and ensures jobs are processed. /// - Parameter job: The job to add to the queue. func insertJobQueue(job newJob: UnownedJob) { - queueLock.lock() - defer { queueLock.unlock() } + os_unfair_lock_lock(&JavaScriptEventLoop.queueLock) + defer { os_unfair_lock_unlock(&JavaScriptEventLoop.queueLock) } insertJob(newJob) @@ -79,8 +77,8 @@ extension JavaScriptEventLoop { /// Removes and returns the next job from the queue. /// - Returns: The next job in the queue, or `nil` if the queue is empty. func claimNextFromQueue() -> UnownedJob? { - queueLock.lock() - defer { queueLock.unlock() } + os_unfair_lock_lock(&JavaScriptEventLoop.queueLock) + defer { os_unfair_lock_unlock(&JavaScriptEventLoop.queueLock) } guard let job = queueState.headJob else { return nil } queueState.headJob = job.nextInQueue().pointee @@ -124,4 +122,4 @@ fileprivate struct JobFlags { var priority: UInt32 { (bits & 0xFF00) >> 8 } -} \ No newline at end of file +}