Skip to content

Commit 5c95e3c

Browse files
authored
Merge pull request #823 from PassiveLogic/kr/job-queue-insert
JavaScriptEventLoop: Avoid O(N^2) job queue insert for bursts of jobs
2 parents dbbfdc3 + c332a3b commit 5c95e3c

3 files changed

Lines changed: 163 additions & 9 deletions

File tree

‎Sources/JavaScriptEventLoop/JobQueue.swift‎

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// This file contains the job queue implementation which re-order jobs based on their priority.
2-
// The current implementation is much simple to be easily debugged, but should be re-implemented
3-
// using priority queue ideally.
2+
// Jobs form runs, one per priority in descending order, FIFO within a run. The first job of each
3+
// run points to its last job, so an insert steps over whole runs.
44

55
import _Concurrency
66
import _CJavaScriptEventLoop
@@ -19,15 +19,18 @@ extension JavaScriptEventLoop {
1919
func insertJobQueue(job newJob: UnownedJob) {
2020
withUnsafeMutablePointer(to: &queueState.headJob) { headJobPtr in
2121
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
22+
while let runHead = position.pointee, runHead.rawPriority >= newJob.rawPriority {
23+
let runTail = runHead.lastInRun().pointee!
24+
if runHead.rawPriority == newJob.rawPriority {
25+
newJob.nextInQueue().pointee = runTail.nextInQueue().pointee
26+
runTail.nextInQueue().pointee = newJob
27+
runHead.lastInRun().pointee = newJob
2628
return
2729
}
28-
position = cur.nextInQueue()
30+
position = runTail.nextInQueue()
2931
}
30-
newJob.nextInQueue().pointee = nil
32+
newJob.nextInQueue().pointee = position.pointee
33+
newJob.lastInRun().pointee = newJob
3134
position.pointee = newJob
3235
}
3336

@@ -81,7 +84,14 @@ extension JavaScriptEventLoop {
8184

8285
func claimNextFromQueue() -> UnownedJob? {
8386
if let job = self.queueState.headJob {
84-
self.queueState.headJob = job.nextInQueue().pointee
87+
let nextJob = job.nextInQueue().pointee
88+
if let nextJob {
89+
let runTail = job.lastInRun().pointee!
90+
if !runTail.isSameJob(as: job) {
91+
nextJob.lastInRun().pointee = runTail
92+
}
93+
}
94+
self.queueState.headJob = nextJob
8595
return job
8696
}
8797
return nil
@@ -107,6 +117,16 @@ extension UnownedJob {
107117
}
108118
}
109119

120+
/// The last job of the run. Valid only on the first job of a run.
121+
fileprivate func lastInRun() -> UnsafeMutablePointer<UnownedJob?> {
122+
return withUnsafeMutablePointer(to: &asImpl().pointee.SchedulerPrivate.1) { rawLastJobPtr in
123+
UnsafeMutableRawPointer(rawLastJobPtr).bindMemory(to: UnownedJob?.self, capacity: 1)
124+
}
125+
}
126+
127+
fileprivate func isSameJob(as other: UnownedJob) -> Bool {
128+
asImpl() == other.asImpl()
129+
}
110130
}
111131

112132
private struct JobFlags {

‎Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift‎

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

116+
final class OrderContext: @unchecked Sendable {
117+
var completed: [String] = []
118+
var expectedCount = 0
119+
var continuation: CheckedContinuation<Void, Never>?
120+
121+
func record(_ name: String) {
122+
completed.append(name)
123+
if completed.count == expectedCount {
124+
continuation?.resume()
125+
continuation = nil
126+
}
127+
}
128+
}
129+
130+
/// Starts tasks without awaiting them, so no await raises the priority of a queued task.
131+
func runUnawaited(expectedCount: Int, _ start: (OrderContext) -> Void) async -> [String] {
132+
let context = OrderContext()
133+
context.expectedCount = expectedCount
134+
await withCheckedContinuation { continuation in
135+
context.continuation = continuation
136+
start(context)
137+
}
138+
return context.completed
139+
}
140+
141+
func testTaskPriorityOrderWithMixedPriorities() async throws {
142+
let burst: [(String, TaskPriority)] = [
143+
("low1", .low), ("medium1", .medium), ("low2", .low), ("high1", .high),
144+
("medium2", .medium), ("low3", .low), ("high2", .high), ("medium3", .medium),
145+
]
146+
let burstOrder = await runUnawaited(expectedCount: burst.count) { context in
147+
for (name, priority) in burst {
148+
Task(priority: priority) { context.record(name) }
149+
}
150+
}
151+
XCTAssertEqual(burstOrder, ["high1", "high2", "medium1", "medium2", "medium3", "low1", "low2", "low3"])
152+
153+
let nestedOrder = await runUnawaited(expectedCount: 7) { context in
154+
Task(priority: .medium) {
155+
context.record("outer")
156+
let nested: [(String, TaskPriority)] = [
157+
("nestedLow1", .low), ("nestedHigh", .high), ("nestedMedium", .medium), ("nestedLow2", .low),
158+
]
159+
for (name, priority) in nested {
160+
Task(priority: priority) { context.record(name) }
161+
}
162+
}
163+
Task(priority: .medium) { context.record("medium") }
164+
Task(priority: .low) { context.record("low") }
165+
}
166+
XCTAssertEqual(
167+
nestedOrder,
168+
["outer", "nestedHigh", "medium", "nestedMedium", "low", "nestedLow1", "nestedLow2"]
169+
)
170+
171+
let priorities: [TaskPriority] = [
172+
.low, .high, .background, .medium, TaskPriority(rawValue: 22), TaskPriority(rawValue: 5),
173+
]
174+
let many = (0..<600).map { index in ("\(index)", priorities[index % priorities.count]) }
175+
let manyOrder = await runUnawaited(expectedCount: many.count) { context in
176+
for (name, priority) in many {
177+
Task(priority: priority) { context.record(name) }
178+
}
179+
}
180+
let expectedManyOrder = many.enumerated()
181+
.sorted { lhs, rhs in
182+
lhs.element.1.rawValue != rhs.element.1.rawValue
183+
? lhs.element.1.rawValue > rhs.element.1.rawValue
184+
: lhs.offset < rhs.offset
185+
}
186+
.map(\.element.0)
187+
XCTAssertEqual(manyOrder, expectedManyOrder)
188+
}
189+
190+
func testTaskPriorityOrderAfterQueueEmpties() async throws {
191+
let rounds: [[(String, TaskPriority)]] = [
192+
[("low1", .low), ("low2", .low), ("high1", .high), ("low3", .low)],
193+
[("high1", .high), ("medium1", .medium), ("high2", .high), ("low1", .low)],
194+
[("medium1", .medium), ("medium2", .medium), ("medium3", .medium)],
195+
]
196+
let expectedOrders = [
197+
["high1", "low1", "low2", "low3"],
198+
["high1", "high2", "medium1", "low1"],
199+
["medium1", "medium2", "medium3"],
200+
]
201+
for (round, expectedOrder) in zip(rounds, expectedOrders) {
202+
let order = await runUnawaited(expectedCount: round.count) { context in
203+
for (name, priority) in round {
204+
Task(priority: priority) { context.record(name) }
205+
}
206+
}
207+
XCTAssertEqual(order, expectedOrder)
208+
}
209+
}
210+
211+
func testTaskPriorityOrderAfterSingleJobRunIsClaimed() async throws {
212+
let order = await runUnawaited(expectedCount: 4) { context in
213+
Task(priority: .low) { context.record("low1") }
214+
Task(priority: .low) { context.record("low2") }
215+
Task(priority: .high) {
216+
context.record("high")
217+
Task(priority: .low) { context.record("low3") }
218+
}
219+
}
220+
XCTAssertEqual(order, ["high", "low1", "low2", "low3"])
221+
}
222+
116223
// MARK: - Promise Tests
117224

118225
func testPromiseResolution() async throws {

‎Tests/JavaScriptEventLoopTests/JavaScriptEventLoopYieldIntervalTests.swift‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,31 @@ final class JavaScriptEventLoopYieldIntervalTests: XCTestCase {
7676
for task in tasks { await task.value }
7777
XCTAssertEqual(order.value, Array(0..<20))
7878
}
79+
80+
func testPriorityOrderAcrossYields() async {
81+
JavaScriptEventLoop.shared.yieldInterval = .milliseconds(1)
82+
let order = Box([String]())
83+
let expectedOrder = ["h0", "h1", "h2", "h3", "h4", "m0", "m1", "m2", "m3", "l0", "l1", "l2", "l3", "l4"]
84+
// Tasks are not awaited, so no await raises the priority of a queued task.
85+
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
86+
@Sendable func record(_ name: String) {
87+
Self.busyWait(milliseconds: 1)
88+
order.value.append(name)
89+
if order.value.count == expectedOrder.count { continuation.resume() }
90+
}
91+
let priorities: [(String, TaskPriority)] = [("l", .low), ("m", .medium), ("h", .high)]
92+
for index in 0..<12 {
93+
let (prefix, priority) = priorities[index % 3]
94+
let name = "\(prefix)\(index / 3)"
95+
Task(priority: priority) {
96+
record(name)
97+
if name == "h0" {
98+
Task(priority: .low) { record("l4") }
99+
Task(priority: .high) { record("h4") }
100+
}
101+
}
102+
}
103+
}
104+
XCTAssertEqual(order.value, expectedOrder)
105+
}
79106
}

0 commit comments

Comments
 (0)