Skip to content

Commit 58a5ae3

Browse files
committed
Don't wait for build graph generation when file is changed
Since we re-index source files if the build server sends us a `buildTarget/didChange` notification, we no longer need to wait for an up-to-date build graph when a file is modified. This resolves an issue that causes a `Scheduling tasks` progress to appear in the status bar whenever a file in the project is changed. Before swiftlang#1973, this happened fairly frequently during the initial indexing when a header file was written into the build directory. After that PR the `Scheduling Indexing` status appears a lot more frequently, namely every time the index’s database is modified, which happens quite all the time during indexing...
1 parent 71dfc73 commit 58a5ae3

File tree

2 files changed

+108
-90
lines changed

2 files changed

+108
-90
lines changed

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 108 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,11 @@ package final actor SemanticIndexManager {
175175

176176
/// The tasks to generate the build graph (resolving package dependencies, generating the build description,
177177
/// ...) and to schedule indexing of modified tasks.
178-
private var scheduleIndexingTasks: [UUID: Task<Void, Never>] = [:]
178+
private var buildGraphGenerationTasks: [UUID: Task<Void, Never>] = [:]
179+
180+
/// Tasks that were created from `workspace/didChangeWatchedFiles` or `buildTarget/didChange` notifications. Needed
181+
/// so that we can wait for these tasks to be handled in the poll index request.
182+
private var fileOrBuildTargetChangedTasks: [UUID: Task<Void, Never>] = [:]
179183

180184
private let preparationUpToDateTracker = UpToDateTracker<BuildTargetIdentifier>()
181185

@@ -225,7 +229,7 @@ package final actor SemanticIndexManager {
225229
if inProgressPreparationTasks.values.contains(where: { $0.purpose == .forEditorFunctionality }) {
226230
return .preparingFileForEditorFunctionality
227231
}
228-
if !scheduleIndexingTasks.isEmpty {
232+
if !buildGraphGenerationTasks.isEmpty {
229233
return .schedulingIndexing
230234
}
231235
let preparationTasks = inProgressPreparationTasks.mapValues { inProgressTask in
@@ -270,46 +274,37 @@ package final actor SemanticIndexManager {
270274
/// Regenerate the build graph (also resolving package dependencies) and then index all the source files known to the
271275
/// build system that don't currently have a unit with a timestamp that matches the mtime of the file.
272276
///
273-
/// If `filesToIndex` is `nil`, all files in the build system with out-of-date units are indexed.
274-
///
275-
/// If `ensureAllUnitsRegisteredInIndex` is `true`, ensure that all units are registered in the index before
276-
/// triggering the indexing. This is a costly operation since it iterates through all the unit files on the file
277+
/// This is a costly operation since it iterates through all the unit files on the file
277278
/// system but if existing unit files are not known to the index, we might re-index those files even if they are
278279
/// up-to-date. Generally this should be set to `true` during the initial indexing (in which case we might be need to
279280
/// build the indexstore-db) and `false` for all subsequent indexing.
280-
package func scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(
281-
filesToIndex: [DocumentURI]?,
282-
ensureAllUnitsRegisteredInIndex: Bool,
283-
indexFilesWithUpToDateUnit: Bool
284-
) async {
281+
package func scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(indexFilesWithUpToDateUnit: Bool) async {
285282
let taskId = UUID()
286283
let generateBuildGraphTask = Task(priority: .low) {
287284
await withLoggingSubsystemAndScope(subsystem: indexLoggingSubsystem, scope: "build-graph-generation") {
288285
await hooks.buildGraphGenerationDidStart?()
289286
await self.buildSystemManager.waitForUpToDateBuildGraph()
290-
if ensureAllUnitsRegisteredInIndex {
291-
index.pollForUnitChangesAndWait()
292-
}
287+
// Polling for unit changes is a costly operation since it iterates through all the unit files on the file
288+
// system but if existing unit files are not known to the index, we might re-index those files even if they are
289+
// up-to-date. This operation is worth the cost during initial indexing and during the manual re-index command.
290+
index.pollForUnitChangesAndWait()
293291
await hooks.buildGraphGenerationDidFinish?()
294292
// TODO: Ideally this would be a type like any Collection<DocumentURI> & Sendable but that doesn't work due to
295293
// https://github.com/swiftlang/swift/issues/75602
296294
let filesToIndex: [DocumentURI] =
297-
if let filesToIndex {
298-
filesToIndex
299-
} else {
300-
await orLog("Getting files to index") {
301-
try await self.buildSystemManager.buildableSourceFiles().sorted { $0.stringValue < $1.stringValue }
302-
} ?? []
303-
}
304-
_ = await self.scheduleIndexing(
295+
await orLog("Getting files to index") {
296+
try await self.buildSystemManager.buildableSourceFiles().sorted { $0.stringValue < $1.stringValue }
297+
} ?? []
298+
await self.scheduleIndexing(
305299
of: filesToIndex,
300+
waitForBuildGraphGenerationTasks: false,
306301
indexFilesWithUpToDateUnit: indexFilesWithUpToDateUnit,
307302
priority: .low
308303
)
309-
scheduleIndexingTasks[taskId] = nil
304+
buildGraphGenerationTasks[taskId] = nil
310305
}
311306
}
312-
scheduleIndexingTasks[taskId] = generateBuildGraphTask
307+
buildGraphGenerationTasks[taskId] = generateBuildGraphTask
313308
indexProgressStatusDidChange()
314309
}
315310

@@ -318,15 +313,11 @@ package final actor SemanticIndexManager {
318313
package func scheduleReindex() async {
319314
await indexStoreUpToDateTracker.markAllKnownOutOfDate()
320315
await preparationUpToDateTracker.markAllKnownOutOfDate()
321-
await scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(
322-
filesToIndex: nil,
323-
ensureAllUnitsRegisteredInIndex: false,
324-
indexFilesWithUpToDateUnit: true
325-
)
316+
await scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(indexFilesWithUpToDateUnit: true)
326317
}
327318

328319
private func waitForBuildGraphGenerationTasks() async {
329-
await scheduleIndexingTasks.values.concurrentForEach { await $0.value }
320+
await buildGraphGenerationTasks.values.concurrentForEach { await $0.value }
330321
}
331322

332323
/// Wait for all in-progress index tasks to finish.
@@ -336,6 +327,8 @@ package final actor SemanticIndexManager {
336327
// can await the index tasks below.
337328
await waitForBuildGraphGenerationTasks()
338329

330+
await fileOrBuildTargetChangedTasks.concurrentForEach { await $0.value.value }
331+
339332
await inProgressIndexTasks.concurrentForEach { (_, inProgress) in
340333
switch inProgress.state {
341334
case .creatingIndexTask:
@@ -358,86 +351,102 @@ package final actor SemanticIndexManager {
358351
logger.info(
359352
"Waiting for up-to-date index for \(uris.map { $0.fileURL?.lastPathComponent ?? $0.stringValue }.joined(separator: ", "))"
360353
)
361-
// If there's a build graph update in progress wait for that to finish so we can discover new files in the build
362-
// system.
363-
await waitForBuildGraphGenerationTasks()
364354

365355
// Create a new index task for the files that aren't up-to-date. The newly scheduled index tasks will
366356
// - Wait for the existing index operations to finish if they have the same number of files.
367357
// - Reschedule the background index task in favor of an index task with fewer source files.
368-
await self.scheduleIndexing(of: uris, indexFilesWithUpToDateUnit: false, priority: nil).value
358+
await self.scheduleIndexing(
359+
of: uris,
360+
waitForBuildGraphGenerationTasks: true,
361+
indexFilesWithUpToDateUnit: false,
362+
priority: nil
363+
).value
369364
index.pollForUnitChangesAndWait()
370365
logger.debug("Done waiting for up-to-date index")
371366
}
372367

373-
package func filesDidChange(_ events: [FileEvent]) async {
374-
// We only re-index the files that were changed and don't re-index any of their dependencies. See the
375-
// `Documentation/Files_To_Reindex.md` file.
376-
let changedFiles = events.map(\.uri)
377-
await indexStoreUpToDateTracker.markOutOfDate(changedFiles)
378-
379-
// Preparation tracking should be per file. For now consider any non-known-language change as having to re-prepare
380-
// the target itself so that we re-prepare potential input files to plugins.
381-
// https://github.com/swiftlang/sourcekit-lsp/issues/1975
382-
var outOfDateTargets = Set<BuildTargetIdentifier>()
383-
for file in changedFiles {
384-
let changedTargets = await buildSystemManager.targets(for: file)
385-
if Language(inferredFromFileExtension: file) == nil {
386-
outOfDateTargets.formUnion(changedTargets)
387-
}
388-
389-
let dependentTargets = await buildSystemManager.targets(dependingOn: changedTargets)
390-
outOfDateTargets.formUnion(dependentTargets)
391-
}
392-
if !outOfDateTargets.isEmpty {
393-
logger.info(
394-
"""
395-
Marking dependent targets as out-of-date: \
396-
\(String(outOfDateTargets.map(\.uri.stringValue).joined(separator: ", ")))
397-
"""
398-
)
399-
await preparationUpToDateTracker.markOutOfDate(outOfDateTargets)
400-
}
401-
402-
await scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(
403-
filesToIndex: changedFiles,
404-
ensureAllUnitsRegisteredInIndex: false,
405-
indexFilesWithUpToDateUnit: false
406-
)
407-
}
408-
409-
package func buildTargetsChanged(_ changes: [BuildTargetEvent]?) async {
410-
let targets = changes?.map(\.target)
368+
package func filesDidChange(_ events: [FileEvent]) {
369+
// We don't care about the order of file change events, so we can execute the changes in a task, allowing further
370+
// file change handling to continue before we have scheduled the file re-indexing.
371+
let uuid = UUID()
372+
fileOrBuildTargetChangedTasks[uuid] = Task {
373+
defer { fileOrBuildTargetChangedTasks[uuid] = nil }
374+
// We only re-index the files that were changed and don't re-index any of their dependencies. See the
375+
// `Documentation/Files_To_Reindex.md` file.
376+
let changedFiles = events.map(\.uri)
377+
await indexStoreUpToDateTracker.markOutOfDate(changedFiles)
378+
379+
// Preparation tracking should be per file. For now consider any non-known-language change as having to re-prepare
380+
// the target itself so that we re-prepare potential input files to plugins.
381+
// https://github.com/swiftlang/sourcekit-lsp/issues/1975
382+
var outOfDateTargets = Set<BuildTargetIdentifier>()
383+
for file in changedFiles {
384+
let changedTargets = await buildSystemManager.targets(for: file)
385+
if Language(inferredFromFileExtension: file) == nil {
386+
outOfDateTargets.formUnion(changedTargets)
387+
}
411388

412-
if let targets {
413-
var targetsAndDependencies = targets
414-
targetsAndDependencies += await buildSystemManager.targets(dependingOn: Set(targets))
415-
if !targetsAndDependencies.isEmpty {
389+
let dependentTargets = await buildSystemManager.targets(dependingOn: changedTargets)
390+
outOfDateTargets.formUnion(dependentTargets)
391+
}
392+
if !outOfDateTargets.isEmpty {
416393
logger.info(
417394
"""
418395
Marking dependent targets as out-of-date: \
419-
\(String(targetsAndDependencies.map(\.uri.stringValue).joined(separator: ", ")))
396+
\(String(outOfDateTargets.map(\.uri.stringValue).joined(separator: ", ")))
420397
"""
421398
)
422-
await preparationUpToDateTracker.markOutOfDate(targetsAndDependencies)
399+
await preparationUpToDateTracker.markOutOfDate(outOfDateTargets)
423400
}
424-
} else {
425-
await preparationUpToDateTracker.markAllKnownOutOfDate()
426-
}
427401

428-
await orLog("Scheduling re-indexing of changed targets") {
429-
var sourceFiles = try await self.buildSystemManager.sourceFiles(includeNonBuildableFiles: false)
430-
if let targets {
431-
sourceFiles = sourceFiles.filter { !$0.value.targets.isDisjoint(with: targets) }
432-
}
433-
_ = await scheduleIndexing(
434-
of: sourceFiles.keys,
402+
await scheduleIndexing(
403+
of: changedFiles,
404+
waitForBuildGraphGenerationTasks: true,
435405
indexFilesWithUpToDateUnit: false,
436406
priority: .low
437407
)
438408
}
439409
}
440410

411+
package func buildTargetsChanged(_ changes: [BuildTargetEvent]?) async {
412+
// We don't care about the order of target change events, so we can execute the changes in a task, allowing further
413+
// target change handling to continue.
414+
let uuid = UUID()
415+
fileOrBuildTargetChangedTasks[uuid] = Task {
416+
defer { fileOrBuildTargetChangedTasks[uuid] = nil }
417+
let targets = changes?.map(\.target)
418+
419+
if let targets {
420+
var targetsAndDependencies = targets
421+
targetsAndDependencies += await buildSystemManager.targets(dependingOn: Set(targets))
422+
if !targetsAndDependencies.isEmpty {
423+
logger.info(
424+
"""
425+
Marking dependent targets as out-of-date: \
426+
\(String(targetsAndDependencies.map(\.uri.stringValue).joined(separator: ", ")))
427+
"""
428+
)
429+
await preparationUpToDateTracker.markOutOfDate(targetsAndDependencies)
430+
}
431+
} else {
432+
await preparationUpToDateTracker.markAllKnownOutOfDate()
433+
}
434+
435+
await orLog("Scheduling re-indexing of changed targets") {
436+
var sourceFiles = try await self.buildSystemManager.sourceFiles(includeNonBuildableFiles: false)
437+
if let targets {
438+
sourceFiles = sourceFiles.filter { !$0.value.targets.isDisjoint(with: targets) }
439+
}
440+
await scheduleIndexing(
441+
of: sourceFiles.keys,
442+
waitForBuildGraphGenerationTasks: true,
443+
indexFilesWithUpToDateUnit: false,
444+
priority: .low
445+
)
446+
}
447+
}
448+
}
449+
441450
/// Returns the files that should be indexed to get up-to-date index information for the given files.
442451
///
443452
/// If `files` contains a header file, this will return a `FileToIndex` that re-indexes a main file which includes the
@@ -696,12 +705,23 @@ package final actor SemanticIndexManager {
696705
/// known to be up-to-date based on `indexStoreUpToDateTracker` or the unit timestamp will not be re-indexed unless
697706
/// `indexFilesWithUpToDateUnit` is `true`.
698707
///
708+
/// If `waitForBuildGraphGenerationTasks` is `true` and there are any tasks in progress that wait for an up-to-date
709+
/// build graph, wait for those to finish. This is helpful so to avoid the following: We receive a build target change
710+
/// notification before the initial background indexing has finished. If indexstore-db hasn't been initialized with
711+
/// `pollForUnitChangesAndWait` yet, we might not know that the changed targets' files' index is actually up-to-date
712+
/// and would thus schedule an unnecessary re-index of the file.
713+
///
699714
/// The returned task finishes when all files are indexed.
715+
@discardableResult
700716
package func scheduleIndexing(
701717
of files: some Collection<DocumentURI> & Sendable,
718+
waitForBuildGraphGenerationTasks: Bool,
702719
indexFilesWithUpToDateUnit: Bool,
703720
priority: TaskPriority?
704721
) async -> Task<Void, Never> {
722+
if waitForBuildGraphGenerationTasks {
723+
await self.waitForBuildGraphGenerationTasks()
724+
}
705725
// Perform a quick initial check to whether the files is up-to-date, in which case we don't need to schedule a
706726
// prepare and index operation at all.
707727
// We will check the up-to-date status again in `IndexTaskDescription.execute`. This ensures that if we schedule

Sources/SourceKitLSP/Workspace.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,6 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
175175
}
176176
if let semanticIndexManager {
177177
await semanticIndexManager.scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(
178-
filesToIndex: nil,
179-
ensureAllUnitsRegisteredInIndex: true,
180178
indexFilesWithUpToDateUnit: false
181179
)
182180
}

0 commit comments

Comments
 (0)