Skip to content

Commit e2e7085

Browse files
committed
JavaScriptEventLoop: Append a job after the queue tail without a scan
1 parent dbbfdc3 commit e2e7085

2 files changed

Lines changed: 100 additions & 10 deletions

File tree

‎Sources/JavaScriptEventLoop/JobQueue.swift‎

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,41 @@ import _CJavaScriptEventLoop
1010
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
1111
struct QueueState: Sendable {
1212
fileprivate var headJob: UnownedJob? = nil
13+
/// The last job in the queue, which has the lowest priority. `nil` when the queue is empty.
14+
fileprivate var tailJob: UnownedJob? = nil
1315
fileprivate var isSpinning: Bool = false
1416
}
1517

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

21+
/// Inserts `newJob` after every queued job of the same or a higher priority.
22+
///
23+
/// The queue is ordered by descending priority, so a job whose priority is not higher than the
24+
/// tail's goes right after the tail, without a scan from the head.
1925
func insertJobQueue(job newJob: UnownedJob) {
20-
withUnsafeMutablePointer(to: &queueState.headJob) { headJobPtr in
21-
var position: UnsafeMutablePointer<UnownedJob?> = headJobPtr
22-
while let cur = position.pointee {
23-
if cur.rawPriority < newJob.rawPriority {
24-
newJob.nextInQueue().pointee = cur
25-
position.pointee = newJob
26-
return
26+
if let tailJob = queueState.tailJob, newJob.rawPriority <= tailJob.rawPriority {
27+
newJob.nextInQueue().pointee = nil
28+
tailJob.nextInQueue().pointee = newJob
29+
queueState.tailJob = newJob
30+
} else {
31+
let isLast = withUnsafeMutablePointer(to: &queueState.headJob) { headJobPtr -> Bool in
32+
var position: UnsafeMutablePointer<UnownedJob?> = headJobPtr
33+
while let cur = position.pointee {
34+
if cur.rawPriority < newJob.rawPriority {
35+
newJob.nextInQueue().pointee = cur
36+
position.pointee = newJob
37+
return false
38+
}
39+
position = cur.nextInQueue()
2740
}
28-
position = cur.nextInQueue()
41+
newJob.nextInQueue().pointee = nil
42+
position.pointee = newJob
43+
return true
44+
}
45+
if isLast {
46+
queueState.tailJob = newJob
2947
}
30-
newJob.nextInQueue().pointee = nil
31-
position.pointee = newJob
3248
}
3349

3450
// TODO: use CAS when supporting multi-threaded environment
@@ -82,6 +98,9 @@ extension JavaScriptEventLoop {
8298
func claimNextFromQueue() -> UnownedJob? {
8399
if let job = self.queueState.headJob {
84100
self.queueState.headJob = job.nextInQueue().pointee
101+
if self.queueState.headJob == nil {
102+
self.queueState.tailJob = nil
103+
}
85104
return job
86105
}
87106
return nil

‎Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,77 @@ final class JavaScriptEventLoopTests: XCTestCase {
113113
XCTAssertEqual(context.completed, ["t4", "t3", "t5"])
114114
}
115115

116+
func testTaskPriorityOrderWithMixedPriorities() async throws {
117+
final class Context: @unchecked Sendable {
118+
var completed: [String] = []
119+
var expectedCount = 0
120+
var continuation: CheckedContinuation<Void, Never>?
121+
122+
func record(_ name: String) {
123+
completed.append(name)
124+
if completed.count == expectedCount {
125+
continuation?.resume()
126+
continuation = nil
127+
}
128+
}
129+
}
130+
/// Starts tasks without awaiting them, so no await raises the priority of a queued task.
131+
func run(expectedCount: Int, _ start: @escaping (Context) -> Void) async -> [String] {
132+
let context = Context()
133+
context.expectedCount = expectedCount
134+
await withCheckedContinuation { continuation in
135+
context.continuation = continuation
136+
start(context)
137+
}
138+
return context.completed
139+
}
140+
141+
let burst: [(String, TaskPriority)] = [
142+
("low1", .low), ("medium1", .medium), ("low2", .low), ("high1", .high),
143+
("medium2", .medium), ("low3", .low), ("high2", .high), ("medium3", .medium),
144+
]
145+
let burstOrder = await run(expectedCount: burst.count) { context in
146+
for (name, priority) in burst {
147+
Task(priority: priority) { context.record(name) }
148+
}
149+
}
150+
XCTAssertEqual(burstOrder, ["high1", "high2", "medium1", "medium2", "medium3", "low1", "low2", "low3"])
151+
152+
let nestedOrder = await run(expectedCount: 7) { context in
153+
Task(priority: .medium) {
154+
context.record("outer")
155+
let nested: [(String, TaskPriority)] = [
156+
("nestedLow1", .low), ("nestedHigh", .high), ("nestedMedium", .medium), ("nestedLow2", .low),
157+
]
158+
for (name, priority) in nested {
159+
Task(priority: priority) { context.record(name) }
160+
}
161+
}
162+
Task(priority: .medium) { context.record("medium") }
163+
Task(priority: .low) { context.record("low") }
164+
}
165+
XCTAssertEqual(
166+
nestedOrder,
167+
["outer", "nestedHigh", "medium", "nestedMedium", "low", "nestedLow1", "nestedLow2"]
168+
)
169+
170+
let priorities: [TaskPriority] = [.low, .high, .background, .medium, .utility, .userInitiated]
171+
let many = (0..<600).map { index in ("\(index)", priorities[index % priorities.count]) }
172+
let manyOrder = await run(expectedCount: many.count) { context in
173+
for (name, priority) in many {
174+
Task(priority: priority) { context.record(name) }
175+
}
176+
}
177+
let expectedManyOrder = many.enumerated()
178+
.sorted { lhs, rhs in
179+
lhs.element.1.rawValue != rhs.element.1.rawValue
180+
? lhs.element.1.rawValue > rhs.element.1.rawValue
181+
: lhs.offset < rhs.offset
182+
}
183+
.map(\.element.0)
184+
XCTAssertEqual(manyOrder, expectedManyOrder)
185+
}
186+
116187
// MARK: - Promise Tests
117188

118189
func testPromiseResolution() async throws {

0 commit comments

Comments
 (0)