Skip to content

Commit 46f5b64

Browse files
authored
Merge pull request #1323 from ahoppen/dont-stack-preparation-for-editor
Cancel preparation tasks for editor functionality if the preparation task hasn't been started yet and the document is no longer active
2 parents 0e58ab1 + 41b810b commit 46f5b64

File tree

9 files changed

+257
-43
lines changed

9 files changed

+257
-43
lines changed

Sources/SKCore/TaskScheduler.swift

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,20 @@ public actor QueuedTask<TaskDescription: TaskDescriptionProtocol> {
147147
/// Gets reset every time `executionTask` finishes.
148148
nonisolated(unsafe) private var cancelledToBeRescheduled: AtomicBool = .init(initialValue: false)
149149

150+
/// Whether `resultTask` has been cancelled.
151+
private nonisolated(unsafe) var resultTaskCancelled: AtomicBool = .init(initialValue: false)
152+
150153
private nonisolated(unsafe) var _isExecuting: AtomicBool = .init(initialValue: false)
151154

152155
/// Whether the task is currently executing or still queued to be executed later.
153156
public nonisolated var isExecuting: Bool {
154157
return _isExecuting.value
155158
}
156159

160+
public nonisolated func cancel() {
161+
resultTask.cancel()
162+
}
163+
157164
/// Wait for the task to finish.
158165
///
159166
/// If the tasks that waits for this queued task to finished is cancelled, the QueuedTask will still continue
@@ -197,31 +204,35 @@ public actor QueuedTask<TaskDescription: TaskDescriptionProtocol> {
197204
self.executionTaskCreatedContinuation = executionTaskCreatedContinuation
198205

199206
self.resultTask = Task.detached(priority: priority) {
200-
await withTaskGroup(of: Void.self) { taskGroup in
201-
taskGroup.addTask {
202-
for await _ in updatePriorityStream {
203-
self.priority = Task.currentPriority
207+
await withTaskCancellationHandler {
208+
await withTaskGroup(of: Void.self) { taskGroup in
209+
taskGroup.addTask {
210+
for await _ in updatePriorityStream {
211+
self.priority = Task.currentPriority
212+
}
204213
}
205-
}
206-
taskGroup.addTask {
207-
for await task in executionTaskCreatedStream {
208-
switch await task.value {
209-
case .cancelledToBeRescheduled:
210-
// Break the switch and wait for a new `executionTask` to be placed into `executionTaskCreatedStream`.
211-
break
212-
case .terminated:
213-
// The task finished. We are done with this `QueuedTask`
214-
return
214+
taskGroup.addTask {
215+
for await task in executionTaskCreatedStream {
216+
switch await task.valuePropagatingCancellation {
217+
case .cancelledToBeRescheduled:
218+
// Break the switch and wait for a new `executionTask` to be placed into `executionTaskCreatedStream`.
219+
break
220+
case .terminated:
221+
// The task finished. We are done with this `QueuedTask`
222+
return
223+
}
215224
}
216225
}
226+
// The first (update priority) task never finishes, so this waits for the second (wait for execution) task
227+
// to terminate.
228+
// Afterwards we also cancel the update priority task.
229+
for await _ in taskGroup {
230+
taskGroup.cancelAll()
231+
return
232+
}
217233
}
218-
// The first (update priority) task never finishes, so this waits for the second (wait for execution) task
219-
// to terminate.
220-
// Afterwards we also cancel the update priority task.
221-
for await _ in taskGroup {
222-
taskGroup.cancelAll()
223-
return
224-
}
234+
} onCancel: {
235+
self.resultTaskCancelled.value = true
225236
}
226237
}
227238
}
@@ -233,7 +244,9 @@ public actor QueuedTask<TaskDescription: TaskDescriptionProtocol> {
233244
func execute() async -> ExecutionTaskFinishStatus {
234245
precondition(executionTask == nil, "Task started twice")
235246
let task = Task.detached(priority: self.priority) {
236-
await self.description.execute()
247+
if !Task.isCancelled && !self.resultTaskCancelled.value {
248+
await self.description.execute()
249+
}
237250
return await self.finalizeExecution()
238251
}
239252
executionTask = task

Sources/SKSupport/LineTable.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import LSPLogging
14+
1415
#if canImport(os)
1516
import os
1617
#endif

Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,9 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
489489
"--disable-index-store",
490490
"--target", target.targetID,
491491
]
492+
if Task.isCancelled {
493+
return
494+
}
492495
let process = try Process.launch(arguments: arguments, workingDirectory: nil)
493496
let result = try await process.waitUntilExitSendingSigIntOnTaskCancellation()
494497
switch result.exitStatus.exhaustivelySwitchable {

Sources/SemanticIndex/PreparationTaskDescription.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public struct PreparationTaskDescription: IndexTaskDescription {
7070
// See comment in `withLoggingScope`.
7171
// The last 2 digits should be sufficient to differentiate between multiple concurrently running preparation operations
7272
await withLoggingScope("preparation-\(id % 100)") {
73+
await testHooks.preparationTaskDidStart?(self)
7374
let targetsToPrepare = await targetsToPrepare.asyncFilter {
7475
await !preparationUpToDateStatus.isUpToDate($0)
7576
}.sorted(by: {

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,17 @@ public final actor SemanticIndexManager {
8686
/// After the file is indexed, it is removed from this dictionary.
8787
private var inProgressIndexTasks: [DocumentURI: InProgressIndexStore] = [:]
8888

89+
/// The currently running task that prepares a document for editor functionality.
90+
///
91+
/// This is used so we can cancel preparation tasks for documents that the user is no longer interacting with and
92+
/// avoid the following scenario: The user browses through documents from targets A, B, and C in quick succession. We
93+
/// don't want stack preparation of A, B, and C. Instead we want to only prepare target C - and also finish
94+
/// preparation of A if it has already started when the user opens C.
95+
///
96+
/// `id` is a unique ID that identifies the preparation task and is used to set `inProgressPrepareForEditorTask` to
97+
/// `nil` when the current in progress task finishes.
98+
private var inProgressPrepareForEditorTask: (id: UUID, document: DocumentURI, task: Task<Void, Never>)? = nil
99+
89100
/// The `TaskScheduler` that manages the scheduling of index tasks. This is shared among all `SemanticIndexManager`s
90101
/// in the process, to ensure that we don't schedule more index operations than processor cores from multiple
91102
/// workspaces.
@@ -287,20 +298,39 @@ public final actor SemanticIndexManager {
287298
/// Schedule preparation of the target that contains the given URI, building all modules that the file depends on.
288299
///
289300
/// This is intended to be called when the user is interacting with the document at the given URI.
290-
public func schedulePreparation(of uri: DocumentURI, priority: TaskPriority? = nil) {
291-
Task(priority: priority) {
301+
public func schedulePreparationForEditorFunctionality(
302+
of uri: DocumentURI,
303+
priority: TaskPriority? = nil
304+
) {
305+
if inProgressPrepareForEditorTask?.document == uri {
306+
// We are already preparing this document, so nothing to do. This is necessary to avoid the following scenario:
307+
// Determining the canonical configured target for a document takes 1s and we get a new document request for the
308+
// document ever 0.5s, which would cancel the previous in-progress preparation task, cancelling the canonical
309+
// configured target configuration, never actually getting to the actual preparation.
310+
return
311+
}
312+
let id = UUID()
313+
let task = Task(priority: priority) {
292314
await withLoggingScope("preparation") {
293315
guard let target = await buildSystemManager.canonicalConfiguredTarget(for: uri) else {
294316
return
295317
}
318+
if Task.isCancelled {
319+
return
320+
}
296321
await self.prepare(targets: [target], priority: priority)
322+
if inProgressPrepareForEditorTask?.id == id {
323+
inProgressPrepareForEditorTask = nil
324+
}
297325
}
298326
}
327+
inProgressPrepareForEditorTask?.task.cancel()
328+
inProgressPrepareForEditorTask = (id, uri, task)
299329
}
300330

301331
// MARK: - Helper functions
302332

303-
/// Prepare the given targets for indexing
333+
/// Prepare the given targets for indexing.
304334
private func prepare(targets: [ConfiguredTarget], priority: TaskPriority?) async {
305335
// Perform a quick initial check whether the target is up-to-date, in which case we don't need to schedule a
306336
// preparation operation at all.
@@ -323,6 +353,9 @@ public final actor SemanticIndexManager {
323353
testHooks: testHooks
324354
)
325355
)
356+
if Task.isCancelled {
357+
return
358+
}
326359
let preparationTask = await indexTaskScheduler.schedule(priority: priority, taskDescription) { task, newState in
327360
guard case .finished = newState else {
328361
return
@@ -337,7 +370,17 @@ public final actor SemanticIndexManager {
337370
for target in targetsToPrepare {
338371
inProgressPreparationTasks[target] = OpaqueQueuedIndexTask(preparationTask)
339372
}
340-
return await preparationTask.waitToFinishPropagatingCancellation()
373+
await withTaskCancellationHandler {
374+
return await preparationTask.waitToFinish()
375+
} onCancel: {
376+
// Only cancel the preparation task if it hasn't started executing yet. This ensures that we always make progress
377+
// during preparation and can't get into the following scenario: The user has two target A and B that both take
378+
// 10s to prepare. The user is now switching between the files every 5 seconds, which would always cause
379+
// preparation for one target to get cancelled, never resulting in an up-to-date preparation status.
380+
if !preparationTask.isExecuting {
381+
preparationTask.cancel()
382+
}
383+
}
341384
}
342385

343386
/// Update the index store for the given files, assuming that their targets have already been prepared.

Sources/SemanticIndex/TestHooks.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@
1212

1313
/// Callbacks that allow inspection of internal state modifications during testing.
1414
public struct IndexTestHooks: Sendable {
15+
public var preparationTaskDidStart: (@Sendable (PreparationTaskDescription) async -> Void)?
16+
1517
public var preparationTaskDidFinish: (@Sendable (PreparationTaskDescription) async -> Void)?
1618

1719
/// A callback that is called when an index task finishes.
1820
public var updateIndexStoreTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)?
1921

2022
public init(
23+
preparationTaskDidStart: (@Sendable (PreparationTaskDescription) async -> Void)? = nil,
2124
preparationTaskDidFinish: (@Sendable (PreparationTaskDescription) async -> Void)? = nil,
2225
updateIndexStoreTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)? = nil
2326
) {
27+
self.preparationTaskDidStart = preparationTaskDidStart
2428
self.preparationTaskDidFinish = preparationTaskDidFinish
2529
self.updateIndexStoreTaskDidFinish = updateIndexStoreTaskDidFinish
2630
}

Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,9 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
301301
processArguments: [String],
302302
workingDirectory: AbsolutePath?
303303
) async throws {
304+
if Task.isCancelled {
305+
return
306+
}
304307
let process = try Process.launch(
305308
arguments: processArguments,
306309
workingDirectory: workingDirectory

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,7 +1000,9 @@ extension SourceKitLSPServer: MessageHandler {
10001000
// which prepares the files. For files that are open but aren't being worked on (eg. a different tab), we don't
10011001
// get requests, ensuring that we don't unnecessarily prepare them.
10021002
let workspace = await self.workspaceForDocument(uri: textDocumentRequest.textDocument.uri)
1003-
await workspace?.semanticIndexManager?.schedulePreparation(of: textDocumentRequest.textDocument.uri)
1003+
await workspace?.semanticIndexManager?.schedulePreparationForEditorFunctionality(
1004+
of: textDocumentRequest.textDocument.uri
1005+
)
10041006
}
10051007

10061008
switch request {
@@ -1543,7 +1545,7 @@ extension SourceKitLSPServer {
15431545
)
15441546
return
15451547
}
1546-
await workspace.semanticIndexManager?.schedulePreparation(of: uri)
1548+
await workspace.semanticIndexManager?.schedulePreparationForEditorFunctionality(of: uri)
15471549
await openDocument(notification, workspace: workspace)
15481550
}
15491551

@@ -1599,7 +1601,7 @@ extension SourceKitLSPServer {
15991601
)
16001602
return
16011603
}
1602-
await workspace.semanticIndexManager?.schedulePreparation(of: uri)
1604+
await workspace.semanticIndexManager?.schedulePreparationForEditorFunctionality(of: uri)
16031605

16041606
// If the document is ready, we can handle the change right now.
16051607
documentManager.edit(notification)

0 commit comments

Comments
 (0)