Skip to content

Commit 10924cf

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 4b9ad3d commit 10924cf

File tree

10 files changed

+290
-36
lines changed

10 files changed

+290
-36
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"git.ignoreLimitWarning": true
3+
}

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: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ 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>)
2427
}
2528

2629
/// Schedules index tasks and keeps track of the index status of files.
@@ -46,22 +49,51 @@ public final actor SemanticIndexManager {
4649
/// workspaces.
4750
private let indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>
4851

52+
/// Called when files are scheduled to be indexed.
53+
///
54+
/// The parameter is the number of files that were scheduled to be indexed.
55+
private let indexTasksWereScheduled: @Sendable (_ numberOfFileScheduled: Int) -> Void
56+
4957
/// Callback that is called when an index task has finished.
5058
///
51-
/// Currently only used for testing.
52-
private let indexTaskDidFinish: (@Sendable () -> Void)?
59+
/// An object observing this property probably wants to check `inProgressIndexTasks` when the callback is called to
60+
/// get the current list of in-progress index tasks.
61+
///
62+
/// The number of `indexTaskDidFinish` calls does not have to relate to the number of `indexTasksWereScheduled` calls.
63+
private let indexTaskDidFinish: @Sendable () -> Void
5364

5465
// MARK: - Public API
5566

67+
/// The files that still need to be indexed.
68+
///
69+
/// See `FileIndexStatus` for the distinction between `scheduled` and `executing`.
70+
public var inProgressIndexTasks: (scheduled: [DocumentURI], executing: [DocumentURI]) {
71+
let scheduled = indexStatus.compactMap { (uri: DocumentURI, status: FileIndexStatus) in
72+
if case .scheduled = status {
73+
return uri
74+
}
75+
return nil
76+
}
77+
let inProgress = indexStatus.compactMap { (uri: DocumentURI, status: FileIndexStatus) in
78+
if case .executing = status {
79+
return uri
80+
}
81+
return nil
82+
}
83+
return (scheduled, inProgress)
84+
}
85+
5686
public init(
5787
index: UncheckedIndex,
5888
buildSystemManager: BuildSystemManager,
5989
indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>,
60-
indexTaskDidFinish: (@Sendable () -> Void)?
90+
indexTasksWereScheduled: @escaping @Sendable (Int) -> Void,
91+
indexTaskDidFinish: @escaping @Sendable () -> Void
6192
) {
6293
self.index = index.checked(for: .modifiedFiles)
6394
self.buildSystemManager = buildSystemManager
6495
self.indexTaskScheduler = indexTaskScheduler
96+
self.indexTasksWereScheduled = indexTasksWereScheduled
6597
self.indexTaskDidFinish = indexTaskDidFinish
6698
}
6799

@@ -93,7 +125,7 @@ public final actor SemanticIndexManager {
93125
await withTaskGroup(of: Void.self) { taskGroup in
94126
for (_, status) in indexStatus {
95127
switch status {
96-
case .inProgress(let task):
128+
case .scheduled(let task), .executing(let task):
97129
taskGroup.addTask {
98130
await task.value
99131
}
@@ -138,7 +170,7 @@ public final actor SemanticIndexManager {
138170
)
139171
)
140172
await self.indexTaskScheduler.schedule(priority: priority, taskDescription).value
141-
self.indexTaskDidFinish?()
173+
self.indexTaskDidFinish()
142174
}
143175

144176
/// Update the index store for the given files, assuming that their targets have already been prepared.
@@ -150,11 +182,28 @@ public final actor SemanticIndexManager {
150182
index: self.index
151183
)
152184
)
153-
await self.indexTaskScheduler.schedule(priority: priority, taskDescription).value
154-
for file in files {
155-
self.indexStatus[file] = .upToDate
185+
let updateIndexStoreTask = await self.indexTaskScheduler.schedule(priority: priority, taskDescription) { newState in
186+
switch newState {
187+
case .executing:
188+
for file in files {
189+
if case .scheduled(let task) = self.indexStatus[file] {
190+
self.indexStatus[file] = .executing(task)
191+
}
192+
}
193+
case .cancelledToBeRescheduled:
194+
for file in files {
195+
if case .executing(let task) = self.indexStatus[file] {
196+
self.indexStatus[file] = .scheduled(task)
197+
}
198+
}
199+
case .finished:
200+
for file in files {
201+
self.indexStatus[file] = .upToDate
202+
}
203+
self.indexTaskDidFinish()
204+
}
156205
}
157-
self.indexTaskDidFinish?()
206+
await updateIndexStoreTask.value
158207
}
159208

160209
/// Index the given set of files at the given priority.
@@ -226,9 +275,15 @@ public final actor SemanticIndexManager {
226275
}
227276
indexTasks.append(indexTask)
228277

229-
for file in targetsBatch.flatMap({ filesByTarget[$0]! }) {
230-
indexStatus[file] = .inProgress(indexTask)
278+
let filesToIndex = targetsBatch.flatMap({ filesByTarget[$0]! })
279+
for file in filesToIndex {
280+
// indexStatus will get set to `.upToDate` by `updateIndexStore`. Setting it to `.upToDate` cannot race with
281+
// setting it to `.scheduled` because we don't have an `await` call between the creation of `indexTask` and
282+
// this loop, so we still have exclusive access to the `SemanticIndexManager` actor and hence `updateIndexStore`
283+
// can't execute until we have set all index statuses to `.scheduled`.
284+
indexStatus[file] = .scheduled(indexTask)
231285
}
286+
indexTasksWereScheduled(filesToIndex.count)
232287
}
233288
let indexTasksImmutable = indexTasks
234289

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)