Skip to content

Commit ba56bae

Browse files
authored
Merge pull request #1275 from ahoppen/index-progress
Add a work done progress that shows the index progress
2 parents 1fb087f + a6389e5 commit ba56bae

16 files changed

+477
-172
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/SKTestSupport/MultiFileTestProject.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,10 @@ public class MultiFileTestProject {
8080
public init(
8181
files: [RelativeFileLocation: String],
8282
workspaces: (URL) async throws -> [WorkspaceFolder] = { [WorkspaceFolder(uri: DocumentURI($0))] },
83+
capabilities: ClientCapabilities = ClientCapabilities(),
8384
serverOptions: SourceKitLSPServer.Options = .testDefault,
8485
usePullDiagnostics: Bool = true,
86+
preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil,
8587
testName: String = #function
8688
) async throws {
8789
scratchDirectory = try testScratchDir(testName: testName)
@@ -112,8 +114,10 @@ public class MultiFileTestProject {
112114

113115
self.testClient = try await TestSourceKitLSPClient(
114116
serverOptions: serverOptions,
117+
capabilities: capabilities,
115118
usePullDiagnostics: usePullDiagnostics,
116119
workspaceFolders: workspaces(scratchDirectory),
120+
preInitialization: preInitialization,
117121
cleanUp: { [scratchDirectory] in
118122
if cleanScratchDirectories {
119123
try? FileManager.default.removeItem(at: scratchDirectory)

Sources/SKTestSupport/SwiftPMTestProject.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ public class SwiftPMTestProject: MultiFileTestProject {
4242
workspaces: (URL) async throws -> [WorkspaceFolder] = { [WorkspaceFolder(uri: DocumentURI($0))] },
4343
build: Bool = false,
4444
allowBuildFailure: Bool = false,
45+
capabilities: ClientCapabilities = ClientCapabilities(),
4546
serverOptions: SourceKitLSPServer.Options = .testDefault,
4647
pollIndex: Bool = true,
48+
preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil,
4749
usePullDiagnostics: Bool = true,
4850
testName: String = #function
4951
) async throws {
@@ -66,8 +68,10 @@ public class SwiftPMTestProject: MultiFileTestProject {
6668
try await super.init(
6769
files: filesByPath,
6870
workspaces: workspaces,
71+
capabilities: capabilities,
6972
serverOptions: serverOptions,
7073
usePullDiagnostics: usePullDiagnostics,
74+
preInitialization: preInitialization,
7175
testName: testName
7276
)
7377

Sources/SKTestSupport/TestSourceKitLSPClient.swift

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ public final class TestSourceKitLSPClient: MessageHandler {
8383
/// - capabilities: The test client's capabilities.
8484
/// - usePullDiagnostics: Whether to use push diagnostics or use push-based diagnostics
8585
/// - workspaceFolders: Workspace folders to open.
86+
/// - preInitialization: A closure that is called after the test client is created but before SourceKit-LSP is
87+
/// initialized. This can be used to eg. register request handlers.
8688
/// - cleanUp: A closure that is called when the `TestSourceKitLSPClient` is destructed.
8789
/// This allows e.g. a `IndexedSingleSwiftFileTestProject` to delete its temporary files when they are no longer
8890
/// needed.
@@ -94,6 +96,7 @@ public final class TestSourceKitLSPClient: MessageHandler {
9496
capabilities: ClientCapabilities = ClientCapabilities(),
9597
usePullDiagnostics: Bool = true,
9698
workspaceFolders: [WorkspaceFolder]? = nil,
99+
preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil,
97100
cleanUp: @escaping () -> Void = {}
98101
) async throws {
99102
if !useGlobalModuleCache {
@@ -135,7 +138,7 @@ public final class TestSourceKitLSPClient: MessageHandler {
135138
guard capabilities.textDocument!.diagnostic == nil else {
136139
struct ConflictingDiagnosticsError: Error, CustomStringConvertible {
137140
var description: String {
138-
"usePushDiagnostics = false is not supported if capabilities already contain diagnostic options"
141+
"usePullDiagnostics = false is not supported if capabilities already contain diagnostic options"
139142
}
140143
}
141144
throw ConflictingDiagnosticsError()
@@ -145,6 +148,7 @@ public final class TestSourceKitLSPClient: MessageHandler {
145148
XCTAssertEqual(request.registrations.only?.method, DocumentDiagnosticsRequest.method)
146149
return VoidResponse()
147150
}
151+
preInitialization?(self)
148152
}
149153
if initialize {
150154
_ = try await self.send(
@@ -286,18 +290,19 @@ public final class TestSourceKitLSPClient: MessageHandler {
286290
id: LanguageServerProtocol.RequestID,
287291
reply: @escaping (LSPResult<Request.Response>) -> Void
288292
) {
289-
guard let requestHandler = requestHandlers.first else {
290-
reply(.failure(.methodNotFound(Request.method)))
291-
return
292-
}
293-
guard let requestHandler = requestHandler as? RequestHandler<Request> else {
294-
print("\(RequestHandler<Request>.self)")
295-
XCTFail("Received request of unexpected type \(Request.method)")
293+
let requestHandlerAndIndex = requestHandlers.enumerated().compactMap {
294+
(index, handler) -> (RequestHandler<Request>, Int)? in
295+
guard let handler = handler as? RequestHandler<Request> else {
296+
return nil
297+
}
298+
return (handler, index)
299+
}.first
300+
guard let (requestHandler, index) = requestHandlerAndIndex else {
296301
reply(.failure(.methodNotFound(Request.method)))
297302
return
298303
}
299304
reply(.success(requestHandler(params)))
300-
requestHandlers.removeFirst()
305+
requestHandlers.remove(at: index)
301306
}
302307

303308
// MARK: - Convenience functions

Sources/SemanticIndex/IndexTaskDescription.swift

Lines changed: 44 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -12,94 +12,72 @@
1212

1313
import SKCore
1414

15-
/// A task that either prepares targets or updates the index store for a set of files.
16-
public enum IndexTaskDescription: TaskDescriptionProtocol {
17-
case updateIndexStore(UpdateIndexStoreTaskDescription)
18-
case preparation(PreparationTaskDescription)
15+
/// Protocol of tasks that are executed on the index task scheduler.
16+
///
17+
/// It is assumed that `IndexTaskDescription` of different types are allowed to execute in parallel.
18+
protocol IndexTaskDescription: TaskDescriptionProtocol {
19+
/// A string that is unique to this type of `IndexTaskDescription`. It is used to produce unique IDs for tasks of
20+
/// different types in `AnyIndexTaskDescription`
21+
static var idPrefix: String { get }
22+
23+
var id: UInt32 { get }
24+
}
25+
26+
extension IndexTaskDescription {
27+
func dependencies(
28+
to currentlyExecutingTasks: [AnyIndexTaskDescription]
29+
) -> [TaskDependencyAction<AnyIndexTaskDescription>] {
30+
return self.dependencies(to: currentlyExecutingTasks.compactMap { $0.wrapped as? Self })
31+
.map {
32+
switch $0 {
33+
case .cancelAndRescheduleDependency(let td):
34+
return .cancelAndRescheduleDependency(AnyIndexTaskDescription(td))
35+
case .waitAndElevatePriorityOfDependency(let td):
36+
return .waitAndElevatePriorityOfDependency(AnyIndexTaskDescription(td))
37+
}
38+
}
39+
40+
}
41+
}
42+
43+
/// Type-erased wrapper of an `IndexTaskDescription`.
44+
public struct AnyIndexTaskDescription: TaskDescriptionProtocol {
45+
let wrapped: any IndexTaskDescription
46+
47+
init(_ wrapped: any IndexTaskDescription) {
48+
self.wrapped = wrapped
49+
}
1950

2051
public var isIdempotent: Bool {
21-
switch self {
22-
case .updateIndexStore(let taskDescription): return taskDescription.isIdempotent
23-
case .preparation(let taskDescription): return taskDescription.isIdempotent
24-
}
52+
return wrapped.isIdempotent
2553
}
2654

2755
public var estimatedCPUCoreCount: Int {
28-
switch self {
29-
case .updateIndexStore(let taskDescription): return taskDescription.estimatedCPUCoreCount
30-
case .preparation(let taskDescription): return taskDescription.estimatedCPUCoreCount
31-
}
56+
return wrapped.estimatedCPUCoreCount
3257
}
3358

3459
public var id: String {
35-
switch self {
36-
case .updateIndexStore(let taskDescription): return "indexing-\(taskDescription.id)"
37-
case .preparation(let taskDescription): return "preparation-\(taskDescription.id)"
38-
}
60+
return "\(type(of: wrapped).idPrefix)-\(wrapped.id)"
3961
}
4062

4163
public var description: String {
42-
switch self {
43-
case .updateIndexStore(let taskDescription): return taskDescription.description
44-
case .preparation(let taskDescription): return taskDescription.description
45-
}
64+
return wrapped.description
4665
}
4766

4867
public var redactedDescription: String {
49-
switch self {
50-
case .updateIndexStore(let taskDescription): return taskDescription.redactedDescription
51-
case .preparation(let taskDescription): return taskDescription.redactedDescription
52-
}
68+
return wrapped.redactedDescription
5369
}
5470

5571
public func execute() async {
56-
switch self {
57-
case .updateIndexStore(let taskDescription): return await taskDescription.execute()
58-
case .preparation(let taskDescription): return await taskDescription.execute()
59-
}
72+
return await wrapped.execute()
6073
}
6174

6275
/// Forward to the underlying task to compute the dependencies. Preparation and index tasks don't have any
6376
/// dependencies that are managed by `TaskScheduler`. `SemanticIndexManager` awaits the preparation of a target before
6477
/// indexing files within it.
6578
public func dependencies(
66-
to currentlyExecutingTasks: [IndexTaskDescription]
67-
) -> [TaskDependencyAction<IndexTaskDescription>] {
68-
switch self {
69-
case .updateIndexStore(let taskDescription):
70-
let currentlyExecutingTasks =
71-
currentlyExecutingTasks
72-
.compactMap { (currentlyExecutingTask) -> UpdateIndexStoreTaskDescription? in
73-
if case .updateIndexStore(let currentlyExecutingTask) = currentlyExecutingTask {
74-
return currentlyExecutingTask
75-
}
76-
return nil
77-
}
78-
return taskDescription.dependencies(to: currentlyExecutingTasks).map {
79-
switch $0 {
80-
case .waitAndElevatePriorityOfDependency(let td):
81-
return .waitAndElevatePriorityOfDependency(.updateIndexStore(td))
82-
case .cancelAndRescheduleDependency(let td):
83-
return .cancelAndRescheduleDependency(.updateIndexStore(td))
84-
}
85-
}
86-
case .preparation(let taskDescription):
87-
let currentlyExecutingTasks =
88-
currentlyExecutingTasks
89-
.compactMap { (currentlyExecutingTask) -> PreparationTaskDescription? in
90-
if case .preparation(let currentlyExecutingTask) = currentlyExecutingTask {
91-
return currentlyExecutingTask
92-
}
93-
return nil
94-
}
95-
return taskDescription.dependencies(to: currentlyExecutingTasks).map {
96-
switch $0 {
97-
case .waitAndElevatePriorityOfDependency(let td):
98-
return .waitAndElevatePriorityOfDependency(.preparation(td))
99-
case .cancelAndRescheduleDependency(let td):
100-
return .cancelAndRescheduleDependency(.preparation(td))
101-
}
102-
}
103-
}
79+
to currentlyExecutingTasks: [AnyIndexTaskDescription]
80+
) -> [TaskDependencyAction<AnyIndexTaskDescription>] {
81+
return wrapped.dependencies(to: currentlyExecutingTasks)
10482
}
10583
}

Sources/SemanticIndex/PreparationTaskDescription.swift

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ private var preparationIDForLogging = AtomicUInt32(initialValue: 1)
2424
/// Describes a task to prepare a set of targets.
2525
///
2626
/// This task description can be scheduled in a `TaskScheduler`.
27-
public struct PreparationTaskDescription: TaskDescriptionProtocol {
27+
public struct PreparationTaskDescription: IndexTaskDescription {
28+
public static let idPrefix = "prepare"
29+
2830
public let id = preparationIDForLogging.fetchAndIncrement()
2931

3032
/// The targets that should be prepared.
@@ -33,11 +35,6 @@ public struct PreparationTaskDescription: TaskDescriptionProtocol {
3335
/// The build system manager that is used to get the toolchain and build settings for the files to index.
3436
private let buildSystemManager: BuildSystemManager
3537

36-
/// A callback that is called when the task finishes.
37-
///
38-
/// Intended for testing purposes.
39-
private let didFinishCallback: @Sendable (PreparationTaskDescription) -> Void
40-
4138
/// The task is idempotent because preparing the same target twice produces the same result as preparing it once.
4239
public var isIdempotent: Bool { true }
4340

@@ -53,18 +50,13 @@ public struct PreparationTaskDescription: TaskDescriptionProtocol {
5350

5451
init(
5552
targetsToPrepare: [ConfiguredTarget],
56-
buildSystemManager: BuildSystemManager,
57-
didFinishCallback: @escaping @Sendable (PreparationTaskDescription) -> Void
53+
buildSystemManager: BuildSystemManager
5854
) {
5955
self.targetsToPrepare = targetsToPrepare
6056
self.buildSystemManager = buildSystemManager
61-
self.didFinishCallback = didFinishCallback
6257
}
6358

6459
public func execute() async {
65-
defer {
66-
didFinishCallback(self)
67-
}
6860
// Only use the last two digits of the preparation ID for the logging scope to avoid creating too many scopes.
6961
// See comment in `withLoggingScope`.
7062
// The last 2 digits should be sufficient to differentiate between multiple concurrently running preparation operations

0 commit comments

Comments
 (0)