Skip to content

Commit a6389e5

Browse files
committed
Show a work done progress that shows the index progress
This makes it a lot easier to work on background indexing because you can easily see how background indexing is making progress. Resolves #1257 rdar://127474057
1 parent c9b51f9 commit a6389e5

File tree

9 files changed

+324
-36
lines changed

9 files changed

+324
-36
lines changed

Sources/SKCore/TaskScheduler.swift

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,19 @@ public protocol TaskDescriptionProtocol: Identifiable, Sendable, CustomLogString
7676
var estimatedCPUCoreCount: Int { get }
7777
}
7878

79+
/// Parameter that's passed to `executionStateChangedCallback` to indicate the new state of a scheduled task.
80+
public enum TaskExecutionState {
81+
/// The task started executing.
82+
case executing
83+
84+
/// The task was cancelled and will be re-scheduled for execution later. Will be followed by another call with
85+
/// `executing`.
86+
case cancelledToBeRescheduled
87+
88+
/// The task has finished executing. Now more state updates will come after this one.
89+
case finished
90+
}
91+
7992
fileprivate actor QueuedTask<TaskDescription: TaskDescriptionProtocol> {
8093
/// Result of `executionTask` / the tasks in `executionTaskCreatedContinuation`.
8194
/// See doc comment on `executionTask`.
@@ -136,9 +149,18 @@ fileprivate actor QueuedTask<TaskDescription: TaskDescriptionProtocol> {
136149
/// Gets reset every time `executionTask` finishes.
137150
nonisolated(unsafe) private var cancelledToBeRescheduled: AtomicBool = .init(initialValue: false)
138151

139-
init(priority: TaskPriority? = nil, description: TaskDescription) async {
152+
/// A callback that will be called when the task starts executing, is cancelled to be rescheduled, or when it finishes
153+
/// execution.
154+
private let executionStateChangedCallback: (@Sendable (TaskExecutionState) async -> Void)?
155+
156+
init(
157+
priority: TaskPriority? = nil,
158+
description: TaskDescription,
159+
executionStateChangedCallback: (@Sendable (TaskExecutionState) async -> Void)?
160+
) async {
140161
self._priority = .init(initialValue: priority?.rawValue ?? Task.currentPriority.rawValue)
141162
self.description = description
163+
self.executionStateChangedCallback = executionStateChangedCallback
142164

143165
var updatePriorityContinuation: AsyncStream<Void>.Continuation!
144166
let updatePriorityStream = AsyncStream {
@@ -194,16 +216,19 @@ fileprivate actor QueuedTask<TaskDescription: TaskDescriptionProtocol> {
194216
}
195217
executionTask = task
196218
executionTaskCreatedContinuation.yield(task)
219+
await executionStateChangedCallback?(.executing)
197220
return await task.value
198221
}
199222

200223
/// Implementation detail of `execute` that is called after `self.description.execute()` finishes.
201-
private func finalizeExecution() -> ExecutionTaskFinishStatus {
224+
private func finalizeExecution() async -> ExecutionTaskFinishStatus {
202225
self.executionTask = nil
203226
if Task.isCancelled && self.cancelledToBeRescheduled.value {
227+
await executionStateChangedCallback?(.cancelledToBeRescheduled)
204228
self.cancelledToBeRescheduled.value = false
205229
return ExecutionTaskFinishStatus.cancelledToBeRescheduled
206230
} else {
231+
await executionStateChangedCallback?(.finished)
207232
return ExecutionTaskFinishStatus.terminated
208233
}
209234
}
@@ -308,12 +333,17 @@ public actor TaskScheduler<TaskDescription: TaskDescriptionProtocol> {
308333
@discardableResult
309334
public func schedule(
310335
priority: TaskPriority? = nil,
311-
_ taskDescription: TaskDescription
336+
_ taskDescription: TaskDescription,
337+
@_inheritActorContext executionStateChangedCallback: (@Sendable (TaskExecutionState) async -> Void)? = nil
312338
) async -> Task<Void, Never> {
313339
withLoggingSubsystemAndScope(subsystem: taskSchedulerSubsystem, scope: nil) {
314340
logger.debug("Scheduling \(taskDescription.forLogging)")
315341
}
316-
let queuedTask = await QueuedTask(priority: priority, description: taskDescription)
342+
let queuedTask = await QueuedTask(
343+
priority: priority,
344+
description: taskDescription,
345+
executionStateChangedCallback: executionStateChangedCallback
346+
)
317347
pendingTasks.append(queuedTask)
318348
Task.detached(priority: priority ?? Task.currentPriority) {
319349
// Poke the `TaskScheduler` to execute a new task. If the `TaskScheduler` is already working at its capacity

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,22 @@ import SKCore
1919
private enum FileIndexStatus {
2020
/// The index is up-to-date.
2121
case upToDate
22-
/// The file is being indexed by the given task.
23-
case inProgress(Task<Void, Never>)
22+
/// The file is not up to date and we have scheduled a task to index it but that index operation hasn't been started
23+
/// yet.
24+
case scheduled(Task<Void, Never>)
25+
/// We are currently actively indexing this file, ie. we are running a subprocess that indexes the file.
26+
case executing(Task<Void, Never>)
27+
28+
var description: String {
29+
switch self {
30+
case .upToDate:
31+
return "upToDate"
32+
case .scheduled:
33+
return "scheduled"
34+
case .executing:
35+
return "executing"
36+
}
37+
}
2438
}
2539

2640
/// Schedules index tasks and keeps track of the index status of files.
@@ -46,22 +60,51 @@ public final actor SemanticIndexManager {
4660
/// workspaces.
4761
private let indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>
4862

63+
/// Called when files are scheduled to be indexed.
64+
///
65+
/// The parameter is the number of files that were scheduled to be indexed.
66+
private let indexTasksWereScheduled: @Sendable (_ numberOfFileScheduled: Int) -> Void
67+
4968
/// Callback that is called when an index task has finished.
5069
///
51-
/// Currently only used for testing.
52-
private let indexTaskDidFinish: (@Sendable () -> Void)?
70+
/// An object observing this property probably wants to check `inProgressIndexTasks` when the callback is called to
71+
/// get the current list of in-progress index tasks.
72+
///
73+
/// The number of `indexTaskDidFinish` calls does not have to relate to the number of `indexTasksWereScheduled` calls.
74+
private let indexTaskDidFinish: @Sendable () -> Void
5375

5476
// MARK: - Public API
5577

78+
/// The files that still need to be indexed.
79+
///
80+
/// See `FileIndexStatus` for the distinction between `scheduled` and `executing`.
81+
public var inProgressIndexTasks: (scheduled: [DocumentURI], executing: [DocumentURI]) {
82+
let scheduled = indexStatus.compactMap { (uri: DocumentURI, status: FileIndexStatus) in
83+
if case .scheduled = status {
84+
return uri
85+
}
86+
return nil
87+
}
88+
let inProgress = indexStatus.compactMap { (uri: DocumentURI, status: FileIndexStatus) in
89+
if case .executing = status {
90+
return uri
91+
}
92+
return nil
93+
}
94+
return (scheduled, inProgress)
95+
}
96+
5697
public init(
5798
index: UncheckedIndex,
5899
buildSystemManager: BuildSystemManager,
59100
indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>,
60-
indexTaskDidFinish: (@Sendable () -> Void)?
101+
indexTasksWereScheduled: @escaping @Sendable (Int) -> Void,
102+
indexTaskDidFinish: @escaping @Sendable () -> Void
61103
) {
62104
self.index = index.checked(for: .modifiedFiles)
63105
self.buildSystemManager = buildSystemManager
64106
self.indexTaskScheduler = indexTaskScheduler
107+
self.indexTasksWereScheduled = indexTasksWereScheduled
65108
self.indexTaskDidFinish = indexTaskDidFinish
66109
}
67110

@@ -93,7 +136,7 @@ public final actor SemanticIndexManager {
93136
await withTaskGroup(of: Void.self) { taskGroup in
94137
for (_, status) in indexStatus {
95138
switch status {
96-
case .inProgress(let task):
139+
case .scheduled(let task), .executing(let task):
97140
taskGroup.addTask {
98141
await task.value
99142
}
@@ -138,7 +181,7 @@ public final actor SemanticIndexManager {
138181
)
139182
)
140183
await self.indexTaskScheduler.schedule(priority: priority, taskDescription).value
141-
self.indexTaskDidFinish?()
184+
self.indexTaskDidFinish()
142185
}
143186

144187
/// Update the index store for the given files, assuming that their targets have already been prepared.
@@ -150,11 +193,44 @@ public final actor SemanticIndexManager {
150193
index: self.index.unchecked
151194
)
152195
)
153-
await self.indexTaskScheduler.schedule(priority: priority, taskDescription).value
154-
for file in files {
155-
self.indexStatus[file] = .upToDate
196+
let updateIndexStoreTask = await self.indexTaskScheduler.schedule(priority: priority, taskDescription) { newState in
197+
switch newState {
198+
case .executing:
199+
for file in files {
200+
if case .scheduled(let task) = self.indexStatus[file] {
201+
self.indexStatus[file] = .executing(task)
202+
} else {
203+
logger.fault(
204+
"""
205+
Index status of \(file) is in an unexpected state \
206+
'\(self.indexStatus[file]?.description ?? "<nil>", privacy: .public)' when update index store task \
207+
started executing
208+
"""
209+
)
210+
}
211+
}
212+
case .cancelledToBeRescheduled:
213+
for file in files {
214+
if case .executing(let task) = self.indexStatus[file] {
215+
self.indexStatus[file] = .scheduled(task)
216+
} else {
217+
logger.fault(
218+
"""
219+
Index status of \(file) is in an unexpected state \
220+
'\(self.indexStatus[file]?.description ?? "<nil>", privacy: .public)' when update index store task \
221+
is cancelled to be rescheduled.
222+
"""
223+
)
224+
}
225+
}
226+
case .finished:
227+
for file in files {
228+
self.indexStatus[file] = .upToDate
229+
}
230+
self.indexTaskDidFinish()
231+
}
156232
}
157-
self.indexTaskDidFinish?()
233+
await updateIndexStoreTask.value
158234
}
159235

160236
/// Index the given set of files at the given priority.
@@ -226,9 +302,15 @@ public final actor SemanticIndexManager {
226302
}
227303
indexTasks.append(indexTask)
228304

229-
for file in targetsBatch.flatMap({ filesByTarget[$0]! }) {
230-
indexStatus[file] = .inProgress(indexTask)
305+
let filesToIndex = targetsBatch.flatMap({ filesByTarget[$0]! })
306+
for file in filesToIndex {
307+
// indexStatus will get set to `.upToDate` by `updateIndexStore`. Setting it to `.upToDate` cannot race with
308+
// setting it to `.scheduled` because we don't have an `await` call between the creation of `indexTask` and
309+
// this loop, so we still have exclusive access to the `SemanticIndexManager` actor and hence `updateIndexStore`
310+
// can't execute until we have set all index statuses to `.scheduled`.
311+
indexStatus[file] = .scheduled(indexTask)
231312
}
313+
indexTasksWereScheduled(filesToIndex.count)
232314
}
233315
let indexTasksImmutable = indexTasks
234316

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ add_library(SourceKitLSP STATIC
33
CapabilityRegistry.swift
44
DocumentManager.swift
55
DocumentSnapshot+FromFileContents.swift
6+
IndexProgressManager.swift
67
IndexStoreDB+MainFilesProvider.swift
78
LanguageService.swift
89
Rename.swift
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import LanguageServerProtocol
14+
import SKSupport
15+
import SemanticIndex
16+
17+
/// Listens for index status updates from `SemanticIndexManagers`. From that information, it manages a
18+
/// `WorkDoneProgress` that communicates the index progress to the editor.
19+
actor IndexProgressManager {
20+
/// A queue on which `indexTaskWasQueued` and `indexStatusDidChange` are handled.
21+
///
22+
/// This allows the two functions two be `nonisolated` (and eg. the caller of `indexStatusDidChange` doesn't have to
23+
/// wait for the work done progress to be updated) while still guaranteeing that we handle them in the order they
24+
/// were called.
25+
private let queue = AsyncQueue<Serial>()
26+
27+
/// The `SourceKitLSPServer` for which this manages the index progress. It gathers all `SemanticIndexManagers` from
28+
/// the workspaces in the `SourceKitLSPServer`.
29+
private weak var sourceKitLSPServer: SourceKitLSPServer?
30+
31+
/// This is the target number of index tasks (eg. the `3` in `1/3 done`).
32+
///
33+
/// Every time a new index task is scheduled, this number gets incremented, so that it only ever increases.
34+
/// When indexing of one session is done (ie. when there are no more `scheduled` or `executing` tasks in any
35+
/// `SemanticIndexManager`), `queuedIndexTasks` gets reset to 0 and the work done progress gets ended.
36+
/// This way, when the next work done progress is started, it starts at zero again.
37+
///
38+
/// The number of outstanding tasks is determined from the `scheduled` and `executing` tasks in all the
39+
/// `SemanticIndexManager`s.
40+
private var queuedIndexTasks = 0
41+
42+
/// While there are ongoing index tasks, a `WorkDoneProgressManager` that displays the work done progress.
43+
private var workDoneProgress: WorkDoneProgressManager?
44+
45+
init(sourceKitLSPServer: SourceKitLSPServer) {
46+
self.sourceKitLSPServer = sourceKitLSPServer
47+
}
48+
49+
/// Called when a new file is scheduled to be indexed. Increments the target index count, eg. the 3 in `1/3`.
50+
nonisolated func indexTaskWasQueued(count: Int) {
51+
queue.async {
52+
await self.indexTaskWasQueuedImpl(count: count)
53+
}
54+
}
55+
56+
private func indexTaskWasQueuedImpl(count: Int) async {
57+
queuedIndexTasks += count
58+
await indexStatusDidChangeImpl()
59+
}
60+
61+
/// Called when a `SemanticIndexManager` finishes indexing a file. Adjusts the done index count, eg. the 1 in `1/3`.
62+
nonisolated func indexStatusDidChange() {
63+
queue.async {
64+
await self.indexStatusDidChangeImpl()
65+
}
66+
}
67+
68+
private func indexStatusDidChangeImpl() async {
69+
guard let sourceKitLSPServer else {
70+
workDoneProgress = nil
71+
return
72+
}
73+
var scheduled: [DocumentURI] = []
74+
var executing: [DocumentURI] = []
75+
for indexManager in await sourceKitLSPServer.workspaces.compactMap({ $0.semanticIndexManager }) {
76+
let inProgress = await indexManager.inProgressIndexTasks
77+
scheduled += inProgress.scheduled
78+
executing += inProgress.executing
79+
}
80+
81+
if scheduled.isEmpty && executing.isEmpty {
82+
// Nothing left to index. Reset the target count and dismiss the work done progress.
83+
queuedIndexTasks = 0
84+
workDoneProgress = nil
85+
return
86+
}
87+
88+
let finishedTasks = queuedIndexTasks - scheduled.count - executing.count
89+
let message = "\(finishedTasks) / \(queuedIndexTasks)"
90+
91+
let percentage = Int(Double(finishedTasks) / Double(queuedIndexTasks) * 100)
92+
if let workDoneProgress {
93+
workDoneProgress.update(message: message, percentage: percentage)
94+
} else {
95+
workDoneProgress = await WorkDoneProgressManager(
96+
server: sourceKitLSPServer,
97+
title: "Indexing",
98+
message: message,
99+
percentage: percentage
100+
)
101+
}
102+
}
103+
}

Sources/SourceKitLSP/SourceKitLSPServer+Options.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ extension SourceKitLSPServer {
4949
/// notification when running unit tests.
5050
public var swiftPublishDiagnosticsDebounceDuration: TimeInterval
5151

52+
/// A callback that is called when an index task finishes.
53+
///
54+
/// Intended for testing purposes.
55+
public var indexTaskDidFinish: (@Sendable () -> Void)?
56+
5257
public init(
5358
buildSetup: BuildSetup = .default,
5459
clangdOptions: [String] = [],

0 commit comments

Comments
 (0)