Skip to content

Commit 6f65659

Browse files
committed
When interacting with a document, prepare the target it belongs to
Whenever we get request for a document, open it or edit it, trigger a preparation of its target, but don’t block any interaction based on it. This should ensure that the target is usually prepared when the user is interacting with it. We need to track the preparation status of targets somewhat accurately in `SemanticIndexManager`, so we don’t unnecessarily re-prepare a target. When updating the index store, it is acceptable to schedule another `UpdateIndexStoreTaskDescription` because it will early exit based on an `mtime` check with the unit file. Null builds of a target take significantly longer and thus we want to avoid them. Fixes #1252 rdar://127474003
1 parent 9c9d771 commit 6f65659

File tree

6 files changed

+225
-108
lines changed

6 files changed

+225
-108
lines changed

Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,9 @@ public actor SwiftPMBuildSystem {
128128
logger.log(level: diagnostic.severity.asLogLevel, "SwiftPM log: \(diagnostic.description)")
129129
})
130130

131-
/// Whether the SwiftPMBuildSystem may modify `Package.resolved` or not.
132-
///
133-
/// This is `false` if the `SwiftPMBuildSystem` is pointed at a `.index-build` directory that's independent of the
134-
/// user's build. In this case `SwiftPMBuildSystem` is allowed to clone repositories even if no `Package.resolved`
135-
/// exists.
136-
private let forceResolvedVersions: Bool
131+
/// Whether the `SwiftPMBuildSystem` is pointed at a `.index-build` directory that's independent of the
132+
/// user's build.
133+
private let isForIndexBuild: Bool
137134

138135
/// Creates a build system using the Swift Package Manager, if this workspace is a package.
139136
///
@@ -148,13 +145,13 @@ public actor SwiftPMBuildSystem {
148145
toolchainRegistry: ToolchainRegistry,
149146
fileSystem: FileSystem = localFileSystem,
150147
buildSetup: BuildSetup,
151-
forceResolvedVersions: Bool,
148+
isForIndexBuild: Bool,
152149
reloadPackageStatusCallback: @escaping (ReloadPackageStatus) async -> Void = { _ in }
153150
) async throws {
154151
self.workspacePath = workspacePath
155152
self.fileSystem = fileSystem
156153
self.toolchainRegistry = toolchainRegistry
157-
self.forceResolvedVersions = forceResolvedVersions
154+
self.isForIndexBuild = isForIndexBuild
158155

159156
guard let packageRoot = findPackageDirectory(containing: workspacePath, fileSystem) else {
160157
throw Error.noManifest(workspacePath: workspacePath)
@@ -234,7 +231,7 @@ public actor SwiftPMBuildSystem {
234231
url: URL,
235232
toolchainRegistry: ToolchainRegistry,
236233
buildSetup: BuildSetup,
237-
forceResolvedVersions: Bool,
234+
isForIndexBuild: Bool,
238235
reloadPackageStatusCallback: @escaping (ReloadPackageStatus) async -> Void
239236
) async {
240237
do {
@@ -243,7 +240,7 @@ public actor SwiftPMBuildSystem {
243240
toolchainRegistry: toolchainRegistry,
244241
fileSystem: localFileSystem,
245242
buildSetup: buildSetup,
246-
forceResolvedVersions: forceResolvedVersions,
243+
isForIndexBuild: isForIndexBuild,
247244
reloadPackageStatusCallback: reloadPackageStatusCallback
248245
)
249246
} catch Error.noManifest {
@@ -272,7 +269,7 @@ extension SwiftPMBuildSystem {
272269

273270
let modulesGraph = try self.workspace.loadPackageGraph(
274271
rootInput: PackageGraphRootInput(packages: [AbsolutePath(projectRoot)]),
275-
forceResolvedVersions: forceResolvedVersions,
272+
forceResolvedVersions: !isForIndexBuild,
276273
observabilityScope: observabilitySystem.topScope
277274
)
278275

@@ -430,6 +427,8 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
430427
for target in targets {
431428
try await prepare(singleTarget: target)
432429
}
430+
let filesInPreparedTargets = targets.flatMap { self.targets[$0.targetID]?.buildTarget.sources ?? [] }
431+
await fileDependenciesUpdatedDebouncer.scheduleCall(Set(filesInPreparedTargets.map(DocumentURI.init)))
433432
}
434433

435434
private func prepare(singleTarget target: ConfiguredTarget) async throws {
@@ -561,9 +560,9 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
561560
// The file watching here is somewhat fragile as well because it assumes that the `.swiftmodule` files are being
562561
// written to a directory within the workspace root. This is not necessarily true if the user specifies a build
563562
// directory outside the source tree.
564-
// All of this shouldn't be necessary once we have background preparation, in which case we know when preparation of
565-
// a target has finished.
566-
if events.contains(where: { $0.uri.fileURL?.pathExtension == "swiftmodule" }) {
563+
// If we have background indexing enabled, this is not necessary because we call `fileDependenciesUpdated` when
564+
// preparation of a target finishes.
565+
if !isForIndexBuild, events.contains(where: { $0.uri.fileURL?.pathExtension == "swiftmodule" }) {
567566
filesWithUpdatedDependencies.formUnion(self.fileToTarget.keys.map { DocumentURI($0.asURL) })
568567
}
569568
await self.fileDependenciesUpdatedDebouncer.scheduleCall(filesWithUpdatedDependencies)

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 111 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ import LanguageServerProtocol
1616
import SKCore
1717

1818
/// Describes the state of indexing for a single source file
19-
private enum IndexStatus {
19+
private enum IndexStatus<T> {
2020
/// The index is up-to-date.
2121
case upToDate
2222
/// The file or target is not up to date and we have scheduled a task to update the index store for the file / prepare
2323
/// the target it but that index operation hasn't been started yet.
24-
case scheduled(Task<Void, Never>)
24+
case scheduled(T)
2525
/// We are currently actively updating the index store for the file / preparing the target, ie. we are running a
2626
/// subprocess that updates the index store / prepares a target.
27-
case executing(Task<Void, Never>)
27+
case executing(T)
2828

2929
var description: String {
3030
switch self {
@@ -54,12 +54,23 @@ public final actor SemanticIndexManager {
5454
/// The preparation status of the targets that the `SemanticIndexManager` has started preparation for.
5555
///
5656
/// Targets will be removed from this dictionary when they are no longer known to be up-to-date.
57-
private var preparationStatus: [ConfiguredTarget: IndexStatus] = [:]
57+
///
58+
/// The associated values of the `IndexStatus` are:
59+
/// - A UUID to track the task. This is used to ensure that status updates from this task don't update
60+
/// `preparationStatus` for targets that are tracked by a different task.
61+
/// - The list of targets that are being prepared in a joint preparation operation
62+
/// - The task that prepares the target
63+
private var preparationStatus: [ConfiguredTarget: IndexStatus<(UUID, [ConfiguredTarget], Task<Void, Never>)>] = [:]
5864

5965
/// The index status of the source files that the `SemanticIndexManager` knows about.
6066
///
6167
/// Files will be removed from this dictionary if their index is no longer up-to-date.
62-
private var indexStatus: [DocumentURI: IndexStatus] = [:]
68+
///
69+
/// The associated values of the `IndexStatus` are:
70+
/// - A UUID to track the task. This is used to ensure that status updates from this task don't update
71+
/// `preparationStatus` for targets that are tracked by a different task.
72+
/// - The task that prepares the target
73+
private var indexStatus: [DocumentURI: IndexStatus<(UUID, Task<Void, Never>)>] = [:]
6374

6475
/// The `TaskScheduler` that manages the scheduling of index tasks. This is shared among all `SemanticIndexManager`s
6576
/// in the process, to ensure that we don't schedule more index operations than processor cores from multiple
@@ -122,7 +133,7 @@ public final actor SemanticIndexManager {
122133
///
123134
/// Indexing is being performed with a low priority.
124135
private func scheduleBackgroundIndex(files: some Collection<DocumentURI>) async {
125-
await self.index(files: files, priority: .low)
136+
_ = await self.scheduleIndexing(of: files, priority: .low)
126137
}
127138

128139
/// Regenerate the build graph (also resolving package dependencies) and then index all the source files known to the
@@ -156,7 +167,7 @@ public final actor SemanticIndexManager {
156167
await withTaskGroup(of: Void.self) { taskGroup in
157168
for (_, status) in indexStatus {
158169
switch status {
159-
case .scheduled(let task), .executing(let task):
170+
case .scheduled((_, let task)), .executing((_, let task)):
160171
taskGroup.addTask {
161172
await task.value
162173
}
@@ -185,7 +196,7 @@ public final actor SemanticIndexManager {
185196
// Create a new index task for the files that aren't up-to-date. The newly scheduled index tasks will
186197
// - Wait for the existing index operations to finish if they have the same number of files.
187198
// - Reschedule the background index task in favor of an index task with fewer source files.
188-
await self.index(files: uris, priority: nil).value
199+
await self.scheduleIndexing(of: uris, priority: nil).value
189200
index.pollForUnitChangesAndWait()
190201
logger.debug("Done waiting for up-to-date index")
191202
}
@@ -233,67 +244,101 @@ public final actor SemanticIndexManager {
233244
return filesToReIndex
234245
}
235246

247+
/// Schedule preparation of the target that contains the given URI, building all modules that the file depends on.
248+
///
249+
/// This is intended to be called when the user is interacting with the document at the given URI.
250+
public func schedulePreparation(of uri: DocumentURI, priority: TaskPriority? = nil) async {
251+
Task(priority: priority) {
252+
await withLoggingScope("preparation") {
253+
guard let target = await buildSystemManager.canonicalConfiguredTarget(for: uri) else {
254+
return
255+
}
256+
await self.prepare(targets: [target], priority: priority)
257+
}
258+
}
259+
}
260+
236261
// MARK: - Helper functions
237262

238263
/// Prepare the given targets for indexing
239264
private func prepare(targets: [ConfiguredTarget], priority: TaskPriority?) async {
240-
let targetsToPrepare = targets.filter {
241-
if case .upToDate = preparationStatus[$0] {
242-
return false
265+
var targetsToPrepare: [ConfiguredTarget] = []
266+
var preparationTasksToAwait: [Task<Void, Never>] = []
267+
for target in targets {
268+
switch preparationStatus[target] {
269+
case .upToDate:
270+
break
271+
case .scheduled((_, let configuredTargets, let task)), .executing((_, let configuredTargets, let task)):
272+
// If we already have a task scheduled that prepares fewer targets, await that instead of overriding the
273+
// target's preparation status with a longer-running task. The key benefit here is that when we get many
274+
// preparation requests for the same target (eg. one for every text document request sent to a file), we don't
275+
// re-create new `PreparationTaskDescription`s for every preparation request. Instead, all the preparation
276+
// requests await the same task. At the same time, if we have a multi-file preparation request and then get a
277+
// single-file preparation request, we will override the preparation of that target with the single-file
278+
// preparation task, ensuring that the task gets prepared as quickly as possible.
279+
if configuredTargets.count <= targets.count {
280+
preparationTasksToAwait.append(task)
281+
}
282+
fallthrough
283+
case nil:
284+
targetsToPrepare.append(target)
243285
}
244-
return true
245286
}
287+
246288
let taskDescription = AnyIndexTaskDescription(
247289
PreparationTaskDescription(
248290
targetsToPrepare: targetsToPrepare,
249291
buildSystemManager: self.buildSystemManager
250292
)
251293
)
252-
let preparationTask = await self.indexTaskScheduler.schedule(priority: priority, taskDescription) { newState in
253-
switch newState {
254-
case .executing:
255-
for target in targetsToPrepare {
256-
if case .scheduled(let task) = self.preparationStatus[target] {
257-
self.preparationStatus[target] = .executing(task)
258-
} else {
259-
logger.fault(
260-
"""
261-
Preparation status of \(target.forLogging) is in an unexpected state \
262-
'\(self.preparationStatus[target]?.description ?? "<nil>", privacy: .public)' when preparation task \
263-
started executing
264-
"""
265-
)
294+
if !targetsToPrepare.isEmpty {
295+
// A UUID that is used to identify the task. This ensures that status updates from this task don't update
296+
// `preparationStatus` for targets that are tracked by a different task, eg. because this task is a multi-target
297+
// preparation task and the target's status is now tracked by a single-file preparation task.
298+
let taskID = UUID()
299+
let preparationTask = await self.indexTaskScheduler.schedule(priority: priority, taskDescription) { newState in
300+
switch newState {
301+
case .executing:
302+
for target in targetsToPrepare {
303+
if case .scheduled((taskID, let targets, let task)) = self.preparationStatus[target] {
304+
self.preparationStatus[target] = .executing((taskID, targets, task))
305+
}
266306
}
267-
}
268-
case .cancelledToBeRescheduled:
269-
for target in targetsToPrepare {
270-
if case .executing(let task) = self.preparationStatus[target] {
271-
self.preparationStatus[target] = .scheduled(task)
272-
} else {
273-
logger.fault(
274-
"""
275-
Preparation status of \(target.forLogging) is in an unexpected state \
276-
'\(self.preparationStatus[target]?.description ?? "<nil>", privacy: .public)' when preparation task \
277-
is cancelled to be rescheduled.
278-
"""
279-
)
307+
case .cancelledToBeRescheduled:
308+
for target in targetsToPrepare {
309+
if case .executing((taskID, let targets, let task)) = self.preparationStatus[target] {
310+
self.preparationStatus[target] = .scheduled((taskID, targets, task))
311+
}
280312
}
313+
case .finished:
314+
for target in targetsToPrepare {
315+
switch self.preparationStatus[target] {
316+
case .executing((taskID, _, _)):
317+
self.preparationStatus[target] = .upToDate
318+
default:
319+
break
320+
}
321+
}
322+
self.indexTaskDidFinish()
281323
}
282-
case .finished:
283-
for target in targetsToPrepare {
284-
self.preparationStatus[target] = .upToDate
285-
}
286-
self.indexTaskDidFinish()
287324
}
325+
for target in targetsToPrepare {
326+
preparationStatus[target] = .scheduled((taskID, targetsToPrepare, preparationTask))
327+
}
328+
preparationTasksToAwait.append(preparationTask)
288329
}
289-
for target in targetsToPrepare {
290-
preparationStatus[target] = .scheduled(preparationTask)
330+
await withTaskGroup(of: Void.self) { taskGroup in
331+
for task in preparationTasksToAwait {
332+
taskGroup.addTask {
333+
await task.value
334+
}
335+
}
336+
await taskGroup.waitForAll()
291337
}
292-
await preparationTask.value
293338
}
294339

295340
/// Update the index store for the given files, assuming that their targets have already been prepared.
296-
private func updateIndexStore(for files: [FileToIndex], priority: TaskPriority?) async {
341+
private func updateIndexStore(for files: [FileToIndex], taskID: UUID, priority: TaskPriority?) async {
297342
let taskDescription = AnyIndexTaskDescription(
298343
UpdateIndexStoreTaskDescription(
299344
filesToIndex: Set(files),
@@ -305,47 +350,38 @@ public final actor SemanticIndexManager {
305350
switch newState {
306351
case .executing:
307352
for file in files {
308-
if case .scheduled(let task) = self.indexStatus[file.uri] {
309-
self.indexStatus[file.uri] = .executing(task)
310-
} else {
311-
logger.fault(
312-
"""
313-
Index status of \(file.uri) is in an unexpected state \
314-
'\(self.indexStatus[file.uri]?.description ?? "<nil>", privacy: .public)' when update index store task \
315-
started executing
316-
"""
317-
)
353+
if case .scheduled((taskID, let task)) = self.indexStatus[file.uri] {
354+
self.indexStatus[file.uri] = .executing((taskID, task))
318355
}
319356
}
320357
case .cancelledToBeRescheduled:
321358
for file in files {
322-
if case .executing(let task) = self.indexStatus[file.uri] {
323-
self.indexStatus[file.uri] = .scheduled(task)
324-
} else {
325-
logger.fault(
326-
"""
327-
Index status of \(file.uri) is in an unexpected state \
328-
'\(self.indexStatus[file.uri]?.description ?? "<nil>", privacy: .public)' when update index store task \
329-
is cancelled to be rescheduled.
330-
"""
331-
)
359+
if case .executing((taskID, let task)) = self.indexStatus[file.uri] {
360+
self.indexStatus[file.uri] = .scheduled((taskID, task))
332361
}
333362
}
334363
case .finished:
335364
for file in files {
336-
self.indexStatus[file.uri] = .upToDate
365+
switch self.indexStatus[file.uri] {
366+
case .executing((taskID, _)):
367+
self.indexStatus[file.uri] = .upToDate
368+
default:
369+
break
370+
}
337371
}
338372
self.indexTaskDidFinish()
339373
}
340374
}
341375
await updateIndexStoreTask.value
342376
}
343377

344-
/// Index the given set of files at the given priority.
378+
/// Index the given set of files at the given priority, preparing their targets beforehand, if needed.
345379
///
346380
/// The returned task finishes when all files are indexed.
347-
@discardableResult
348-
private func index(files: some Collection<DocumentURI>, priority: TaskPriority?) async -> Task<Void, Never> {
381+
private func scheduleIndexing(
382+
of files: some Collection<DocumentURI>,
383+
priority: TaskPriority?
384+
) async -> Task<Void, Never> {
349385
let outOfDateFiles = await filesToIndex(toCover: files).filter {
350386
if case .upToDate = indexStatus[$0.uri] {
351387
return false
@@ -389,6 +425,7 @@ public final actor SemanticIndexManager {
389425
// processor count, so we can get parallelism during preparation.
390426
// https://github.com/apple/sourcekit-lsp/issues/1262
391427
for targetsBatch in sortedTargets.partition(intoBatchesOfSize: 1) {
428+
let taskID = UUID()
392429
let indexTask = Task(priority: priority) {
393430
// First prepare the targets.
394431
await prepare(targets: targetsBatch, priority: priority)
@@ -401,7 +438,7 @@ public final actor SemanticIndexManager {
401438
// https://github.com/apple/sourcekit-lsp/issues/1268
402439
for fileBatch in filesByTarget[target]!.partition(intoBatchesOfSize: 1) {
403440
taskGroup.addTask {
404-
await self.updateIndexStore(for: fileBatch, priority: priority)
441+
await self.updateIndexStore(for: fileBatch, taskID: taskID, priority: priority)
405442
}
406443
}
407444
}
@@ -416,7 +453,7 @@ public final actor SemanticIndexManager {
416453
// setting it to `.scheduled` because we don't have an `await` call between the creation of `indexTask` and
417454
// this loop, so we still have exclusive access to the `SemanticIndexManager` actor and hence `updateIndexStore`
418455
// can't execute until we have set all index statuses to `.scheduled`.
419-
indexStatus[file.uri] = .scheduled(indexTask)
456+
indexStatus[file.uri] = .scheduled((taskID, indexTask))
420457
}
421458
indexTasksWereScheduled(filesToIndex.count)
422459
}

0 commit comments

Comments
 (0)