Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 29 additions & 10 deletions Sources/JavaScriptEventLoop/JobQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,41 @@ import _CJavaScriptEventLoop
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
struct QueueState: Sendable {
fileprivate var headJob: UnownedJob? = nil
/// The last job in the queue, which has the lowest priority. `nil` when the queue is empty.
fileprivate var tailJob: UnownedJob? = nil
fileprivate var isSpinning: Bool = false
}

@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
extension JavaScriptEventLoop {

/// Inserts `newJob` after every queued job of the same or a higher priority.
///
/// The queue is ordered by descending priority, so a job whose priority is not higher than the
/// tail's goes right after the tail, without a scan from the head.
func insertJobQueue(job newJob: UnownedJob) {
withUnsafeMutablePointer(to: &queueState.headJob) { headJobPtr in
var position: UnsafeMutablePointer<UnownedJob?> = headJobPtr
while let cur = position.pointee {
if cur.rawPriority < newJob.rawPriority {
newJob.nextInQueue().pointee = cur
position.pointee = newJob
return
if let tailJob = queueState.tailJob, newJob.rawPriority <= tailJob.rawPriority {
newJob.nextInQueue().pointee = nil
tailJob.nextInQueue().pointee = newJob
queueState.tailJob = newJob
} else {
let isLast = withUnsafeMutablePointer(to: &queueState.headJob) { headJobPtr -> Bool in
var position: UnsafeMutablePointer<UnownedJob?> = headJobPtr
while let cur = position.pointee {
if cur.rawPriority < newJob.rawPriority {
newJob.nextInQueue().pointee = cur
position.pointee = newJob
return false
}
position = cur.nextInQueue()
}
position = cur.nextInQueue()
newJob.nextInQueue().pointee = nil
position.pointee = newJob
return true
}
if isLast {
queueState.tailJob = newJob
}
newJob.nextInQueue().pointee = nil
position.pointee = newJob
}

// TODO: use CAS when supporting multi-threaded environment
Expand Down Expand Up @@ -82,6 +98,9 @@ extension JavaScriptEventLoop {
func claimNextFromQueue() -> UnownedJob? {
if let job = self.queueState.headJob {
self.queueState.headJob = job.nextInQueue().pointee
if self.queueState.headJob == nil {
self.queueState.tailJob = nil
}
return job
}
return nil
Expand Down
71 changes: 71 additions & 0 deletions Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,77 @@ final class JavaScriptEventLoopTests: XCTestCase {
XCTAssertEqual(context.completed, ["t4", "t3", "t5"])
}

func testTaskPriorityOrderWithMixedPriorities() async throws {
final class Context: @unchecked Sendable {
var completed: [String] = []
var expectedCount = 0
var continuation: CheckedContinuation<Void, Never>?

func record(_ name: String) {
completed.append(name)
if completed.count == expectedCount {
continuation?.resume()
continuation = nil
}
}
}
/// Starts tasks without awaiting them, so no await raises the priority of a queued task.
func run(expectedCount: Int, _ start: @escaping (Context) -> Void) async -> [String] {
let context = Context()
context.expectedCount = expectedCount
await withCheckedContinuation { continuation in
context.continuation = continuation
start(context)
}
return context.completed
}

let burst: [(String, TaskPriority)] = [
("low1", .low), ("medium1", .medium), ("low2", .low), ("high1", .high),
("medium2", .medium), ("low3", .low), ("high2", .high), ("medium3", .medium),
]
let burstOrder = await run(expectedCount: burst.count) { context in
for (name, priority) in burst {
Task(priority: priority) { context.record(name) }
}
}
XCTAssertEqual(burstOrder, ["high1", "high2", "medium1", "medium2", "medium3", "low1", "low2", "low3"])

let nestedOrder = await run(expectedCount: 7) { context in
Task(priority: .medium) {
context.record("outer")
let nested: [(String, TaskPriority)] = [
("nestedLow1", .low), ("nestedHigh", .high), ("nestedMedium", .medium), ("nestedLow2", .low),
]
for (name, priority) in nested {
Task(priority: priority) { context.record(name) }
}
}
Task(priority: .medium) { context.record("medium") }
Task(priority: .low) { context.record("low") }
}
XCTAssertEqual(
nestedOrder,
["outer", "nestedHigh", "medium", "nestedMedium", "low", "nestedLow1", "nestedLow2"]
)

let priorities: [TaskPriority] = [.low, .high, .background, .medium, .utility, .userInitiated]
let many = (0..<600).map { index in ("\(index)", priorities[index % priorities.count]) }
let manyOrder = await run(expectedCount: many.count) { context in
for (name, priority) in many {
Task(priority: priority) { context.record(name) }
}
}
let expectedManyOrder = many.enumerated()
.sorted { lhs, rhs in
lhs.element.1.rawValue != rhs.element.1.rawValue
? lhs.element.1.rawValue > rhs.element.1.rawValue
: lhs.offset < rhs.offset
}
.map(\.element.0)
XCTAssertEqual(manyOrder, expectedManyOrder)
}

// MARK: - Promise Tests

func testPromiseResolution() async throws {
Expand Down
Loading