Skip to content

Commit 5575e90

Browse files
authored
Merge pull request #2085 from ahoppen/quadratic-queue
Fix some more quadratic performance issues in `AsyncQueue`
2 parents e573519 + dd2d2af commit 5575e90

File tree

4 files changed

+40
-34
lines changed

4 files changed

+40
-34
lines changed

Sources/BuildSystemIntegration/BuildSystemMessageDependencyTracker.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,6 @@ package enum BuildSystemMessageDependencyTracker: QueueBasedMessageHandlerDepend
7474
}
7575
}
7676

77-
package func dependencies(in pendingTasks: [PendingTask<Self>]) -> [PendingTask<Self>] {
78-
return pendingTasks.filter { $0.metadata.isDependency(of: self) }
79-
}
80-
8177
package init(_ request: some RequestType) {
8278
switch request {
8379
case is BuildShutdownRequest:

Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
package import LanguageServerProtocol
1414
import LanguageServerProtocolExtensions
1515
import SKLogging
16-
package import SwiftExtensions
16+
import SwiftExtensions
1717

1818
/// A lightweight way of describing tasks that are created from handling LSP
1919
/// requests or notifications for the purpose of dependency tracking.
@@ -83,10 +83,6 @@ package enum MessageHandlingDependencyTracker: QueueBasedMessageHandlerDependenc
8383
}
8484
}
8585

86-
package func dependencies(in pendingTasks: [PendingTask<Self>]) -> [PendingTask<Self>] {
87-
return pendingTasks.filter { $0.metadata.isDependency(of: self) }
88-
}
89-
9086
package init(_ notification: some NotificationType) {
9187
switch notification {
9288
case is CancelRequestNotification:

Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,6 @@ fileprivate enum TaskMetadata: DependencyTracker, Equatable {
6161
return !lhsUris.intersection(rhsUris).isEmpty
6262
}
6363
}
64-
65-
package func dependencies(in pendingTasks: [PendingTask<Self>]) -> [PendingTask<Self>] {
66-
return pendingTasks.filter { $0.metadata.isDependency(of: self) }
67-
}
6864
}
6965

7066
/// Data from a syntactic scan of a source file for tests.

Sources/SwiftExtensions/AsyncQueue.swift

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,23 @@ extension Task: AnyTask {
2727
}
2828

2929
/// A type that is able to track dependencies between tasks.
30-
package protocol DependencyTracker: Sendable {
31-
/// Which tasks need to finish before a task described by `self` may start executing.
32-
/// `pendingTasks` is sorted in the order in which the tasks were enqueued to `AsyncQueue`.
33-
func dependencies(in pendingTasks: [PendingTask<Self>]) -> [PendingTask<Self>]
30+
package protocol DependencyTracker: Sendable, Hashable {
31+
/// Whether the task described by `self` needs to finish executing before `other` can start executing.
32+
func isDependency(of other: Self) -> Bool
3433
}
3534

3635
/// A dependency tracker where each task depends on every other, i.e. a serial
3736
/// queue.
3837
package struct Serial: DependencyTracker {
39-
package func dependencies(in pendingTasks: [PendingTask<Self>]) -> [PendingTask<Self>] {
40-
if let lastTask = pendingTasks.last {
41-
return [lastTask]
42-
}
43-
return []
38+
package func isDependency(of other: Serial) -> Bool {
39+
return true
4440
}
4541
}
4642

47-
package struct PendingTask<TaskMetadata: Sendable>: Sendable {
43+
package struct PendingTask<TaskMetadata: Sendable & Hashable>: Sendable {
4844
/// The task that is pending.
4945
fileprivate let task: any AnyTask
5046

51-
package let metadata: TaskMetadata
52-
5347
/// A unique value used to identify the task. This allows tasks to get
5448
/// removed from `pendingTasks` again after they finished executing.
5549
fileprivate let id: UUID
@@ -58,23 +52,25 @@ package struct PendingTask<TaskMetadata: Sendable>: Sendable {
5852
/// A list of pending tasks that can be sent across actor boundaries and is guarded by a lock.
5953
///
6054
/// - Note: Unchecked sendable because the tasks are being protected by a lock.
61-
private class PendingTasks<TaskMetadata: Sendable>: @unchecked Sendable {
55+
private final class PendingTasks<TaskMetadata: Sendable & Hashable>: Sendable {
6256
/// Lock guarding `pendingTasks`.
6357
private let lock = NSLock()
6458

6559
/// Pending tasks that have not finished execution yet.
6660
///
6761
/// - Important: This must only be accessed while `lock` has been acquired.
68-
private var tasks: [PendingTask<TaskMetadata>] = []
62+
private nonisolated(unsafe) var tasksByMetadata: [TaskMetadata: [PendingTask<TaskMetadata>]] = [:]
6963

7064
init() {
7165
self.lock.name = "AsyncQueue"
7266
}
7367

7468
/// Capture a lock and execute the closure, which may modify the pending tasks.
75-
func withLock<T>(_ body: (_ pendingTasks: inout [PendingTask<TaskMetadata>]) throws -> T) rethrows -> T {
69+
func withLock<T>(
70+
_ body: (_ tasksByMetadata: inout [TaskMetadata: [PendingTask<TaskMetadata>]]) throws -> T
71+
) rethrows -> T {
7672
try lock.withLock {
77-
try body(&tasks)
73+
try body(&tasksByMetadata)
7874
}
7975
}
8076
}
@@ -122,10 +118,29 @@ package final class AsyncQueue<TaskMetadata: DependencyTracker>: Sendable {
122118
) -> Task<Success, any Error> {
123119
let id = UUID()
124120

125-
return pendingTasks.withLock { tasks in
121+
return pendingTasks.withLock { tasksByMetadata in
126122
// Build the list of tasks that need to finished execution before this one
127123
// can be executed
128-
let dependencies = metadata.dependencies(in: tasks)
124+
var dependencies: [PendingTask<TaskMetadata>] = []
125+
for (pendingMetadata, pendingTasks) in tasksByMetadata {
126+
guard pendingMetadata.isDependency(of: metadata) else {
127+
// No dependency
128+
continue
129+
}
130+
if metadata.isDependency(of: metadata), let lastPendingTask = pendingTasks.last {
131+
// This kind of task depends on all other tasks of the same kind finishing. It is sufficient to just wait on
132+
// the last task with this metadata, it will have all the other tasks with the same metadata as transitive
133+
// dependencies.
134+
dependencies.append(lastPendingTask)
135+
} else {
136+
// We depend on tasks with this metadata, but they don't have any dependencies between them, eg.
137+
// `documentUpdate` depends on all `documentRequest` but `documentRequest` don't have dependencies between
138+
// them. We need to depend on all of them unless we knew that we depended on some other task that already
139+
// depends on all of these. But determining that would also require knowledge about the entire dependency
140+
// graph, which is likely as expensive as depending on all of these tasks.
141+
dependencies += pendingTasks
142+
}
143+
}
129144

130145
// Schedule the task.
131146
let task = Task(priority: priority) { [pendingTasks] in
@@ -139,14 +154,17 @@ package final class AsyncQueue<TaskMetadata: DependencyTracker>: Sendable {
139154

140155
let result = try await operation()
141156

142-
pendingTasks.withLock { tasks in
143-
tasks.removeAll(where: { $0.id == id })
157+
pendingTasks.withLock { tasksByMetadata in
158+
tasksByMetadata[metadata, default: []].removeAll(where: { $0.id == id })
159+
if tasksByMetadata[metadata]?.isEmpty ?? false {
160+
tasksByMetadata[metadata] = nil
161+
}
144162
}
145163

146164
return result
147165
}
148166

149-
tasks.append(PendingTask(task: task, metadata: metadata, id: id))
167+
tasksByMetadata[metadata, default: []].append(PendingTask(task: task, id: id))
150168

151169
return task
152170
}

0 commit comments

Comments
 (0)